implementation

Fireblocks whitelisted wallets

docs/implementation/fireblocks-whitelisted-wallets.md

Fireblocks whitelisted wallets

Objective

DAX outbound transfers to saved payees and bulk-payout recipients use Fireblocks external wallets instead of one-time addresses. OTA remains available to explicit developer tooling, but customer-facing sends never silently fall back to it.

Delivered architecture

  1. payee_wallets owns the immutable destination lifecycle independently from contact details in payees.
  2. Wallets start as draft, move through Fireblocks Admin Quorum approval as pending, and become sendable only when active.
  3. Live and mock Fireblocks providers implement create, add-asset, read, and remove-asset operations for external wallets.
  4. Transfer requests use an explicit destination union: EXTERNAL_WALLET or ONE_TIME_ADDRESS.
  5. Normal sends resolve the selected active payee wallet again immediately before broadcast and fail if the address or wallet identity changed.
  6. Bulk-payout validation resolves every ready address to an active whitelist entry and snapshots its Fireblocks identifiers. Unmatched rows are blocked.
  7. Fireblocks whitelist webhooks synchronize added/removed assets through the durable provider-webhook inbox.

Rollout

  1. Deploy migration 0042_payee_wallet_whitelist.sql before the application. Existing active payee addresses are copied as draft; none are trusted or submitted to Fireblocks automatically.
  2. Ensure the application API user has Fireblocks permissions to create external wallets and add assets. Configure the Admin Quorum for whitelist approvals.
  3. Review the Fireblocks Transaction Authorization Policy so transfers from the parent vault to external wallets are allowed for the application initiator.
  4. In DAX, open each payee and select request whitelist. Approve the resulting configuration change in Fireblocks, then use sync status if the webhook has not yet arrived.
  5. Confirm the wallet shows active before enabling outbound processing.
  6. Verify one transfer per supported network family before enabling bulk payouts: BTC, EVM, Litecoin, and Tron.

Operational invariants

  • A provider-approved address is never edited in place. Editing the payee address archives the previous local wallet and creates a new draft.
  • pending, rejected, removed, and error wallets cannot be selected for a normal send.
  • A send snapshots the local and Fireblocks wallet IDs. The worker re-resolves the active wallet and rejects a changed destination before calling Fireblocks.
  • Bulk payout rows without an active external-wallet snapshot never broadcast.
  • Fireblocks status remains authoritative; DAX cannot manually force a wallet to active.
  • Idempotency keys are deterministic and capped at Fireblocks' 40-character limit.

Verification

  • pnpm typecheck
  • node node_modules/vitest/vitest.mjs run
  • Production next build
  • Provider helper tests cover OTA and external-wallet request shapes.
  • Payout-engine tests cover active whitelist resolution.

Follow-up operations

  • Add an admin-wide whitelist queue if operations need to manage approvals across merchants without using the Fireblocks console.
  • Add a scheduled reconciliation job for wallets that remain pending beyond the expected Admin Quorum SLA; webhook delivery and manual sync already cover the correctness path.
  • Introduce an explicitly approved OTA product flow only if the business requires arbitrary one-off withdrawals.