Skip to content

fix(offramp): derive destination rail from account type, not country (PEANUT-API-5P/5M/5N)#2173

Open
Hugo0 wants to merge 1 commit into
mainfrom
hotfix/bankflow-derive-from-account-type
Open

fix(offramp): derive destination rail from account type, not country (PEANUT-API-5P/5M/5N)#2173
Hugo0 wants to merge 1 commit into
mainfrom
hotfix/bankflow-derive-from-account-type

Conversation

@Hugo0
Copy link
Copy Markdown
Contributor

@Hugo0 Hugo0 commented Jun 2, 2026

Why

2026-06-02 21:24 UTC. PEANUT-API-5P/5M/5N + Discord on-call page.

A user with a UK GBP bank account tried to offramp USDC → EUR via SEPA. Bridge correctly rejected:
```
country is not supported for SEPA
```
SEPA only credits EUR-denominated Eurozone accounts. You can't SEPA-credit a GBP account.

Root cause (FE)

`BankFlowManager.handleCreateOfframpAndClaim` picked the destination from country, not from the bank account's actual type:

```ts
const destination = getOfframpCurrencyConfig(
account.country ?? selectedCountry!.id
)
```

When `account.country` is missing or doesn't match, the picker falls through to `selectedCountry`. `getOfframpCurrencyConfig`'s "everything unknown → EUR/SEPA" default then fires — even for a clearly GBP/UK account. Result: a GB account was paired with EUR/SEPA → Bridge 400.

The bank account's own `type` already carries the right answer for every Bridge destination we support (`us` / `gb` / `clabe` / `iban`). Deriving directly from type closes this class of bug at the source.

What this PR does

New helper `getOfframpConfigFromAccount(account)` in `src/utils/bridge.utils.ts`:

account.type currency paymentRail
`us` usd ach
`gb` gbp faster_payments
`clabe` mxn spei
`iban` eur sepa
`manteca` throws (must use the Manteca offramp path, not Bridge)
missing/unknown falls back to country-based picking (prior behavior)

Also accepts BE Prisma-shape suffixes (`BANK_IBAN`, `BANK_ACCOUNT_GB`, …) so it works regardless of which side of the API the account row came from.

Use site: `BankFlowManager.view.tsx:248` — the only call site for the offramp destination picker on this code path.

Tests

`src/utils/tests/bridge.utils.test.ts` — 7 new cases under `getOfframpConfigFromAccount (PEANUT-API-5P/5M/5N regression)`:

  • GB account → GBP/faster_payments (was EUR/SEPA in the incident — locked in)
  • ✅ US → USD/ach, CLABE → MXN/spei, IBAN → EUR/sepa
  • ✅ BE Prisma-suffix shapes (`BANK_IBAN`, `BANK_ACCOUNT_GB`)
  • ✅ Manteca throws to surface a wrong-path call early
  • ✅ Missing-type fallback to country picking still works

71 tests passing in the file (was 64 before).

What this PR does NOT do (deliberate)

  • The companion BE PR fixes error surfacing + Discord pager filter: peanut-api-ts #964. Even after this FE fix, other user-input mistakes (e.g. a Bridge customer status change mid-flow) shouldn't page on-call.
  • Doesn't audit every other call site of `getOfframpCurrencyConfig`. The `withdraw/[country]/bank/page.tsx` flow uses the URL country, which by design matches the account type — out of scope here.

Test plan

  • Unit tests cover the regression scenarios (above).
  • Typecheck clean for changed files.
  • Prettier clean.
  • Post-merge: watch Sentry — `country is not supported for SEPA` should drop to zero from non-test traffic.

2026-06-02 21:24 UTC, PEANUT-API-5P/5M/5N. A user with a UK GBP bank
account tried to offramp USDC → EUR via SEPA. Bridge rejected with
"country is not supported for SEPA" — SEPA only credits EUR-denominated
Eurozone accounts, not GBP/UK.

Root cause was in BankFlowManager.handleCreateOfframpAndClaim:
    const destination = getOfframpCurrencyConfig(
        account.country ?? selectedCountry!.id
    )

When `account.country` is missing, the picker falls back to
`selectedCountry`. If `selectedCountry` doesn't match the saved-account's
actual type (e.g. user picks a random country, or country resolution is
buggy), the helper's "everything unknown → EUR/SEPA" default fires —
even for a clearly GBP/UK account.

The account's own `type` already carries the right answer for every
Bridge destination we support (us/gb/clabe/iban). Adding a tiny helper
`getOfframpConfigFromAccount` that derives currency+rail directly from
account.type closes this class of bug.

- `gb`  → gbp/faster_payments
- `us`  → usd/ach
- `clabe` → mxn/spei
- `iban` → eur/sepa
- `manteca` → throws (must use the Manteca offramp path, not Bridge)
- type missing → falls back to country-based picking (prior behavior)

The helper also accepts BE Prisma-shape suffixes (`BANK_IBAN`,
`BANK_ACCOUNT_GB`, etc.) so it doesn't matter which side of the API the
account row came from.

7 new unit tests in bridge.utils.test.ts cover:
- The GB regression (was EUR/SEPA, now GBP/faster_payments)
- All 4 supported account types map to the right rail
- BE Prisma-suffix shapes (BANK_IBAN, BANK_ACCOUNT_GB)
- Manteca throws to surface a wrong-path call early
- Missing-type fallback to country picking still works

All 71 bridge.utils tests passing.

Companion PR: peanut-api-ts #964 — surfaces Bridge's actual error
message in Sentry + stops Discord-paging on user-input 4xx errors.
Even after this FE fix, other user-input mistakes (e.g. a Bridge
customer status change mid-flow) shouldn't page on-call.
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview, Comment Jun 2, 2026 10:35pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ced1a0c0-7aa8-40d8-a4b2-28df2d08dfe6

📥 Commits

Reviewing files that changed from the base of the PR and between 73dfae3 and 6489bf5.

📒 Files selected for processing (3)
  • src/components/Claim/Link/views/BankFlowManager.view.tsx
  • src/utils/__tests__/bridge.utils.test.ts
  • src/utils/bridge.utils.ts

Walkthrough

This PR refactors how the UI derives off-ramp destination configuration. Instead of selecting the off-ramp currency and payment rail based on the user's selected country, the system now derives them from the selected bank account's type field. A new utility function implements type-to-configuration mapping with fallback to country-based selection, and BankFlowManager is updated to use it.

Changes

Off-ramp routing by account type

Layer / File(s) Summary
Off-ramp config helper and tests
src/utils/bridge.utils.ts, src/utils/__tests__/bridge.utils.test.ts
getOfframpConfigFromAccount maps account types (US/ACH, GB, CLABE, IBAN) directly to off-ramp currency/rail configs, throws an error for Manteca accounts to prevent misrouting, and falls back to country-based selection when type is absent. Tests verify all type mappings, Prisma-style type variants, error handling, and country-based fallback behavior.
BankFlowManager integration
src/components/Claim/Link/views/BankFlowManager.view.tsx
Destination off-ramp configuration is now derived from the selected account's type via the new helper, removing the prior selectedCountry-based selection and binding the destination to the account's capabilities.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • peanutprotocol/peanut-ui#2160: Adds AccountType.MANTECA to useSavedAccounts, which would interact with the new getOfframpConfigFromAccount routing logic that explicitly throws for Manteca accounts.

Suggested labels

enhancement

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: deriving the offramp destination from account type instead of country, fixing the root cause of the incident.
Description check ✅ Passed The description thoroughly explains the incident, root cause, solution, test coverage, and scope, directly relating to all changes in the pull request.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added the enhancement New feature or request label Jun 2, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 2, 2026

Code-analysis diff

Painscore total: 5878.54 → 5879.42 (+0.88)
Findings: 0 net (+14 new, -14 resolved)

🆕 New findings (14)

  • critical complexity — src/components/Claim/Link/views/BankFlowManager.view.tsx — CC 98, MI 47.76, SLOC 404
  • critical complexity — src/utils/bridge.utils.ts — CC 80, MI 58.99, SLOC 187
  • high method-complexity — src/components/Claim/Link/views/BankFlowManager.view.tsx:296 — CC 32 SLOC 105
  • medium high-mdd — src/components/Claim/Link/views/BankFlowManager.view.tsx:57 — BankFlowManager: MDD 107.0 (uses across many lines from declarations)
  • medium high-mdd — src/components/Claim/Link/views/BankFlowManager.view.tsx:296 — handleSuccess: MDD 37.9 (uses across many lines from declarations)
  • medium high-mdd — src/utils/bridge.utils.ts:242 — inferBankAccountType: MDD 34.8 (uses across many lines from declarations)
  • medium hotspot — src/components/Claim/Link/views/BankFlowManager.view.tsx — 29 commits, +326/-217 lines since 6 months ago
  • medium method-complexity — src/utils/bridge.utils.ts:242 — CC 20 SLOC 47
  • medium method-complexity — src/components/Claim/Link/views/BankFlowManager.view.tsx:450 — CC 15 SLOC 31
  • low high-mdd — src/components/Claim/Link/views/BankFlowManager.view.tsx:187 — handleCreateOfframpAndClaim: MDD 19.8 (uses across many lines from declarations)
  • low structural-dup — src/components/Claim/Link/views/BankFlowManager.view.tsx:563 — 17 duplicate lines / 0 tokens with src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:411
  • low high-dlt — src/components/Claim/Link/views/BankFlowManager.view.tsx:296 — handleSuccess: DLT 16 (calls 16 distinct functions — high context load)
  • low high-mdd — src/components/Claim/Link/views/BankFlowManager.view.tsx:450 — : MDD 10.4 (uses across many lines from declarations)
  • low structural-dup — src/components/Claim/Link/views/BankFlowManager.view.tsx:581 — 10 duplicate lines / 0 tokens with src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:541

✅ Resolved (14)

  • src/components/Claim/Link/views/BankFlowManager.view.tsx — CC 98, MI 47.75, SLOC 404
  • src/utils/bridge.utils.ts — CC 68, MI 60.1, SLOC 168
  • src/components/Claim/Link/views/BankFlowManager.view.tsx:290 — CC 32 SLOC 105
  • src/components/Claim/Link/views/BankFlowManager.view.tsx:57 — BankFlowManager: MDD 105.2 (uses across many lines from declarations)
  • src/components/Claim/Link/views/BankFlowManager.view.tsx:290 — handleSuccess: MDD 37.9 (uses across many lines from declarations)
  • src/utils/bridge.utils.ts:211 — inferBankAccountType: MDD 34.8 (uses across many lines from declarations)
  • src/components/Claim/Link/views/BankFlowManager.view.tsx — 28 commits, +318/-215 lines since 6 months ago
  • src/utils/bridge.utils.ts:211 — CC 20 SLOC 47
  • src/components/Claim/Link/views/BankFlowManager.view.tsx:444 — CC 15 SLOC 31
  • src/components/Claim/Link/views/BankFlowManager.view.tsx:187 — handleCreateOfframpAndClaim: MDD 18.1 (uses across many lines from declarations)
  • src/components/Claim/Link/views/BankFlowManager.view.tsx:557 — 17 duplicate lines / 0 tokens with src/app/(mobile-ui)/add-money/[country]/bank/page.tsx:411
  • src/components/Claim/Link/views/BankFlowManager.view.tsx:290 — handleSuccess: DLT 16 (calls 16 distinct functions — high context load)
  • src/components/Claim/Link/views/BankFlowManager.view.tsx:444 — : MDD 10.4 (uses across many lines from declarations)
  • src/components/Claim/Link/views/BankFlowManager.view.tsx:575 — 10 duplicate lines / 0 tokens with src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:541

📈 Painscore deltas (top movers)

File Before After Δ
src/utils/bridge.utils.ts 9.1 9.9 +0.7

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 2, 2026

🧪 UI test report — ✅ all green

Suites

  • unit: 1301 ran, 0 failed, 0 skipped, 16.5s

📊 Coverage (unit)

metric %
statements 50.4%
branches 31.5%
functions 34.8%
lines 50.2%
⏱ 10 slowest test cases
time test
0.2s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in updateUserById body
0.2s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in updateUserById
0.2s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every stamp stays within canvas at any count
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid 9-digit US account
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid ETH address
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle maximum length (17 digits) US account
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle too long for US account
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid ETH address with surrounding spaces
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid ENS name
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid German IBAN
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant