Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
});
});

Expand Down
30 changes: 19 additions & 11 deletions components/DepositOption/WalletDepositAddress/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -59,19 +59,27 @@ const MINIMUM_DEPOSIT_BY_TOKEN: Record<string, number> = {
/** 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
Expand Down
27 changes: 12 additions & 15 deletions hooks/useWalletDepositAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -11,29 +12,25 @@ 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
* would hand out an address the deposit would be stranded on.
*/
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],
Expand Down
Loading