Tracking & Lifecycle

The TransactionTracker from sendTransaction — its change/confirmed/failed events and whenSubmitted/whenMined/whenConfirmed/whenFinalized promises — plus TransactionStatus, TransactionReceipt, TransactionStateChange and SimulationResult, and the timeout-vs-failed distinction.

sendTransaction() (and tx.send()) returns a TransactionTracker synchronously — the broadcast and confirmation polling run in the background. You follow the transaction either by awaiting the tracker's lifecycle promises or by subscribing to its events. The tracker also satisfies Settling<TransactionReceipt>, so it exposes settled, value, confirm(), and abort().

track.ts
const tracker = evm.sendTransaction(tx);

const hash = await tracker.whenSubmitted();   // string — the broadcast id
const receipt = await tracker.whenConfirmed(); // TransactionReceipt

Events and promises

The lifecycle promises resolve once and cache their result, so awaiting one after the fact still resolves (or rejects with the real cause).

PromiseResolves withWhen
whenSubmitted()Promise<string>the transaction has a broadcast hash
whenMined()Promise<TransactionReceipt>reaches mined (first inclusion) or later
whenConfirmed(blocks?)Promise<TransactionReceipt>reaches confirmed
whenFinalized()Promise<TransactionReceipt>reaches finalized

Events are subscribed with on(event, listener), which returns an unsubscribe function:

events.ts
const off = tracker.on("change", (c) => {
  console.log(`${c.previous} → ${c.next}`);
});

tracker.on("confirmed", (receipt) => { /* success only */ });
tracker.on("failed", (err) => { /* err is a WativeError */ });

off(); // unsubscribe
EventListener argumentFires
"change"(c: TransactionStateChange) => voidon every status transition
"confirmed"(r: TransactionReceipt) => voidonly on a successful receipt
"failed"(e: WativeError) => voidon the first terminal failure

whenConfirmed(n) and whenFinalized()

Confirmation-depth tracking is not implemented. whenConfirmed() resolves at first inclusion (one block). Asking for more is refused rather than resolved early — because a caller who got an early resolve might treat the funds as reorg-safe:

await tracker.whenConfirmed(3);
// UNSUPPORTED_OP: whenConfirmed(3): confirmation-depth tracking is not supported;
// this resolves at first inclusion (1 block). Poll receipt.blockNumber against
// the chain head for deeper confirmation.

Tracking ends at inclusion, so whenFinalized() does not always resolve. EVM receipt polling ends at confirmed; Solana polling ends at whichever of confirmed / finalized the node reports first. When tracking ends at confirmed, awaiting whenFinalized() rejects with an UNSUPPORTED_OP explaining that tracking stopped and the state will never be reached — it resolves only when the node genuinely reported finalized.

timeout vs failed — the distinction that matters

For a wallet library, the dangerous mistake is telling a caller a broadcast transaction is safe to resend when it might already be on the network. The two terminal statuses draw exactly that line.

StatusMeaningSafe to resend?
failedThe send never left the process, or the endpoint read the transaction and refused it (RPC_REJECTED: nonce too low, insufficient funds, on-chain revert).Yes — rebuild and send again.
timeoutThe broadcast got no usable answer, or no receipt arrived within the budget. The signed bytes may have been relayed.No — look it up by its hash first.
dropped(Solana) the blockhash expired; the transaction can never be accepted.Yes — safe to replace.
abortedThe caller stopped observing via abort(). Asserts nothing about the chain.

timeout (introduced in 2.4.0 as the word for broadcast, outcome unknown) is never entered from a poll being unable to reach the node — a transient poll failure keeps polling. Its unknown-outcome messages name the signed hash so you can look the transaction up before sending another. tracker.hash is left null on an unacknowledged broadcast; the transaction's own tx.hash still carries the signed id the message references.

TransactionStatus

TransactionStatus.ts
type TransactionStatus =
  | "draft" | "signed" | "submitted" | "pending"
  | "mined" | "confirmed" | "finalized"
  | "failed" | "dropped" | "aborted" | "timeout";

The terminal (settled) statuses are confirmed, finalized, failed, dropped, timeout, and aborted. tracker.settled is true in exactly those.

TransactionReceipt

FieldTypeNotes
hashstringtransaction hash / signature
vmVmToken"evm" / "svm"
blockNumbernumber | nullblock / slot
blockHashstring | nullEVM block hash
indexnumber | nulltransaction index in block
successbooleanwhether the transaction succeeded
gasUsedbigint (opt)EVM; dropped if the node reported it unparseably
effectiveGasPricebigint (opt)EVM
feebigint (opt)EVM; gasUsed × effectiveGasPrice when both are known
logsReadonlyArray<unknown> (opt)raw logs
revertReasonstring (opt)only when the node volunteered one
rawunknownthe underlying node response

TransactionStateChange

Each entry pushed onto tracker.history and passed to a "change" listener.

FieldTypeNotes
previousTransactionStatusstatus before the transition
nextTransactionStatusstatus after the transition
atnumberepoch milliseconds
reasonstring (opt)human-readable cause
hashstring (opt)hash, once known
blockNumbernumber (opt)block / slot, once known
confirmationsnumber (opt)reserved; never populated (depth tracking is not implemented)

SimulationResult

Returned by simulateTransaction() / tx.simulate().

FieldTypeNotes
successbooleanwhether the pre-flight would succeed
errorstring (opt)failure message
logsReadonlyArray<string> (opt)execution logs
returnDatastring (opt)EVM return data
gasUsedbigint (opt)EVM estimated gas
computeUnitsConsumednumber (opt)Solana compute units
rawunknownthe underlying node response

Last updated on