Skip to content

ShiftStatus

Owner context: shifts | Entity: Shift | Field: status | Values: 12

The lifecycle of a shift (exchange transaction): from creation through deposit, exchange, transfer, and final state. Every shift starts at waiting and ends at a terminal state — the happy path is waiting → received → exchanging → confirming → confirmed, with recovery branches for failures, expirations, and refunds.

States

StateTerminalDescriptionEntry condition
waitingNoShift created, awaiting deposit on the user's addressInitial state (Shift.create())
receivedNoDeposit detected on-chain, not yet confirmed enoughShiftDepositDetector finds matching tx
exchangingNoDeposit confirmed, exchange leg initiated at the CEXShiftDepositConfirmer after enough confirmations
confirmingNoOutbound transfer created, awaiting chain confirmationShiftLooseTransferCreator / ShiftPreciseTransferCreator
refundingNoRefund transfer created, awaiting confirmationShiftRefunder
confirmedYes (sink)Happy path completed: user received fundsShiftConfirmer on transfer confirmed
refundedYes (sink)Refund completedShiftRefundConfirmer on refund transfer confirmed
pausedYes (sink)Manually paused by adminShiftStatusUpdateController with force=true
failedYes (recoverable)Failure (invalid deposit, transfer error, etc.)Any active state on error; admin can push to expired or refunding
expiredYes (recoverable)TTL elapsed without deposit, or admin recoveryShiftDepositDetector timeout; admin unsuspend
suspendedYes (recoverable)Held by KYT / complianceBlocked address detected; admin ShiftUnsuspender can release
bannedYes (recoverable)Banned by abuse / complianceAdmin force=true; only refunding allowed out

Sink means TRANSITIONS has no key for that state — nothing moves it. Recoverable means isFinal() === true but the matrix still allows specific outgoing transitions (admin recovery, refund, unsuspend).

Transitions

Transition Table

Source of truth: ShiftStatus.TRANSITIONS at packages/shifts/domain/valueObjects/ShiftStatus.ts:21-33.

FromToTrigger (service / command)InvariantSide effects
waitingreceivedShiftDepositDetector detects depositdepositTxId must be setEmits ShiftReceivedDomainEvent
waitingexpiredShiftDepositDetector timeoutNo deposit detected within TTLEmits ShiftExpiredDomainEvent
waitingfailedShiftDepositDetector on invalid depositEmits ShiftFailedDomainEvent
waitingsuspendedKYT blocks deposit addressBlocked address present
receivedexchangingShiftDepositConfirmer after N confirmationsdepositTxId setEmits ShiftExchangingDomainEvent
receivedfailedShiftDepositConfirmer on deposit invalidationEmits ShiftFailedDomainEvent
receivedrefundingShiftRefunder (manual / admin)Shift must be refundableEmits ShiftRefundingDomainEvent + TransferCreatedDomainEvent
exchangingconfirmingShiftLooseTransferCreator / ShiftPreciseTransferCreatorOutbound transfer createdEmits ShiftConfirmingDomainEvent + TransferCreatedDomainEvent
exchangingrefundingShiftRefunderState in refundableStatusesEmits ShiftRefundingDomainEvent
exchangingfailedExchange errorEmits ShiftFailedDomainEvent
confirmingconfirmedShiftConfirmer on transfer confirmedtransferId must exist and be confirmedEmits ShiftConfirmedDomainEvent; sets confirmedAt
confirmingfailedKYT/transfer errorEmits ShiftFailedDomainEvent
confirmingsuspendedCompliance flag
refundingrefundedShiftRefundConfirmer on refund confirmedRefund transfer confirmedEmits ShiftRefundedDomainEvent; sets refundedAt
refundingfailedRefund transfer failedEmits ShiftFailedDomainEvent
expiredreceivedAdmin push (recovery)depositTxId must be setEmits FailedShiftHasBeenUpdatedDomainEvent
expiredexchangingAdmin push (recovery)depositTxId must be setEmits FailedShiftHasBeenUpdatedDomainEvent
expiredfailedAdmin force
expiredrefundingShiftRefunderRefundableEmits ShiftRefundingDomainEvent
suspendedexpiredShiftUnsuspender (KYT cleared)No blocked addresses
suspendedrefundingShiftRefunderRefundableEmits ShiftRefundingDomainEvent
failedexpiredAdmin recoveryEmits FailedShiftHasBeenUpdatedDomainEvent
failedrefundingShiftRefunderRefundableEmits ShiftRefundingDomainEvent
bannedrefundingShiftRefunder (mandatory refund)RefundableEmits ShiftRefundingDomainEvent

Any transition not listed here is rejected by Shift.transitionTo() with InvalidShiftTransitionError. Admin overrides use Shift.forceTransitionTo() which bypasses TRANSITIONS validation (use sparingly).

Invariants

  1. Initial state is always waitingShift.create() hardcodes it (Shift.ts:117).
  2. depositTxId required before received or exchanging — enforced in Shift.transitionTo() at Shift.ts:242-244; violation throws MissingDepositError.
  3. transferId required before confirmed — enforced in transfer pipeline before ShiftConfirmer runs.
  4. Refundable statuses are exactly [exchanging, expired, failed, suspended, banned] (ShiftStatus.refundableStatuses, ShiftStatus.ts:105-111). Any refund attempt from another state fails validation in ShiftRefunder.
  5. In-progress statuses are [waiting, received, exchanging, confirming, refunding] (ShiftStatus.inProgressStatuses, ShiftStatus.ts:97-103); used by ShiftStatusValidator to filter active shifts.
  6. isFinal() vs true sinksisFinal() returns true for [confirmed, failed, refunded, expired, suspended] (ShiftStatus.ts:123-131), but the TRANSITIONS matrix still allows outgoing transitions for failed, expired, and suspended (admin recovery, refund, unsuspend). Treat isFinal() as "the shift has reached a conclusion", not "this state never moves".
  7. True sinks (no outgoing transitions in TRANSITIONS) are confirmed, refunded, and paused. Note paused is not in isFinal() — that is a known discrepancy; if this matters for a consumer, check both isFinal() and the transition matrix.
  8. banned is marked final-by-convention but has one outgoing edge: banned → refunding. Refund is mandatory for banned shifts.

Code Pointers