Skip to content

shift-lifecycle

Spans: shiftswalletscex-walletspartners Trigger: POST /v3/order on apps/sw-apiShiftCreator.run() Terminal states: confirmed (happy path) · refunded (slippage or explicit refund) · failed · expired · suspended

The end-to-end saga of a Switchain shift: from order creation through deposit detection, CEX trading, transfer dispatch, on-chain settlement, and partner due accounting. This flow drives the three core state machines in lockstep:

Pull vs. event-driven

The flow is a hybrid: domain events enqueue BullMQ jobs for downstream work, but most cross-state-machine signaling happens via polling the next layer's status. Knowing which is which is load-bearing — when something is stuck, look at the right boundary.

BoundaryMechanism
Shift status transitions (waiting → received → exchanging → confirming → confirmed/refunded)PollingShiftSupervisor runs on a BullMQ recurring job (~60 s) and calls each phase's service in turn
ShiftCreatedDomainEvent → start supervisingEvent-drivenShiftSupervisorOnCreated enqueues the supervisor job
ShiftReceivedDomainEvent → CEX spot tradesEvent-drivenCreateTradesOnShiftDepositReceived enqueues CexShiftTradesCreatorQueue
TransferCreatedDomainEvent → send the transferEvent-drivenTransferSenderQueueOnCreated enqueues TransferSenderQueue
TransferSentDomainEvent → poll the transferEvent-drivenTransferUpdaterQueueOnSent enqueues TransferUpdaterQueue
Transfer status (open → sending → confirmed/failed)PullShiftTransferUpdater reads Transaction.status (for sws sender) or calls the CEX withdraw API (for CEX senders)
Shift.confirming → confirmedPullShiftConfirmer reads Transfer.status inside the supervisor loop (no TransferConfirmedDomainEvent subscriber triggers it)
ShiftConfirmedDomainEvent → create partner dueEvent-drivenDueCreatorOnShiftConfirmed enqueues DueCreatorQueue

Rule of thumb: state transitions within a context are polling-driven; cross-context hand-offs are event-driven. The only exception is Shift.confirming → confirmed, which reads Transfer.status by pulling — an internal boundary that behaves like a cross-context one because Transfer's terminal state is what decides it.

Sequence (happy path)

Events

#EventEmitted byConsumed byEffect
1ShiftCreatedDomainEventShiftCreator on POST /v3/orderShiftSupervisorOnCreated (shifts-worker); ShiftKytAnalyzerOnCreatedEnqueues supervisor recurring job; KYT compliance check
2ShiftReceivedDomainEventShiftDepositDetector during supervisor pollCreateTradesOnShiftDepositReceived (wallets-worker)Shift waiting → received; enqueues CEX trades loop
3ShiftExchangingDomainEventShiftDepositConfirmer during supervisor poll— (supervisor continues internally)Shift received → exchanging; deposit confirmed on-chain
4(no event)CexAccountSpotTradeCreator via CexShiftTradesCreatorQueue— (poll-based completion check)Converts deposit coin to output coin via CEX spot trades
5TransferCreatedDomainEvent + ShiftConfirmingDomainEventShiftLooseTransferCreator / ShiftPreciseTransferCreatorTransferSenderQueueOnCreated (wallets-worker)Creates Transfer (status open); shift exchanging → confirming; enqueues transfer sender
6TransferSentDomainEventShiftTransferSender via TransferSenderQueueTransferUpdaterQueueOnSent (wallets-worker)Transfer open → sending; records withdrawId; for sws sender creates a Transaction (pending → submitted); enqueues transfer updater
7TransferConfirmedDomainEventShiftTransferUpdater via TransferUpdaterQueue (pulls Transaction.status or CEX withdraw API)No direct subscriber — read by ShiftConfirmer via pollingTransfer sending/confirming → confirmed; stores txHash
8ShiftConfirmedDomainEventShiftConfirmer during supervisor poll (after reading Transfer.status)DueCreatorOnShiftConfirmed (shifts-worker)Shift confirming → confirmed; enqueues DueCreatorQueue with 5 s delay
9DueCreatedDomainEventDueCreator via DueCreatorQueueShiftProfitsCalculatorOnDueCreated (and partner payout chain)Creates Due in partners context; kicks off profit/payout cycle

Failure modes

ScenarioTerminal stateWhere it's decidedRecovery / runbook
No deposit arrives before TTLexpiredShiftDepositDetector timeoutAdmin push back to received/exchanging if deposit arrives late (ShiftStatus recovery edges)
Deposit tx invalidated / wrong amountfailedShiftDepositDetector / ShiftDepositConfirmerAdmin recovery to expired, then refund via ShiftRefunder
KYT flags deposit addresssuspendedKYT integration during deposit checkShiftUnsuspender if address clears, else refund
Slippage breach at loose transfer creationrefunded (via refunding)ShiftLooseTransferCreator detects price deviationShiftRefunder creates a new refund Transfer; flow re-enters steps 6–7 with motive=refund
CEX refuses withdrawal (coin disabled)sending → suspended (Transfer) + Shift → suspended via SuspendShiftOnTransferSuspendedShiftTransferSender.cexTransfer() catches CexWithdrawalsSuspendedError (ShiftTransferSender.ts:107-112)Wait for CEX to re-enable (→ retry suspended → sending) or refund
CEX account banned mid-flowTransfer stuck; Shift stuck confirmingShiftTransferSenderOperator intervention; ShiftTransferReplacer to switch sender or refund
Transfer broadcast failsopen → failed (Transfer)ShiftTransferSenderShiftTransferReplacer creates a new Transfer with a different sender; old one marked replaced
On-chain tx reverts or drops from mempoolTransfer → failed; underlying Transactionfailed (via notOnChain + timeout in TransactionUpdater)TransactionUpdaterShiftTransferUpdater reads itReplace via CPFP/RBF (ShiftTransferReplacer) or manual failed → confirmed if the tx actually landed
Banned shift (compliance)banned → refunding → refundedAdmin force=true transition; ShiftRefunder mandatory refund

Terminal paths

Only confirmed reaches the partners leg (step 9). refunded replays steps 6–7 with a refund transfer but never emits ShiftConfirmedDomainEvent.

Code Pointers