Skip to content
GitLab
Projects Groups Topics Snippets
  • /
  • Help
    • Help
    • Support
    • Community forum
    • Submit feedback
    • Contribute to GitLab
  • Register
  • Sign in
  • A Ahad Mobile
  • Project information
    • Project information
    • Activity
    • Labels
    • Members
  • Repository
    • Repository
    • Files
    • Commits
    • Branches
    • Tags
    • Contributor statistics
    • Graph
    • Compare revisions
  • Issues 0
    • Issues 0
    • List
    • Boards
    • Service Desk
    • Milestones
  • Merge requests 0
    • Merge requests 0
  • CI/CD
    • CI/CD
    • Pipelines
    • Jobs
    • Schedules
  • Deployments
    • Deployments
    • Environments
    • Releases
  • Packages and registries
    • Packages and registries
    • Package Registry
    • Terraform modules
  • Monitor
    • Monitor
    • Incidents
  • Analytics
    • Analytics
    • Value stream
    • CI/CD
    • Repository
  • Wiki
    • Wiki
  • Snippets
    • Snippets
  • Activity
  • Graph
  • Create a new issue
  • Jobs
  • Commits
  • Issue Boards
Collapse sidebar
  • holatech
  • Ahad Mobile
  • Merge requests
  • !88

feat: convert between usdc and the in-app usd wallet

  • Review changes

  • Download
  • Patches
  • Plain diff
Merged Khanh Pham requested to merge feat/convert-to-fiat-wallet into develop Oct 02, 2026
  • Overview 0
  • Commits 14
  • Pipelines 2
  • Changes 53

What

Convert now moves money in both directions between USDC and the in-app USD fiat wallet. The card is no longer part of Convert: it is funded only through Transfer (wallet → KeySecure card).

Backend

  • Quote accepts USDC → USD (OFF_RAMP) and USD → USDC (ON_RAMP). Any other pair returns 400.
  • Quotes no longer carry a card account. The expand-only migration 1790100000000 makes ramp_quotes.card_id nullable.
  • Settlement runs in one transaction and ends COMPLETED:
    • USDC moves from the user to HOUSE.
    • DR reserve.USD / CR the user's USD wallet.
    • DR reserve.USD / CR fee_revenue.USD for the fee.
    • If reserve.USD is short, the order ends REJECTED INSUFFICIENT_RESERVE and nothing is posted.
  • ON_RAMP settlement also runs in one transaction and ends COMPLETED:
    • DR the user's USD wallet / CR reserve.USD (from − fee).
    • DR the user's USD wallet / CR fee_revenue.USD (fee).
    • USDC moves from HOUSE to the user.
    • If the wallet is short, the order ends REJECTED INSUFFICIENT_FUNDS. If HOUSE USDC is short, it ends REJECTED INSUFFICIENT_RESERVE. Nothing is posted in either case.
    • Both directions take locks in the same order.
  • Reversal debits the user's USD wallet.
    • Returns 409 RAMP_REVERSAL_INSUFFICIENT_WALLET when the wallet no longer holds the amount.
    • Returns 409 RAMP_REVERSAL_LEGACY_CARD_ORDER for card-era orders.
    • An ON_RAMP reversal returns the USDC and refunds the wallet. It returns 409 RAMP_REVERSAL_INSUFFICIENT_CRYPTO when the user no longer holds the USDC.
  • The conversion module no longer depends on card-account: no CARD_TOPUP leg, no card_transfers hooks, and the timeout sweeper only fails stalled ACCEPTED/LIQUIDITY_OK orders.
  • Treasury:
    • Invariant 5 reads the USD fee book, counting only ramp-linked entries, because fee_revenue.USD is shared with other fees.
    • Per-order checks split by era: card_id IS NULL is checked against USD, otherwise against CARD_USD.
    • Only a short Fireblocks vault throttles a corridor, and only ON_RAMP. A KeySecure shortfall throttles nothing.

Frontend

  • The Convert screen is fixed to USDC → "USD wallet": no swap, no currency picker, and it shows the wallet balance.
  • The receipt has a single COMPLETED state.
  • After a submit, balances and history refresh through invalidateBalanceQueries.

UI screens changed

Screen File Before After
Convert (form) fe/src/app/convert/index.tsx From/To pickers (USDC/USDT ↔ fiat currency), swap button, card picker, card balance USDC ⇄ USD wallet with a swap button (convert.swap). Swapping clears the amount and the held quote. No currency or asset picker, USDT removed. The source balance gate follows the source side: the USDC balance or the wallet balance. The "From" side shows the USDC balance, and Confirm is disabled with "Insufficient balance" when the amount exceeds it. The "To" side shows USD wallet with the current wallet balance (0 if the user has none)
Confirm convert sheet fe/src/components/convert/confirm-convert-sheet.tsx "Card Target / Card Receive ••• last4" row Shows To: USD wallet or From: USD wallet, depending on direction
Convert completed (receipt) fe/src/app/convert/completed.tsx Three outcomes (Completed / "Conversion posted" settling / partial) plus a card row A single Completed receipt with a USD wallet row (From or To)
Convert history fe/src/app/convert/history.tsx, components/convert/order-row.tsx Example copy described card on-ramp Copy updated to the off-ramp example ("20.00 USDC became 19.96 USD"). Old orders still render
Home / Transfer (no change) — The Home header shows the fiat wallet, so it goes up after a convert. Balances and history refresh automatically. The Transfer form keeps the "Convert to USD first" link

Removed: currency-select-sheet.tsx and the copy keys cardReceive, cardSend, noCardAdded, selectCard, selectCurrency, outcomeSettling*, outcomePartial*. Added convert.receiveWallet = "USD wallet".

Before deploy (staging/prod)

  1. Each query below must return 0. If not, resolve the orders per be/src/modules/conversion/runbooks/ramp-partial-settlement.md first.
    SELECT count(*) FROM ramp_orders WHERE status IN ('SETTLING','PARTIALLY_SETTLED');
    SELECT count(*) FROM ramp_orders o WHERE o.status='FAILED'
      AND EXISTS (SELECT 1 FROM ramp_order_legs l WHERE l.order_id=o.id AND l.status='POSTED');
    SELECT count(*) FROM card_transfers WHERE source_type='RAMP_ORDER' AND source_settled_at IS NULL;
  2. Deploy BE (migration), then FE.
  3. Fund reserve.USD through admin treasury fund-reserve. Without it, every USDC → USD convert returns CONVERSION_UNAVAILABLE.
  4. Check that the HOUSE USDC system row (account_code = 'HOUSE') can cover the payouts. Otherwise every USD → USDC convert is rejected with insufficient reserve.

Verification

  • yarn build passes. Unit tests for conversion, treasury, card-account and transactions: 627 passed. DB specs (isolated DB): 445+ passed. FE convert tests: 73 passed. pnpm type-check is clean.
  • API E2E on a local stack:
    • USD→USDC quote returns 400.
    • Converting 20 USDC returns COMPLETED: the USD wallet goes up by 19.96, the fee is 0.04, and reserve.USD goes down by 20.
    • USD 20.00 → USDC returns COMPLETED: the wallet goes down by 20.00, the user gets 19.96 USDC, and reserve.USD goes up by 19.96 (fee 0.04).
    • A wallet-short submit returns INSUFFICIENT_FUNDS and no balance changes.
    • Reconciliation REVENUE and DOUBLE_ENTRY are OK.
  • Each task was reviewed separately. The whole branch passed an Opus review gate in 2 rounds, and the ON_RAMP addition passed its own Opus gate in 1 round.

Known follow-ups

  • Both convert directions lock USD accounts as reserve → fee → wallet. Card deposit and transfer lock the wallet first, then reserve. A concurrent convert and card deposit by the same user can deadlock: Convert retries, the deposit side returns a 500, and no money is misposted. Follow-up ticket needed.
  • Simulator check was partial: the Convert screen renders correctly. QA still needs to run the receipt → Home → Transfer-to-card path on a device.
  • pnpm generate-api was not run. Orval rejects the current swagger because of the capitalised bearer scheme, which predates this branch. Convert uses the hand-written fe/src/api/convert wrapper.
  • Pre-existing bug, not from this branch: recordDeposit on the Fireblocks path reads the user's USDC balance without a lock and writes an absolute value. A deposit that lands during a convert can overwrite the convert's change to the cached balance. The ledger entries stay correct, so it is detectable. Needs a high-priority ticket.
  • Admin console does not translate the three new reversal codes yet (admin changes are scheduled for SP7).

🤖 Generated with Claude Code

Edited Oct 02, 2026 by Khanh Pham
Assignee
Assign to
Reviewers
Request review from
Time tracking
Source branch: feat/convert-to-fiat-wallet