diff --git a/src/features/payments/shared/hooks/useCrossChainTransfer.test.ts b/src/features/payments/shared/hooks/useCrossChainTransfer.test.ts new file mode 100644 index 0000000000..a3b563496f --- /dev/null +++ b/src/features/payments/shared/hooks/useCrossChainTransfer.test.ts @@ -0,0 +1,79 @@ +/** + * useCrossChainTransfer — calculate() error mapping. + * + * Rhino tags an unroutable pair `NoRouteFoundError`; the backend surfaces the + * tag verbatim inside the thrown message. The hook maps it to + * ROUTE_NOT_FOUND_ERROR so the Confirm view's existing special-case (Retry → + * onBack) sends the user back to fix the input. Any other failure passes + * through verbatim. + */ +import { act, renderHook } from '@testing-library/react' +import { useCrossChainTransfer } from './useCrossChainTransfer' +import { ROUTE_NOT_FOUND_ERROR } from '@/constants/general.consts' +import { previewSdaTransfer } from '@/services/rhino-sda' + +jest.mock('@sentry/nextjs', () => ({ captureException: jest.fn() })) +jest.mock('@/services/rhino-sda', () => ({ + previewSdaTransfer: jest.fn(), + provisionSdaTransfer: jest.fn(), +})) +jest.mock('@/services/rhino-bridge', () => ({ + getBridgeQuote: jest.fn(), + commitBridgeQuote: jest.fn(), + getBridgeStatus: jest.fn(), + isQuoteNearExpiry: jest.fn(() => false), +})) +jest.mock('@/utils/peanut-claim.utils', () => ({ prepareRequestLinkFulfillmentTransaction: jest.fn() })) +jest.mock('@/app/actions/tokens', () => ({ estimateTransactionCostUsd: jest.fn() })) + +const mockPreview = previewSdaTransfer as jest.Mock + +// Cross-chain SDA input: Arbitrum USDC → Solana USDC. tokenSymbol is passed +// explicitly so the test doesn't depend on token-details lookups. +const CALC_INPUT = { + source: { + address: '0x1111111111111111111111111111111111111111' as `0x${string}`, + tokenAddress: '0xaf88d065e77c8cc2239327c5edb3a432268e5831' as `0x${string}`, + chainId: '42161', + }, + destination: { + recipientAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + tokenAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + tokenAmount: '2', + tokenDecimals: 6, + tokenType: 1, + chainId: 'solana', + tokenSymbol: 'USDC', + }, + context: 'withdraw' as const, + contextId: 'charge-uuid-1', +} + +describe('useCrossChainTransfer calculate() error mapping', () => { + afterEach(() => jest.clearAllMocks()) + + test('NoRouteFoundError from the preview maps to ROUTE_NOT_FOUND_ERROR', async () => { + mockPreview.mockRejectedValue( + new Error('Failed to preview SDA transfer: 400 {"error":"Preview quote failed: NoRouteFoundError"}') + ) + const { result } = renderHook(() => useCrossChainTransfer()) + + await act(() => result.current.calculate(CALC_INPUT)) + + expect(result.current.error).toBe(ROUTE_NOT_FOUND_ERROR) + expect(result.current.isFeeEstimationError).toBe(true) + }) + + test('any other failure passes through verbatim', async () => { + mockPreview.mockRejectedValue( + new Error('Failed to preview SDA transfer: 400 {"error":"Preview quote failed: HTTP 404 with empty body"}') + ) + const { result } = renderHook(() => useCrossChainTransfer()) + + await act(() => result.current.calculate(CALC_INPUT)) + + expect(result.current.error).toBe( + 'Failed to preview SDA transfer: 400 {"error":"Preview quote failed: HTTP 404 with empty body"}' + ) + }) +}) diff --git a/src/features/payments/shared/hooks/useCrossChainTransfer.ts b/src/features/payments/shared/hooks/useCrossChainTransfer.ts index e142eefc07..534d8e27e8 100644 --- a/src/features/payments/shared/hooks/useCrossChainTransfer.ts +++ b/src/features/payments/shared/hooks/useCrossChainTransfer.ts @@ -45,6 +45,7 @@ import { type BridgeStatusResponse, } from '@/services/rhino-bridge' import { chainIdToRhinoName } from '@/constants/rhino.consts' +import { ROUTE_NOT_FOUND_ERROR } from '@/constants/general.consts' import { NON_EVM_WITHDRAW_CHAINS } from '@/constants/chainRegistry.consts' import { areEvmAddressesEqual, getTokenSymbol } from '@/utils/general.utils' @@ -356,7 +357,12 @@ export function useCrossChainTransfer(): UseCrossChainTransferReturn { }) setPath('sda') } catch (err) { - const message = err instanceof Error ? err.message : 'failed to calculate cross-chain transfer' + const raw = err instanceof Error ? err.message : 'failed to calculate cross-chain transfer' + // Rhino tags an unroutable pair NoRouteFoundError; the backend + // surfaces the tag verbatim. Map it to the copy the Confirm view + // already special-cases — its Retry button sends the user back to + // fix the input for this constant. + const message = raw.includes('NoRouteFoundError') ? ROUTE_NOT_FOUND_ERROR : raw setError(message) setIsFeeEstimationError(true) captureException(err)