TransactionStatus
Owner context: common (value object) — consumed by wallets for on-chain transactions | Entity: Transaction | Field: status | Values: 6
The lifecycle of a Transaction in the custodial wallets domain: from creation through broadcast to final settlement on-chain. Unlike ShiftStatus and TransferStatus, there is no explicit transition matrix on the value object — transitions are enforced only by guards inside the application services that mutate the entity. The happy path is pending → submitted → confirmed.
States
| State | Terminal | Description | Entry condition |
|---|---|---|---|
pending | No | Transaction created, not yet broadcast | Initial state (Transaction.create()) |
submitted | No | Broadcast to the chain (or CEX), txHash known, awaiting settlement | TransactionSender successful broadcast |
confirmed | Yes (sink) | Settled on-chain | TransactionUpdater when provider reports confirmed |
failed | Yes (recoverable) | Settled on-chain as failed, or dropped from the mempool after timeout | TransactionUpdater (settled-failed or notOnChain + timeout) |
cancelled | Yes (sink) | Aborted before broadcast | TransactionCanceller on a pending tx |
notOnChain | — | A value the provider port can return; not a state the entity persists under normal flow | See note below |
notOnChain is special. It is a value the blockchain provider may return when polling, but Transaction.status is never stored as notOnChain by any service in the current codebase. The relevant logic is in TransactionUpdater.ts:63-70: if the provider reports notOnChain and the transaction is past its settlement window (isWaitingForSettlement() returns false), the entity is moved to failed. Otherwise the entity stays in submitted and will be polled again. Treat notOnChain as a provider signal, not a resting state.
Recoverable means the admin can still act on the state (replace via CPFP/RBF for EVM or UTXO chains, when isReplaceable() returns true); it does not mean the entity will automatically move again.
Transitions
Transition Table
There is no TRANSITIONS matrix on the value object. The transitions below are the ones reachable in practice via the application services that mutate the entity; every other transition is simply never called.
| From | To | Trigger (service) | Invariant / guard | Side effects |
|---|---|---|---|---|
pending | submitted | TransactionSender | Guard: must be isPending(); broadcast succeeds | Sets txHash, sentAt; emits TransactionSubmittedDomainEvent |
pending | cancelled | TransactionCanceller | Guard: must be isPending() | — |
submitted | confirmed | TransactionUpdater | Provider returns status with isSettled() && is('confirmed') | Sets fee; emits TransactionConfirmedDomainEvent |
submitted | failed | TransactionUpdater (settled path) | Provider returns isSettled() && is('failed') | Sets fee; emits TransactionFailedDomainEvent |
submitted | failed | TransactionUpdater (timeout path) | Provider returns notOnChain and !transaction.isWaitingForSettlement() | Emits TransactionFailedDomainEvent |
A failed broadcast inside TransactionSender does not move the status — it emits TransactionBalanceInsufficientDomainEvent while the entity stays in pending (TransactionSender.ts).
Invariants
- Initial state is always
pending—Transaction.create()hardcodes it (Transaction.ts:54-76). - No state-transition validation in the value object. The value object has no
canTransition()orTRANSITIONSmap. Guards live only in the services:TransactionSenderchecksisPending()before broadcasting;TransactionCancellerchecksisPending()before cancelling. This is the looser model compared toShiftStatusandTransferStatus. - True sinks are
confirmedandcancelled. Nothing in the codebase moves a transaction out of them. failedis quasi-terminal. No service moves a transaction out offailed, butisReplaceable()is true forfailed(and forsubmitted) — this is the signal that an admin can create a replacement transaction via CPFP/RBF. Replacement does not mutate this transaction's status; it creates a new one.isSettled()excludescancelled. Returns true only forconfirmed || failed(TransactionStatus.ts:44-46). The distinction matters: a cancelled transaction was never broadcast, so it has no on-chain outcome.notOnChainis never persisted. The value is in the enum for completeness with the provider port, butTransactionUpdaterconverts it to either no-op (still waiting) orfailed(past timeout). If you ever seestatus === 'notOnChain'persisted, that is a bug.- Settlement timeout is per-chain.
Transaction.isWaitingForSettlement()returns true inside the settlement window (2h for UTXO, 20 min for other chains) based onsentAt(Transaction.ts:151-157). Past that,notOnChainfrom the provider promotes tofailed. - No direct event bridge to
shifts.TransactionConfirmedDomainEventis consumed byWalletUpdaterOnTransactionSettledto refresh the wallet balance, not to move aTransfer.Transferstate advancement pollsTransaction.statusthroughTransferWithdrawStatusRetrieverinstead.
Cross-context effect: Transaction → Wallet (and indirectly Transfer)
| Transaction transition | Downstream effect |
|---|---|
pending → submitted | TransactionSubmittedDomainEvent — subscribed by wallet-facing updaters; sets txHash on the entity so downstream consumers (polling) can resolve it |
submitted → confirmed | TransactionConfirmedDomainEvent — consumed by WalletUpdaterOnTransactionSettled to trigger a delayed wallet refresh job. No event subscriber drives Transfer forward; that happens via polling. |
submitted → failed | TransactionFailedDomainEvent — same wallet refresh path; no direct Transfer effect |
Full cross-context saga: shift-lifecycle (to be documented).
Code Pointers
- Value object (in
common): packages/common/domain/valueObjects/TransactionStatus.ts - Entity (in
wallets): packages/wallets/domain/entities/Transaction.ts (create,updateStatus,setTxHash,isWaitingForSettlement,isReplaceable) - Broadcast and guard: packages/wallets/application/transaction/TransactionSender.ts
- Polling and settlement: packages/wallets/application/transaction/TransactionUpdater.ts
- Cancellation: packages/wallets/application/transaction/TransactionCanceller.ts
- Creation: packages/wallets/application/transaction/TransactionCreator.ts
- Events: packages/wallets/domain/events/TransactionCreatedDomainEvent.ts,
TransactionSubmittedDomainEvent.ts,TransactionConfirmedDomainEvent.ts,TransactionFailedDomainEvent.ts - Wallet refresh on settlement: apps/wallets-worker/application/eventSubscribers/wallets/WalletUpdaterOnTransactionSettled.ts
- Bridge to transfers (pull model): packages/shifts/application/services/transfers/TransferWithdrawStatusRetriever.ts
Related
- Provider-side semantics: packages/wallets/domain/ports/BlockchainProviderPort.ts —
transactionStatus()returns aTransactionStatus. - Owning context for the entity:
wallets. - Owning context for the value object:
common. - Sibling state machine that pulls this state:
TransferStatus.
