Skip to content

TransferSender

Owner context: shifts | Entity: Transfer | Field: sender | Value object

Every Transfer is dispatched by exactly one sender. The sender is the dispatch channel used to move funds out — either a Switchain-custodial wallet or an external CEX account. Orthogonal to TransferStatus: status describes the lifecycle, sender describes who executes it.

Values

Source of truth: senders const at packages/shifts/domain/valueObjects/TransferSender.ts:1.

SenderKindSelectable todayNotes
swsCustodialYesSwitchain-custodial wallet. isSws() true.
binanceCEXYesisCex() true. The only CEX returned by Cex.active().
htxCEXNo (accepted on existing transfers, not chosen by the retriever)isCex() true. Present in TransferSender.activeSenders() but excluded from Cex.active(); the retriever never picks it.
kucoinCEXNoisCex() true. Historical only.
ownHistoricalNoPre-custodial transfers.
legacyFallbackNoCatch-all for unknown senders via fromStringOrLegacy().

TransferSender.activeSenders() returns ['sws', 'binance', 'htx'] (TransferSender.ts:21-23). "Selectable today" in the table above reflects which senders the retriever will actually choose for new transfers — not every active sender.

Selection strategy

When a shift needs a new transfer, ShiftTransferSenderRetriever.run() picks the sender before the transfer is created:

effectiveBalance = balance − sum of open transfers already assigned to that sender (retriever.ts:45-57, 68-80). This reservation prevents double-spending pending balance across in-flight transfers.

Key details:

  • Custodial is preferred. The CEX branch only runs when the custodial check fails.
  • Only one CEX is tried. Cex.binance is hardcoded at retriever.ts:59 — there is no fallback from Binance to HTX/Kucoin.
  • Strict inequality. The check is effectiveBalance.isGreaterThan(amount), not >=. A transfer exactly equal to available balance is rejected.
  • undefined ⇒ no transfer. The caller interprets a missing sender as "cannot dispatch right now" — no transfer is created.

Dispatch

Once the transfer exists with a sender assigned, ShiftTransferSender.run() branches on the sender kind:

BranchConditionAdapterPath
Custodialtransfer.sender.isSws()WalletWithdrawer from @sws/walletsswsTransfer() at L44-L71
CEXtransfer.sender.isCex()CexMainAccountWithdrawer from @sws/cex-walletscexTransfer() at L73-L115

run() is a no-op unless the transfer is in open. In the CEX branch, Cex.fromString(sender.toString()) converts the shifts-domain TransferSender into the common-domain Cex value object that the adapter accepts.

Relation to TransferStatus

Sender is orthogonal to status, with one exception:

  • suspended is CEX-only. The only code path that writes TransferStatus.suspended is cexTransfer()'s catch block on CexWithdrawalsSuspendedError (ShiftTransferSender.ts:107-112). An sws transfer never enters suspended — custodial withdrawal failures return early with ShiftTransferInsufficientBalanceDomainEvent or throw, they do not suspend.

All other TransferStatus transitions (transfer-status.md) apply regardless of sender.

Ownership

ConceptPackageFile
TransferSender value objectpackages/shifts/domainvalueObjects/TransferSender.ts
Sender selectionpackages/shifts/applicationservices/transfers/ShiftTransferSenderRetriever.ts
Sender dispatchpackages/shifts/applicationservices/transfers/ShiftTransferSender.ts
Supported CEX list (cross-context)packages/common/domainvalueObjects/Cex.ts
Custodial withdrawal adapterpackages/walletsWalletWithdrawer
CEX withdrawal adapterpackages/cex-walletsCexMainAccountWithdrawer, CexMainAccountWithdrawChecker

The shifts domain owns which senders exist. The common domain owns which CEXes are recognised platform-wide. The cex-wallets and wallets packages own how to actually withdraw.

Legacy name translation

Historical records stored CEX names as binance_dac and huobi. TransferSender.translateFromLegacy() and Cex.fromSwitchain() map those to the current binance / htx identifiers on read (TransferSender.ts:72-90, Cex.ts:4-10). Use translateToLegacy() / toSwitchain() when writing to external systems that still expect the old names.