diff --git a/components/DepositOption/WalletDepositAddress/__tests__/walletDepositConstants.test.ts b/components/DepositOption/WalletDepositAddress/__tests__/walletDepositConstants.test.ts index ca646fdd..962dfb43 100644 --- a/components/DepositOption/WalletDepositAddress/__tests__/walletDepositConstants.test.ts +++ b/components/DepositOption/WalletDepositAddress/__tests__/walletDepositConstants.test.ts @@ -13,7 +13,7 @@ import { resolveWalletDepositSymbol, usesDirectDepositAddress, } from '@/components/DepositOption/WalletDepositAddress/constants'; -import { DepositAsset } from '@/lib/types'; +import { CardProvider, DepositAsset } from '@/lib/types'; // The module resolves icons through the asset barrel; what is under test is // which chains and currencies are offered and what each pairing's floor is. @@ -81,23 +81,32 @@ describe('getWalletDepositNetworks', () => { }); /** - * Which address the screen hands out. A minted address is watched by the deposit - * pipeline; the Safe is not. Getting this wrong either strands a transfer or - * makes the screen promise a detection that can never happen. + * Which address the screen hands out. The minted one is watched by the deposit + * pipeline and resolves by issuer; the Safe is neither. Getting this wrong + * either strands a transfer or sends it to someone's card instead of their + * wallet. */ describe('usesDirectDepositAddress', () => { - it('mints an address for the stablecoins the pipeline has a route for', () => { - expect(usesDirectDepositAddress('USDC')).toBe(true); - expect(usesDirectDepositAddress('USDT')).toBe(true); - }); - - // These land in the Safe and stay as the token that was sent, so the Safe - // address is the right answer rather than a fallback. - it('keeps the Safe for the currencies with no route', () => { - expect(usesDirectDepositAddress('ETH')).toBe(false); - expect(usesDirectDepositAddress('WETH')).toBe(false); - expect(usesDirectDepositAddress('FUSE')).toBe(false); - expect(usesDirectDepositAddress('WFUSE')).toBe(false); + it('mints for a Wirex cardholder sending a stablecoin', () => { + expect(usesDirectDepositAddress('USDC', CardProvider.WIREX)).toBe(true); + expect(usesDirectDepositAddress('USDT', CardProvider.WIREX)).toBe(true); + }); + + // Minting resolves by issuer: for Rain it would deliver to the card, and with + // no card there is no issuer to resolve. Both see the Safe, for everything. + it('keeps the Safe for everyone who is not a Wirex cardholder', () => { + expect(usesDirectDepositAddress('USDC', CardProvider.RAIN)).toBe(false); + expect(usesDirectDepositAddress('USDC', null)).toBe(false); + expect(usesDirectDepositAddress('USDC', undefined)).toBe(false); + }); + + // These have no route through the pipeline — they land in the Safe and stay as + // the token that was sent, so there the Safe is the right answer. + it('keeps the Safe for the currencies with no route, even for Wirex', () => { + expect(usesDirectDepositAddress('ETH', CardProvider.WIREX)).toBe(false); + expect(usesDirectDepositAddress('WETH', CardProvider.WIREX)).toBe(false); + expect(usesDirectDepositAddress('FUSE', CardProvider.WIREX)).toBe(false); + expect(usesDirectDepositAddress('WFUSE', CardProvider.WIREX)).toBe(false); }); }); diff --git a/components/DepositOption/WalletDepositAddress/constants.ts b/components/DepositOption/WalletDepositAddress/constants.ts index 36acb6e0..ee1465f5 100644 --- a/components/DepositOption/WalletDepositAddress/constants.ts +++ b/components/DepositOption/WalletDepositAddress/constants.ts @@ -3,7 +3,7 @@ import { arbitrum, base, bsc, fuse, mainnet, polygon } from 'viem/chains'; import { BRIDGE_TOKENS } from '@/constants/bridge'; import { getAsset } from '@/lib/assets'; -import { DepositAsset } from '@/lib/types'; +import { CardProvider, DepositAsset } from '@/lib/types'; import { getAllowedTokensForChain, getVaultDepositConfig } from '@/lib/vaults'; export type WalletDepositNetwork = { @@ -59,19 +59,27 @@ const MINIMUM_DEPOSIT_BY_TOKEN: Record = { /** Used for a chain with no entry above, rather than claiming there is no floor. */ const DEFAULT_MINIMUM_DEPOSIT = 1; +/** The stablecoins the deposit pipeline has a route for. */ +const DIRECT_DEPOSIT_SYMBOLS = new Set(['USDC', 'USDT']); + /** - * Currencies the deposit address is minted for, rather than being the Safe. + * Whether the deposit address is one the pipeline mints, rather than the Safe. + * + * Both halves have to hold: * - * The stablecoins, and only them, because they are the ones the deposit pipeline - * has a route for: a minted address is watched, so the transfer is detected, - * credited and shown as activity. ETH, WETH, FUSE and WFUSE have no such route — - * they are sent to the Safe and simply sit there as what was sent — so for those - * the Safe address is not a fallback, it is the right answer. + * - A Wirex cardholder. The address is minted against the card destination, and + * the backend resolves that by issuer: for Wirex it delivers to their Safe on + * Fuse, which is the same balance their card settles from. For a Rain + * cardholder it would deliver to the card, and for someone with no card there + * is no issuer to resolve — so everyone else is shown the Safe, everywhere. + * - A stablecoin. ETH, WETH, FUSE and WFUSE have no route through the pipeline; + * they land in the Safe and stay as the token that was sent, so there the Safe + * address is the right answer rather than a fallback. */ -const DIRECT_DEPOSIT_SYMBOLS = new Set(['USDC', 'USDT']); - -export const usesDirectDepositAddress = (symbol: string): boolean => - DIRECT_DEPOSIT_SYMBOLS.has(symbol); +export const usesDirectDepositAddress = ( + symbol: string, + provider: CardProvider | null | undefined, +): boolean => provider === CardProvider.WIREX && DIRECT_DEPOSIT_SYMBOLS.has(symbol); /** * The order "Select token" leads with. The stablecoins people actually deposit diff --git a/hooks/useWalletDepositAddress.ts b/hooks/useWalletDepositAddress.ts index de9ca1a3..caea3cb1 100644 --- a/hooks/useWalletDepositAddress.ts +++ b/hooks/useWalletDepositAddress.ts @@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query'; import { CARD_FUND_DESTINATION_TYPE } from '@/components/Card/CardFund/constants'; import { usesDirectDepositAddress } from '@/components/DepositOption/WalletDepositAddress/constants'; +import { useCardProvider } from '@/hooks/useCardProvider'; import useUser from '@/hooks/useUser'; import { createDirectDepositSession } from '@/lib/api'; import { withRefreshToken } from '@/lib/utils'; @@ -11,21 +12,16 @@ export const WALLET_DEPOSIT_ADDRESS_QUERY_KEY = 'wallet-deposit-address'; /** * The address to show for a (chain, currency) pairing on the deposit screen. * - * Two answers, because the pipeline only has a route for one of them: + * A Wirex cardholder depositing a stablecoin gets an address minted by + * `createDirectDepositSession` — watched, so the transfer is detected, credited + * and recorded as activity. Everyone else sees the Safe address, for every + * currency (see {@link usesDirectDepositAddress}). * - * - Stablecoins get an address minted by `createDirectDepositSession`. That - * address is watched, so a transfer to it is detected, credited and recorded - * as activity. - * - ETH, WETH, FUSE and WFUSE get the Safe. Nothing mints an address for them - * and nothing would watch it; they are sent to the Safe and sit there as the - * token that was sent, which is what the screen should say. - * - * The destination is the card's (`RAIN_CARD`), which is not the misnomer it - * looks like: the name is historical and the backend resolves the issuer, so it - * delivers to a Rain card or to the cardholder's Safe accordingly. Everyone who - * reaches this screen — a Wirex cardholder, or someone with no card at all — is - * on the Safe side of that. Rain cardholders are routed to "Fund your card" - * instead and never see it. + * The destination is the card's (`RAIN_CARD`), which is historical naming: the + * backend resolves the issuer, and for Wirex that means their Safe on Fuse — + * the balance their card settles from. Which is exactly why the rule is theirs + * alone: the same call for a Rain cardholder would deliver to the card, and for + * someone with no card there is no issuer to resolve. * * Minting a session is a POST, so a failure is not retried into a loop; the * caller shows the failure rather than quietly falling back to the Safe, which @@ -33,7 +29,8 @@ export const WALLET_DEPOSIT_ADDRESS_QUERY_KEY = 'wallet-deposit-address'; */ export const useWalletDepositAddress = (chainId: number, symbol: string) => { const { user } = useUser(); - const needsSession = usesDirectDepositAddress(symbol); + const { provider } = useCardProvider(); + const needsSession = usesDirectDepositAddress(symbol, provider); const { data, isLoading, isError } = useQuery({ queryKey: [WALLET_DEPOSIT_ADDRESS_QUERY_KEY, chainId, symbol],