Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/components/Claim/Link/views/BankFlowManager.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import useClaimLink from '../../useClaimLink'
import { type AddBankAccountPayload } from '@/app/actions/types/users.types'
import { useAuth } from '@/context/authContext'
import { type TCreateOfframpRequest, type TCreateOfframpResponse } from '@/services/services.types'
import { getOfframpCurrencyConfig } from '@/utils/bridge.utils'
import { getOfframpConfigFromAccount } from '@/utils/bridge.utils'
import { getBridgeChainName, getBridgeTokenName } from '@/utils/bridge-accounts.utils'
import { generateKeysFromString, getParamsFromLink } from '@/utils/peanut-link.utils'
import { getContractAddress } from '@/utils/peanut-claim.utils'
Expand Down Expand Up @@ -239,7 +239,13 @@ export const BankFlowManager = (props: IClaimScreenProps) => {

const externalAccountId = (account.bridgeAccountId ?? account.id) as string

const destination = getOfframpCurrencyConfig(account.country ?? selectedCountry!.id)
// Derive destination currency + rail from the SELECTED ACCOUNT's
// type, not from `selectedCountry`. Pairing a GB/GBP account with
// a SEPA destination is semantically impossible — Bridge rejects
// with "country is not supported for SEPA" (PEANUT-API-5P/5M/5N
// on 2026-06-02). The account's `type` already carries the right
// answer for every Bridge destination we support.
const destination = getOfframpConfigFromAccount(account)

// handle offramp request creation
const offrampRequestParams: TCreateOfframpRequest = {
Expand Down
65 changes: 65 additions & 0 deletions src/utils/__tests__/bridge.utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { BRIDGE_DEVELOPER_FEE_RATE } from '@/constants/payment.consts'
import {
applyBridgeCrossCurrencyFee,
getCurrencyConfig,
getOfframpConfigFromAccount,
getOfframpCurrencyConfig,
getPaymentRailDisplayName,
getMinimumAmount,
Expand Down Expand Up @@ -142,6 +143,70 @@ describe('bridge.utils', () => {
paymentRail: 'sepa',
})
})
})

describe('getOfframpConfigFromAccount (PEANUT-API-5P/5M/5N regression)', () => {
// The 2026-06-02 21:24 incident: a GB/GBP account got paired with
// EUR/SEPA because the picker used `account.country ?? selectedCountry`
// and the user-selected country fell through the "everything else →
// EUR/SEPA" default. These cases lock in the behavior that the account
// type alone is enough to pick the right rail.
it('GB account → GBP / faster_payments (was EUR/SEPA in the incident)', () => {
expect(getOfframpConfigFromAccount({ type: 'gb' })).toEqual({
currency: 'gbp',
paymentRail: 'faster_payments',
})
})

it('US account → USD / ach', () => {
expect(getOfframpConfigFromAccount({ type: 'us' })).toEqual({
currency: 'usd',
paymentRail: 'ach',
})
})

it('CLABE account → MXN / spei', () => {
expect(getOfframpConfigFromAccount({ type: 'clabe' })).toEqual({
currency: 'mxn',
paymentRail: 'spei',
})
})

it('IBAN account → EUR / sepa', () => {
expect(getOfframpConfigFromAccount({ type: 'iban' })).toEqual({
currency: 'eur',
paymentRail: 'sepa',
})
})

it('accepts BE Prisma-shape suffixes like BANK_IBAN / BANK_ACH_GB', () => {
expect(getOfframpConfigFromAccount({ type: 'BANK_IBAN' })).toEqual({
currency: 'eur',
paymentRail: 'sepa',
})
expect(getOfframpConfigFromAccount({ type: 'bank_account_gb' })).toEqual({
currency: 'gbp',
paymentRail: 'faster_payments',
})
})

it('throws on Manteca account type — must use the Manteca offramp path', () => {
expect(() => getOfframpConfigFromAccount({ type: 'manteca' })).toThrow(
'Manteca accounts route through a separate offramp path'
)
})

it('falls back to country-based picking when type is missing', () => {
expect(getOfframpConfigFromAccount({ country: 'US' })).toEqual({
currency: 'usd',
paymentRail: 'ach',
})
// Unknown country → EU/SEPA default, mirrors prior behavior
expect(getOfframpConfigFromAccount({ country: 'XYZ' })).toEqual({
currency: 'eur',
paymentRail: 'sepa',
})
})

describe('getMinimumAmount', () => {
it('should return 50 for Mexico', () => {
Expand Down
31 changes: 31 additions & 0 deletions src/utils/bridge.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,37 @@ export const getOfframpCurrencyConfig = (countryId: string): CurrencyConfig => {
return getCurrencyConfig(countryId, 'offramp')
}

/**
* Derive the offramp destination currency + payment rail from the bank
* account's actual `type`, falling back to country only when the type is
* unknown.
*
* Why: `getOfframpCurrencyConfig(country)` defaults *any* unknown country to
* EUR+SEPA. Pairing that default with a GBP/UK account caused Bridge to 400
* with "country is not supported for SEPA" (PEANUT-API-5P/5M/5N, 2026-06-02
* 21:24 UTC) — Bridge can't SEPA-credit a GBP account. The bank account's
* `type` already carries the right answer for every Bridge destination we
* support (`us`/`gb`/`clabe`/`iban`), so derive from it directly.
*
* Manteca-type accounts use a non-Bridge rail; the caller must NOT route
* those through this helper. We throw rather than silently misclassify.
*/
export const getOfframpConfigFromAccount = (account: {
type?: string | AccountType | null
country?: string | null
}): CurrencyConfig => {
const t = account.type?.toString().toLowerCase()
if (t === AccountType.US || t?.endsWith('ach') || t?.endsWith('us')) return getCurrencyConfig('US', 'offramp')
if (t === AccountType.GB || t?.endsWith('gb')) return getCurrencyConfig('GB', 'offramp')
if (t === AccountType.CLABE || t?.endsWith('clabe')) return getCurrencyConfig('MX', 'offramp')
if (t === AccountType.IBAN || t?.endsWith('iban')) return getCurrencyConfig('EU', 'offramp')
if (t === AccountType.MANTECA || t?.endsWith('manteca')) {
throw new Error('Manteca accounts route through a separate offramp path, not Bridge.')
}
// type missing / unknown — fall back to country, preserving prior behavior.
return getOfframpCurrencyConfig(account.country ?? 'EU')
}

/**
* Map ISO fiat currency → Bridge bank AccountType. Used by onramp quote
* + exchange-rate callers that only have the currency string in hand.
Expand Down
Loading