Skip to main content

Outputs and locking

Starting a send reserves the inputs it selected, and nothing releases them automatically. If the transfer does not complete, those funds stay unavailable until you cancel the transaction. There is no timeout and no background process that cleans up.

Output states

Every output the wallet owns carries a status. Unspent is confirmed and available. Unconfirmed is seen but not yet buried under enough blocks, which includes the change from a send that has not confirmed. Locked is reserved as an input to a transfer you started. Spent was consumed by a confirmed transaction, and Deleted was removed by scan --delete_unconfirmed. Only Unspent is spendable.

One transfer, followed through the output states, with the balance summary recomputed at each step. Step it, or pick what happens when it does not complete.

Output states and balance. Step 1 of 5. One confirmed output

Coinbase outputs carry an extra rule. A mined reward appears in the wallet before it can be spent, and the maturity window differs per network. See emission.

What reserves outputs

One operation reserves them, tx_lock_outputs. It marks the selected inputs Locked and writes the change output as Unconfirmed. On the command line it runs inside the same send, once the slate has been handed to the transport (controller/src/command.rs:542). In a manual sequence over the Owner API you call it yourself, immediately after init_send_tx has selected the inputs and built the first slate.

During an in-flight send, both the amount you sent and your change are unavailable. Sending 1 EPIC from a single 100 EPIC output leaves you with nothing spendable until the transfer resolves, not 99 EPIC. The change output does not exist on the chain yet. Splitting a balance across several outputs is what keeps the rest of it usable during a transfer; see keep more than one spendable output.

What releases them

The transfer confirms. Inputs become Spent, change becomes Unspent. Nothing to do.

You cancel it. cancel_tx returns Locked inputs to Unspent and discards the unconfirmed change.

There is no third route. Restarting the wallet does not release outputs, because the state is on disk, and neither does setting an expiry height.

Cancelling has preconditions

cancel_tx contacts the node before it does anything, so a node that is down or unsynced blocks it (libwallet/src/api_impl/owner.rs:758). It then needs exactly one transaction matching the id or slate id you pass, that transaction's type to be TxSentCreated or TxReceived, and its confirmed field to be false (libwallet/src/internal/tx.rs:309).

The type condition sets the boundary. Once a posted transfer has been observed in the node's mempool its entry becomes TxSentMempool, and from that point you are waiting on the chain rather than choosing to abandon it. The wallet polls for that observation for up to four minutes after posting; see the send appears to hang.

Transaction states

The wallet's record of what it has done is a transaction log entry carrying a type. ConfirmedCoinbase is a mined reward. TxReceived and TxSent are a completed inbound and outbound transfer. TxSentCreated is an outbound transfer in flight, with its inputs locked. TxSentMempool and TxReceivedMempool are transfers observed in the node's mempool. TxSentCancelled and TxReceivedCancelled are the cancelled forms.

Two field semantics matter when you read the log. A TxReceived entry keeps its type after confirming, so confirmed is the field that tells you, not the type. And fee is empty on a received transaction, because the receiver does not set it; reconcile from the sender's record or from the kernel.

The CLI prints display names for these, so the Sent (Created) row in epic-wallet txs is TxSentCreated in tx_type through the Owner API. Field types are in the Wallet Owner API reference.

Reading the balance summary

epic-wallet info divides the wallet's value across the rows that account for a shortfall:

$ epic-wallet info

____ Wallet Summary Info - Account 'default' as of height 3675163 ____

Confirmed Total | 12.50000000
Awaiting Confirmation (< 10) | 0.00000000
Awaiting Finalization | 8.49300000
Locked by transaction | 12.50000000
-------------------------------- | -------------
Currently Spendable | 0.00000000

That is one wallet holding a single confirmed 12.5 EPIC output, part way through sending 4 EPIC with a 0.007 fee. The whole input is Locked by transaction, the 8.493 change is Awaiting Finalization because it is not on the chain yet, and nothing is spendable. Neither row resolves on its own.

The (< 10) is minimum_confirmations, the confirmations an output needs before this wallet will spend it. It defaults to 10 and --min_conf overrides it; see read what the wallet knows. An Immature Coinbase row appears only when the wallet holds coinbase outputs inside the maturity window.

Finding which transfer holds the inputs, releasing it and rescanning is stuck transactions. The programmatic version is send and receive.

Next