import * as TestFunding from '../../../test/Funding.js' import * as FundingCatalog from './Catalog.js' import * as FundingProvider from './Provider.js' import * as Across from './providers/across.js' import * as Relay from './providers/relay.js' import * as Rhino from './providers/rhino.js' import * as Squid from './providers/squid.js' import * as Stargate from './providers/stargate.js' import * as Symbiosis from './providers/symbiosis.js' const providers = [ Across.across({ apiKey: 'across-api-key', integratorId: '0xdead', userAddress: '0x1111111111111111111111111111111111111111', }), Relay.relay({ userAddresses: TestFunding.relayQuoteUsers }), Rhino.rhino(), Squid.squid({ integratorId: 'squid-integrator-id', userAddress: '0x1111111111111111111111111111111111111111', }), Stargate.stargate(), Symbiosis.symbiosis({ userAddress: '0x1111111111111111111111111111111111111111' }), ] as const const baseUsdcRequest = { catalog: TestFunding.snapshot, provider: 'relay', sourceAmount: '1000000', sourceAmountUnits: 'baseUnits', sourceChain: 'base', sourceToken: 'USDC', } as const const depositAddressOptions = { amount: '100000000', catalog: TestFunding.snapshot, destinationChainId: 4217, destinationToken: '0x20c00000000000000000000014f22ca97301eb73', recipient: `0x${'11'.repeat(20)}`, refundAddress: 'TJRabPrwbZy45sbavfcjinPJC18kjpRTv8', sourceChain: 'tron', sourceToken: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', subsidize: false, } as const const prepareOptions = { catalog: TestFunding.snapshot, destinationChainId: 4217, destinationToken: '0x20c0000000000000000000000000000000000000', method: 'transaction', mode: 'exactSource', recipient: `0x${'11'.repeat(20)}`, sender: `0x${'22'.repeat(20)}`, sourceAmount: '1000000', sourceChain: 'base', sourceToken: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', } as const describe('from', () => { test('registers the funding provider type', () => { expect(providers[0]?.type).toBe('funding') }) test('rejects provider ids that are not normalized', () => { expect(() => FundingProvider.from({ getQuote: async () => ({ sampledAt: '2026-01-01T00:00:00Z', status: 'unavailable' }), id: ' Relay ', name: 'Relay', }), ).toThrowErrorMatchingInlineSnapshot( `[FundingProvider.ProviderConfigurationError: Provider ids must be lowercase and trimmed.]`, ) }) }) describe('is', () => { test('requires the funding quote capability', () => { expect( FundingProvider.is({ id: 'incomplete', name: 'Incomplete', type: 'funding', }), ).toBe(false) expect(FundingProvider.is(providers[0])).toBe(true) }) }) describe('getQuotes', () => { test('returns normalized successful provider quotes', async () => { const attempts: FundingProvider.ProviderAttempt[] = [] let seen: FundingProvider.getQuote.Parameters | undefined const quotes = await FundingProvider.getQuotes({ ...baseUsdcRequest, onAttempt: (attempt) => attempts.push(attempt), providers: [ withGetQuote('relay', async (input, signal) => { seen = input expect(signal.aborted).toBe(false) return { destinationAmountMin: '980000', destinationAmount: '990000', quality: { estimatedSeconds: 4, liquiditySource: 'providerQuote', sourceDetail: 'providerEndpoint', tier: 'liquid', }, sampledAt: '2026-01-01T00:00:00Z', status: 'available', } }), ], }) expect(seen?.candidate.id).toBe('base-usdc-tempo-usdce-relay') expect(quotes).toHaveLength(1) expect(quotes[0]).toMatchObject({ destinationAmount: { baseUnits: '990000', currency: 'USD', decimals: 6, formatted: '0.99', }, destinationAmountMin: { baseUnits: '980000', currency: 'USD', decimals: 6, formatted: '0.98', }, id: 'base-usdc-tempo-usdce-relay', quality: { liquiditySource: 'providerQuote', tier: 'liquid', }, quote: { sampledAt: '2026-01-01T00:00:00Z' }, sourceAmount: { baseUnits: '1000000', currency: 'USD', decimals: 6, formatted: '1', }, }) expect(attempts.map(stableAttempt)).toEqual([ { operation: 'getQuote', outcome: 'available', provider: 'relay' }, ]) expect(attempts[0]?.durationMs).toEqual(expect.any(Number)) }) test('omits provider-declared unavailable quotes', async () => { const attempts: FundingProvider.ProviderAttempt[] = [] const quotes = FundingProvider.getQuotes({ ...baseUsdcRequest, onAttempt: (attempt) => attempts.push(attempt), providers: [ withGetQuote('relay', async () => ({ sampledAt: '2026-01-01T00:00:00Z', status: 'unavailable', })), ], }) await expect(quotes).resolves.toEqual([]) expect(attempts.map(stableAttempt)).toEqual([ { operation: 'getQuote', outcome: 'unavailable', provider: 'relay' }, ]) }) test('rejects provider failures when no quote succeeds', async () => { const attempts: FundingProvider.ProviderAttempt[] = [] const quotes = FundingProvider.getQuotes({ ...baseUsdcRequest, onAttempt: (attempt) => attempts.push(attempt), providers: [ withGetQuote('relay', async () => { throw new Error('upstream unavailable') }), ], }) const cause = await quotes.catch((error: unknown) => error) expect(cause).toMatchObject({ failures: [{ cause: expect.any(Error), provider: { id: 'relay' } }], fundingProviderAttempts: [ { durationMs: expect.any(Number), failure: 'unknown', id: 'relay', operation: 'getQuote', outcome: 'failed', }, ], name: 'FundingProvider.ProviderQuoteError', providerFailure: { failure: 'unknown', id: 'relay', operation: 'getQuote' }, }) expect(attempts.map(stableAttempt)).toEqual([ { cause: expect.any(Error), operation: 'getQuote', outcome: 'failed', provider: 'relay', }, ]) }) test('records thin quotes separately from available quotes', async () => { const attempts: FundingProvider.ProviderAttempt[] = [] const quotes = await FundingProvider.getQuotes({ ...baseUsdcRequest, onAttempt: (attempt) => attempts.push(attempt), providers: [ withGetQuote('relay', async () => ({ destinationAmount: '990000', sampledAt: '2026-01-01T00:00:00Z', status: 'thin', })), ], }) expect(quotes).toHaveLength(1) expect(attempts.map(stableAttempt)).toEqual([ { operation: 'getQuote', outcome: 'thin', provider: 'relay' }, ]) }) test('rejects successful provider responses without output amounts', async () => { const quotes = FundingProvider.getQuotes({ ...baseUsdcRequest, providers: [ withGetQuote('relay', async () => ({ sampledAt: '2026-01-01T00:00:00Z', status: 'available', })), ], }) await expect(quotes).rejects.toThrowErrorMatchingInlineSnapshot( `[FundingProvider.ProviderQuoteError]`, ) }) test('rejects provider responses outside the shared schema', async () => { const quotes = FundingProvider.getQuotes({ ...baseUsdcRequest, providers: [ withGetQuote( 'relay', async () => ({ destinationAmount: 'invalid', sampledAt: 'invalid', status: 'available', }) as never, ), ], }) await expect(quotes).rejects.toMatchObject({ providerFailure: { failure: 'payload', id: 'relay', operation: 'getQuote' }, }) }) test.each([ { destinationAmountMin: undefined, destinationAmount: '0' }, { destinationAmountMin: '0', destinationAmount: '990000' }, { destinationAmountMin: '1000000', destinationAmount: '990000' }, ])( 'rejects invalid successful amounts %#', async ({ destinationAmountMin, destinationAmount }) => { const quotes = FundingProvider.getQuotes({ ...baseUsdcRequest, providers: [ withGetQuote('relay', async () => ({ ...(destinationAmountMin === undefined ? {} : { destinationAmountMin }), destinationAmount, sampledAt: '2026-01-01T00:00:00Z', status: 'available', })), ], }) await expect(quotes).rejects.toMatchObject({ providerFailure: { failure: 'payload', id: 'relay', operation: 'getQuote' }, }) }, ) test('allows provider quotes within the default deadline', async () => { vi.useFakeTimers() try { const quotes = FundingProvider.getQuotes({ ...baseUsdcRequest, providers: [ withGetQuote( 'relay', () => new Promise((resolve) => { setTimeout( () => resolve({ destinationAmount: '990000', sampledAt: '2026-01-01T00:00:00Z', status: 'available', }), 3_000, ) }), ), ], }) await vi.advanceTimersByTimeAsync(3_000) await expect(quotes).resolves.toHaveLength(1) } finally { vi.useRealTimers() } }) test('aborts providers that exceed the quote deadline', async () => { const attempts: FundingProvider.ProviderAttempt[] = [] let aborted = false const quotes = FundingProvider.getQuotes({ ...baseUsdcRequest, providerTimeoutMs: 1, providers: [ withGetQuote('relay', (_input, signal) => { signal.addEventListener('abort', () => { aborted = true }) return new Promise(() => {}) }), ], onAttempt: (attempt) => attempts.push(attempt), }) await expect(quotes).rejects.toThrowErrorMatchingInlineSnapshot( `[FundingProvider.ProviderQuoteError]`, ) expect(aborted).toBe(true) expect(attempts.map(stableAttempt)).toEqual([ { cause: expect.any(FundingProvider.ProviderTimeoutError), operation: 'getQuote', outcome: 'failed', provider: 'relay', }, ]) }) test('passes cache policy through to providers', async () => { let seen: FundingProvider.getQuote.Parameters | undefined await FundingProvider.getQuotes({ ...baseUsdcRequest, cache: { bypass: true }, providers: [ withGetQuote('relay', async (input) => { seen = input return { sampledAt: '2026-01-01T00:00:00Z', status: 'unavailable' } }), ], }) expect(seen?.cache.bypass).toBe(true) }) test('returns successful quotes when another provider fails', async () => { const attempts: FundingProvider.ProviderAttempt[] = [] const quotes = await FundingProvider.getQuotes({ ...baseUsdcRequest, onAttempt: (attempt) => attempts.push(attempt), provider: undefined, providers: [ withGetQuote('across', async () => { throw new Error('upstream unavailable') }), withGetQuote('relay', async () => ({ destinationAmount: '990000', sampledAt: '2026-01-01T00:00:00Z', status: 'available', })), ], }) expect(quotes).toHaveLength(1) expect(quotes[0]?.provider.id).toBe('relay') expect(attempts.map(stableAttempt)).toEqual([ { cause: expect.any(Error), operation: 'getQuote', outcome: 'failed', provider: 'across', }, { operation: 'getQuote', outcome: 'available', provider: 'relay' }, ]) }) test('does not let attempt observers change quote outcomes', async () => { const providers_success = [ withGetQuote('relay', async () => ({ destinationAmount: '990000', sampledAt: '2026-01-01T00:00:00Z', status: 'available', })), ] await expect( FundingProvider.getQuotes({ ...baseUsdcRequest, onAttempt() { throw new Error('observer failed') }, providers: providers_success, }), ).resolves.toHaveLength(1) await expect( FundingProvider.getQuotes({ ...baseUsdcRequest, onAttempt() { throw new Error('observer failed') }, providers: [ withGetQuote('relay', async () => { throw new Error('provider failed') }), ], }), ).rejects.toThrowErrorMatchingInlineSnapshot(`[FundingProvider.ProviderQuoteError]`) }) }) describe('createDepositAddress', () => { test('rejects invalid base-unit amounts', async () => { await expect( FundingProvider.createDepositAddress({ ...depositAddressOptions, amount: '10.5', providers: [withCreateDepositAddress('relay', async () => depositAddressResult())], }), ).rejects.toThrowErrorMatchingInlineSnapshot(`[FundingProvider.InvalidAmountError]`) }) test('provisions one deterministic capability-enabled candidate', async () => { const attempts: FundingProvider.ProviderAttempt[] = [] let seen: FundingProvider.createDepositAddress.Parameters | undefined const selection = await FundingProvider.createDepositAddress({ ...depositAddressOptions, onAttempt: (attempt) => attempts.push(attempt), providers: [ withCreateDepositAddress('relay', async (input) => { seen = input return { address: 'TJRabPrwbZy45sbavfcjinPJC18kjpRTv8', correlation: { checkEndpoint: '/requests/request_1', requestId: 'request_1' }, destinationAmount: '1000000', destinationAmountMin: '1000000', fees: [], sampledAt: '2026-08-04T00:00:00.000Z', } }), ], }) expect(seen).toMatchObject({ amount: depositAddressOptions.amount, candidate: { destinationToken: { symbol: 'USDT0' }, sourceChain: { id: 'tron:0x2b6653dc' }, sourceToken: { symbol: 'USDT' }, }, recipient: depositAddressOptions.recipient, refundAddress: depositAddressOptions.refundAddress, subsidize: false, }) expect(selection?.result).toEqual({ address: 'TJRabPrwbZy45sbavfcjinPJC18kjpRTv8', correlation: { checkEndpoint: '/requests/request_1', requestId: 'request_1' }, destinationAmount: '1000000', destinationAmountMin: '1000000', fees: [], sampledAt: '2026-08-04T00:00:00.000Z', }) expect(attempts.map(stableAttempt)).toEqual([ { operation: 'createDepositAddress', outcome: 'available', provider: 'relay' }, ]) }) test('returns no selection when the provider reports no route', async () => { const attempts: FundingProvider.ProviderAttempt[] = [] await expect( FundingProvider.createDepositAddress({ ...depositAddressOptions, onAttempt: (attempt) => attempts.push(attempt), providers: [ withCreateDepositAddress('relay', async () => { throw new FundingProvider.ProviderUnavailableError() }), ], }), ).resolves.toBeUndefined() expect(attempts.map(stableAttempt)).toEqual([ { operation: 'createDepositAddress', outcome: 'unavailable', provider: 'relay' }, ]) }) test('rejects malformed provider responses', async () => { await expect( FundingProvider.createDepositAddress({ ...depositAddressOptions, providers: [ withCreateDepositAddress('relay', async () => ({ address: '', correlation: { requestId: '' }, destinationAmount: '1000000', destinationAmountMin: '1000000', fees: [], sampledAt: '2026-08-04T00:00:00.000Z', })), ], }), ).rejects.toMatchObject({ providerFailure: { failure: 'payload', id: 'relay', operation: 'createDepositAddress' }, }) }) test('rejects provider addresses that do not match the source chain', async () => { await expect( FundingProvider.createDepositAddress({ ...depositAddressOptions, providers: [ withCreateDepositAddress('relay', async () => ({ ...depositAddressResult(), address: `0x${'11'.repeat(20)}`, })), ], }), ).rejects.toMatchObject({ providerFailure: { failure: 'payload', id: 'relay', operation: 'createDepositAddress' }, }) }) test('rejects provider minimums above the estimated destination amount', async () => { await expect( FundingProvider.createDepositAddress({ ...depositAddressOptions, providers: [ withCreateDepositAddress('relay', async () => ({ ...depositAddressResult(), destinationAmountMin: '1000001', })), ], }), ).rejects.toMatchObject({ providerFailure: { failure: 'payload', id: 'relay', operation: 'createDepositAddress' }, }) }) test('rejects expired provider terms', async () => { await expect( FundingProvider.createDepositAddress({ ...depositAddressOptions, providers: [ withCreateDepositAddress('relay', async () => ({ ...depositAddressResult(), expiresAt: '2000-01-01T00:00:00.000Z', })), ], }), ).rejects.toMatchObject({ providerFailure: { failure: 'payload', id: 'relay', operation: 'createDepositAddress' }, }) }) test('rejects invalid refund addresses before calling the provider', async () => { await expect( FundingProvider.createDepositAddress({ ...depositAddressOptions, providers: [ withCreateDepositAddress('relay', async () => { throw new Error('provider should not be called') }), ], refundAddress: `0x${'11'.repeat(20)}`, }), ).rejects.toThrowErrorMatchingInlineSnapshot(`[FundingProvider.InvalidRefundAddressError]`) }) }) describe('getDepositAddressCandidates', () => { test('selects every curated Relay stablecoin route', () => { const routes = [ { destinationToken: '0x20c000000000000000000000b9537d11c60e8b50', sourceChain: 'ethereum', sourceToken: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', }, { destinationToken: '0x20c00000000000000000000014f22ca97301eb73', sourceChain: 'ethereum', sourceToken: '0xdac17f958d2ee523a2206206994597c13d831ec7', }, { destinationToken: '0x20c000000000000000000000b9537d11c60e8b50', sourceChain: 'base', sourceToken: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', }, { destinationToken: '0x20c00000000000000000000014f22ca97301eb73', sourceChain: 'base', sourceToken: '0xfde4c96c8593536e31f229ea8f37b2ada2699bb2', }, { destinationToken: '0x20c000000000000000000000b9537d11c60e8b50', sourceChain: 'bnb', sourceToken: '0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d', }, { destinationToken: '0x20c00000000000000000000014f22ca97301eb73', sourceChain: 'bnb', sourceToken: '0x55d398326f99059ff775485246999027b3197955', }, { destinationToken: '0x20c000000000000000000000b9537d11c60e8b50', sourceChain: 'polygon', sourceToken: '0x3c499c542cef5e3811e1192ce70d8cc03d5c3359', }, { destinationToken: '0x20c00000000000000000000014f22ca97301eb73', sourceChain: 'polygon', sourceToken: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f', }, { destinationToken: '0x20c000000000000000000000b9537d11c60e8b50', sourceChain: 'solana', sourceToken: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', }, { destinationToken: '0x20c00000000000000000000014f22ca97301eb73', sourceChain: 'solana', sourceToken: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', }, { destinationToken: '0x20c00000000000000000000014f22ca97301eb73', sourceChain: 'tron', sourceToken: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', }, ] expect( routes.map((route) => { const candidate = FundingProvider.getDepositAddressCandidates({ ...route, catalog: TestFunding.snapshot, destinationChainId: 4217, providers: [providers[1]!], })[0]?.candidate return candidate ? { destinationToken: candidate.destinationToken.symbol, sourceChain: candidate.sourceChain.id, sourceToken: candidate.sourceToken.symbol, } : undefined }), ).toMatchInlineSnapshot(` [ { "destinationToken": "USDC.e", "sourceChain": "eip155:1", "sourceToken": "USDC", }, { "destinationToken": "USDT0", "sourceChain": "eip155:1", "sourceToken": "USDT", }, { "destinationToken": "USDC.e", "sourceChain": "eip155:8453", "sourceToken": "USDC", }, { "destinationToken": "USDT0", "sourceChain": "eip155:8453", "sourceToken": "USDT", }, { "destinationToken": "USDC.e", "sourceChain": "eip155:56", "sourceToken": "USDC", }, { "destinationToken": "USDT0", "sourceChain": "eip155:56", "sourceToken": "USDT", }, { "destinationToken": "USDC.e", "sourceChain": "eip155:137", "sourceToken": "USDC", }, { "destinationToken": "USDT0", "sourceChain": "eip155:137", "sourceToken": "USDT", }, { "destinationToken": "USDC.e", "sourceChain": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "sourceToken": "USDC", }, { "destinationToken": "USDT0", "sourceChain": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "sourceToken": "USDT", }, { "destinationToken": "USDT0", "sourceChain": "tron:0x2b6653dc", "sourceToken": "USDT", }, ] `) }) test('requires both catalog and provider capability', () => { const quoteOnly = FundingProvider.from({ getQuote: (input, signal) => providers[1]!.getQuote(input, signal), id: 'relay', name: 'Relay', }) expect( FundingProvider.getDepositAddressCandidates({ ...depositAddressOptions, providers: [quoteOnly], }), ).toEqual([]) expect( FundingProvider.getDepositAddressCandidates({ ...depositAddressOptions, providers: [providers[1]!], }), ).toHaveLength(1) }) }) describe('prepareTransfer', () => { test('validates preparation results at the provider boundary', async () => { const transfer = FundingProvider.prepareTransfer({ ...prepareOptions, providers: [ withPrepareTransfer( 'relay', async () => ({ ...preparedResult(), sampledAt: 'invalid' }) as never, ), ], }) await expect(transfer).rejects.toMatchObject({ providerFailure: { failure: 'payload', id: 'relay', operation: 'prepareTransfer' }, }) }) test('enforces provider-style slippage rounding', async () => { const inputs: FundingProvider.prepareTransfer.Parameters[] = [] const prepare = (destinationAmountMin: string) => FundingProvider.prepareTransfer({ ...prepareOptions, providers: [ withPrepareTransfer('relay', async (input) => { inputs.push(input) return { ...preparedResult(), destinationAmount: '10001', destinationAmountMin, } }), ], slippageBps: 1, }) await expect(prepare('9998')).rejects.toMatchObject({ providerFailure: { failure: 'payload', id: 'relay', operation: 'prepareTransfer' }, }) await expect(prepare('9999')).resolves.toMatchObject({ result: { destinationAmount: '10001', destinationAmountMin: '9999' }, }) await expect(prepare('10000')).resolves.toMatchObject({ result: { destinationAmount: '10001', destinationAmountMin: '10000' }, }) expect(inputs.map((input) => input.slippageBps)).toEqual([1, 1, 1]) }) test.each([ { expiresAt: '2026-01-01T00:00:59.999Z', sampledAt: '2026-01-01T00:00:00.000Z' }, { sampledAt: '2025-12-31T23:59:59.999Z' }, ])('rejects expired preparations %#', async (quote) => { vi.useFakeTimers() vi.setSystemTime(new Date('2026-01-01T00:01:00.000Z')) try { await expect( FundingProvider.prepareTransfer({ ...prepareOptions, providers: [ withPrepareTransfer('relay', async () => ({ ...preparedResult(), ...quote })), ], }), ).rejects.toMatchObject({ providerFailure: { failure: 'payload', id: 'relay', operation: 'prepareTransfer' }, }) } finally { vi.useRealTimers() } }) test('returns no selection when every provider confirms unavailability', async () => { const attempts: FundingProvider.ProviderAttempt[] = [] await expect( FundingProvider.prepareTransfer({ ...prepareOptions, onAttempt: (attempt) => attempts.push(attempt), providers: [ withPrepareTransfer('relay', async () => { throw new FundingProvider.ProviderUnavailableError() }), ], }), ).resolves.toBeUndefined() expect(attempts.map(stableAttempt)).toEqual([ { operation: 'prepareTransfer', outcome: 'unavailable', provider: 'relay' }, ]) }) test('accepts preparations without correlation', async () => { const { correlation: _correlation, ...result } = preparedResult() const spender = `0x${'44'.repeat(20)}` const selected = FundingProvider.prepareTransfer({ ...prepareOptions, providers: [ withPrepareTransfer('relay', async () => { return { ...result, action: { calls: [ { data: `0x095ea7b3${spender.slice(2).padStart(64, '0')}${(1_000_000).toString(16).padStart(64, '0')}`, to: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', value: '0x0', }, { data: '0xe8017952', to: spender, value: '0x0' }, ], type: 'evm:calls', }, } }), ], }) expect((await selected)?.result.correlation).toBeUndefined() }) }) describe('getPrepareCandidates', () => { test('restricts candidates to the requested provider', () => { const relay = withPrepareTransfer('relay', async () => preparedResult()) const stargate = withPrepareTransfer('stargate', async () => preparedResult()) const route = TestFunding.data.routes.find( (entry) => entry.providerId === 'relay' && entry.capabilities?.transfer, )! const catalog = FundingCatalog.compile({ catalog: { id: 'default', updatedAt: '2026-01-01T00:00:00.000Z', version: 'test', }, ...TestFunding.data, routes: [...TestFunding.data.routes, { ...route, providerId: 'stargate' }], }) expect( FundingProvider.getPrepareCandidates({ ...prepareOptions, catalog, provider: 'stargate', providers: [relay, stargate], }).map(({ provider }) => provider.id), ).toEqual(['stargate']) }) test('restricts candidates to the selected destination chain', () => { const provider = withPrepareTransfer('relay', async () => preparedResult()) expect( FundingProvider.getPrepareCandidates({ ...prepareOptions, destinationChainId: 4217, providers: [provider], }), ).toHaveLength(1) expect( FundingProvider.getPrepareCandidates({ ...prepareOptions, destinationChainId: 421700001, providers: [provider], }), ).toEqual([]) }) test('uses transfer modes without treating deposit addresses as transfers', () => { const catalog = FundingCatalog.compile({ catalog: { id: 'default', updatedAt: '2026-01-01T00:00:00.000Z', version: 'test', }, ...TestFunding.data, routes: TestFunding.data.routes.map((route) => route.capabilities ? { ...route, capabilities: { depositAddress: true, transfer: { modes: ['exactDestination'] }, }, } : route, ), }) const provider = withPrepareTransfer('relay', async () => preparedResult()) const candidates = (mode: 'exactDestination' | 'exactSource') => FundingProvider.getPrepareCandidates({ ...prepareOptions, catalog, mode, providers: [provider], }) expect(candidates('exactDestination')).toHaveLength(1) expect(candidates('exactSource')).toEqual([]) const depositAddressOnly = FundingCatalog.compile({ catalog: { id: 'default', updatedAt: '2026-01-01T00:00:00.000Z', version: 'test', }, ...TestFunding.data, routes: TestFunding.data.routes.map((route) => route.capabilities ? { ...route, capabilities: { depositAddress: true } } : route, ), }) expect( FundingProvider.getPrepareCandidates({ ...prepareOptions, catalog: depositAddressOnly, providers: [provider], }), ).toEqual([]) }) }) function stableAttempt(attempt: FundingProvider.ProviderAttempt) { const { durationMs: _durationMs, provider, ...rest } = attempt return { ...rest, provider: provider.id } } function withGetQuote( id: FundingProvider.ProviderId, getQuote: FundingProvider.Provider['getQuote'], ) { const provider = providers.find((entry) => entry.id === id) if (!provider) throw new Error(`Missing ${id} provider`) return FundingProvider.from({ ...provider, getQuote }) } function depositAddressResult(): FundingProvider.createDepositAddress.ReturnType { return { address: 'TJRabPrwbZy45sbavfcjinPJC18kjpRTv8', correlation: { requestId: 'request_1' }, destinationAmount: '1000000', destinationAmountMin: '1000000', fees: [], sampledAt: '2026-08-04T00:00:00.000Z', } } function withCreateDepositAddress( id: FundingProvider.ProviderId, createDepositAddress: NonNullable, ) { const provider = providers.find((entry) => entry.id === id) if (!provider) throw new Error(`Missing ${id} provider`) return FundingProvider.from({ ...provider, createDepositAddress }) } function preparedResult(): FundingProvider.prepareTransfer.ReturnType { return { action: { calls: [{ data: '0xe8017952', to: `0x${'44'.repeat(20)}`, value: '0x0' }], type: 'evm:calls', }, correlation: { requestId: `0x${'ab'.repeat(32)}` }, destinationAmount: '990000', destinationAmountMin: '980000', fees: [], sampledAt: '2099-01-01T00:00:00.000Z', } } function withPrepareTransfer( id: FundingProvider.ProviderId, prepareTransfer: NonNullable, ) { const provider = providers.find((entry) => entry.id === id) if (!provider) throw new Error(`Missing ${id} provider`) return FundingProvider.from({ ...provider, prepareTransfer }) } describe('getChains', () => { test('derives custom chain and token inventory from provider routes', () => { const catalog = FundingCatalog.compile({ catalog: { id: 'default', updatedAt: '2026-01-01T00:00:00.000Z', version: 'custom', }, chains: [ { aliases: [], id: 'eip155:10', name: 'Optimism', parentChainId: null, rpcUrls: [], slug: 'optimism', }, { aliases: [], id: 'eip155:4217', name: 'Tempo', parentChainId: null, rpcUrls: [], slug: 'tempo', }, ], chainTokens: [ { address: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', chainId: 'eip155:10', decimals: 6, name: null, standard: 'ERC-20', tokenId: 'usdc', }, { address: '0x20c000000000000000000000b9537d11c60e8b50', chainId: 'eip155:4217', decimals: 6, name: null, standard: 'TIP-20', tokenId: 'usdce', }, ], routes: [ { destinationChainId: 'eip155:4217', destinationTokenId: 'usdce', providerId: 'custom', sourceChainId: 'eip155:10', sourceTokenId: 'usdc', }, ], tokens: [ { currency: 'USD', id: 'usdc', name: 'USD Coin', symbol: 'USDC', }, { currency: 'USD', id: 'usdce', name: 'Bridged USDC (Stargate)', symbol: 'USDC.e', }, ], }) const provider = FundingProvider.from({ id: 'custom', name: 'Custom', async getQuote() { return { sampledAt: '2026-01-01T00:00:00Z', status: 'unavailable' } }, }) expect(FundingProvider.getChains({ catalog, providers: [provider] })).toMatchInlineSnapshot(` [ { "addressFormat": "hex", "id": "eip155:10", "kind": "evm", "name": "Optimism", "tokens": [ { "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", "currency": "USD", "decimals": 6, "name": "USD Coin", "standard": "ERC-20", "symbol": "USDC", "tokenKey": "eip155:10/erc20:0x0b2c639c533813f4aa9d7837caf62653d097ff85", "verified": true, }, ], }, ] `) }) test('returns only chain and token inputs with quote inventory', () => { expect( FundingProvider.getChains({ catalog: TestFunding.snapshot, providers }).map( (chain) => chain.id, ), ).toEqual([ 'eip155:1', 'eip155:8453', 'eip155:56', 'eip155:137', 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', 'tron:0x2b6653dc', ]) }) test('uses chain-specific token keys for non-EVM inventory', () => { const chains = FundingProvider.getChains({ catalog: TestFunding.snapshot, providers }) expect(chains.find((chain) => chain.kind === 'solana')?.tokens[0]?.tokenKey).toBe( 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', ) expect(chains.find((chain) => chain.kind === 'tron')?.tokens[0]?.tokenKey).toBe( 'tron:0x2b6653dc/trc20:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', ) }) }) describe('getQuoteCandidates', () => { test('validates base-unit syntax without configured providers', () => { expect(() => FundingProvider.getQuoteCandidates({ ...baseUsdcRequest, providers: [], sourceAmount: '1.5', }), ).toThrowErrorMatchingInlineSnapshot(`[FundingProvider.InvalidAmountError]`) }) test('converts decimal source amounts with token precision', () => { const [candidate] = FundingProvider.getQuoteCandidates({ ...baseUsdcRequest, sourceAmount: '1.25', sourceAmountUnits: 'decimal', providers, }) expect(candidate?.sourceAmount).toEqual({ amount: '1250000', formatted: '1.25' }) }) test('carries provider route configuration without duplicating token metadata', () => { const [candidate] = FundingProvider.getQuoteCandidates({ ...baseUsdcRequest, provider: 'stargate', providers, }) expect(candidate?.configuration).toEqual({ poolAddress: '0x27a16dc786820B16E5c9028b75B99F6f604b5d26', }) expect(candidate?.sourceToken.address).toBe('0x833589fcd6edb6e08f4c7c32d4f71b54bda02913') }) test('resolves normalized catalog aliases', () => { const catalog = FundingCatalog.compile({ catalog: { id: 'default', updatedAt: '2026-01-01T00:00:00.000Z', version: 'aliases', }, ...TestFunding.data, chains: TestFunding.data.chains.map((chain) => chain.id === 'eip155:56' ? { ...chain, aliases: ['BSC'] } : chain, ), }) expect( FundingProvider.getQuoteCandidates({ ...baseUsdcRequest, catalog, providers, sourceChain: 'bsc', })[0]?.sourceChain.id, ).toBe('eip155:56') }) test('uses canonical token slugs in quote ids', () => { const bridgedAddress = '0x1111111111111111111111111111111111111111' const catalog = FundingCatalog.compile({ catalog: { id: 'default', updatedAt: '2026-01-01T00:00:00.000Z', version: 'duplicate-symbols', }, ...TestFunding.data, chainTokens: [ ...TestFunding.data.chainTokens, { address: bridgedAddress, chainId: 'eip155:8453', decimals: 6, name: null, standard: 'ERC-20', tokenId: 'usdc-bridged', }, ], routes: [ ...TestFunding.data.routes, { destinationChainId: 'eip155:4217', destinationTokenId: 'usdce', providerId: 'relay', sourceChainId: 'eip155:8453', sourceTokenId: 'usdc-bridged', }, ], tokens: [ ...TestFunding.data.tokens, { currency: 'USD', id: 'usdc-bridged', name: 'Bridged USD Coin', symbol: 'USDC', }, ], }) const ids = [ FundingProvider.getQuoteCandidates({ ...baseUsdcRequest, catalog, providers, sourceToken: TestFunding.data.chainTokens.find( (token) => token.chainId === 'eip155:8453' && token.tokenId === 'usdc', )!.address, })[0]?.id, FundingProvider.getQuoteCandidates({ ...baseUsdcRequest, catalog, providers, sourceToken: bridgedAddress, })[0]?.id, ] expect(ids).toEqual(['base-usdc-tempo-usdce-relay', 'base-usdc-bridged-tempo-usdce-relay']) }) test('rejects decimal source amounts beyond token precision', () => { expect(() => FundingProvider.getQuoteCandidates({ ...baseUsdcRequest, sourceAmount: '1.0000001', sourceAmountUnits: 'decimal', providers, }), ).toThrowErrorMatchingInlineSnapshot(`[FundingProvider.InvalidAmountError]`) }) test('round-trips the advertised Solana chain and token ids', () => { const [candidate] = FundingProvider.getQuoteCandidates({ catalog: TestFunding.snapshot, providers, provider: 'rhino', sourceAmount: '1000000', sourceAmountUnits: 'baseUnits', sourceChain: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', sourceToken: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', }) expect(candidate?.id).toBe('solana-usdc-tempo-usdce-rhino') }) test('keeps non-EVM token identifiers case-sensitive', () => { expect( FundingProvider.getQuoteCandidates({ catalog: TestFunding.snapshot, providers, provider: 'rhino', sourceAmount: '1000000', sourceAmountUnits: 'baseUnits', sourceChain: 'solana', sourceToken: 'epjfwdd5aufqssqem2qn1xzybapc8g4wegkzwytdt1v', }), ).toEqual([]) }) test('resolves the complete Tron token key', () => { const [candidate] = FundingProvider.getQuoteCandidates({ catalog: TestFunding.snapshot, providers, destinationToken: 'USDT0', provider: 'rhino', sourceAmount: '1000000', sourceAmountUnits: 'baseUnits', sourceChain: 'tron:0x2b6653dc', sourceToken: 'tron:0x2b6653dc/trc20:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', }) expect(candidate?.id).toBe('tron-usdt-tempo-usdt0-rhino') }) test('selects proven Squid stablecoin corridors', () => { const candidates = [ FundingProvider.getQuoteCandidates({ catalog: TestFunding.snapshot, providers, destinationToken: 'USDC.e', provider: 'squid', sourceAmount: '1000000', sourceAmountUnits: 'baseUnits', sourceChain: 'base', sourceToken: 'USDC', })[0], FundingProvider.getQuoteCandidates({ catalog: TestFunding.snapshot, providers, destinationToken: 'USDT0', provider: 'squid', sourceAmount: '1000000', sourceAmountUnits: 'baseUnits', sourceChain: 'ethereum', sourceToken: 'USDT', })[0], ] expect(candidates.map((candidate) => candidate?.id)).toEqual([ 'base-usdc-tempo-usdce-squid', 'ethereum-usdt-tempo-usdt0-squid', ]) }) test('selects proven Symbiosis stablecoin corridors', () => { const candidates = [ FundingProvider.getQuoteCandidates({ catalog: TestFunding.snapshot, providers, destinationToken: 'USDC.e', provider: 'symbiosis', sourceAmount: '1000000', sourceAmountUnits: 'baseUnits', sourceChain: 'base', sourceToken: 'USDC', })[0], FundingProvider.getQuoteCandidates({ catalog: TestFunding.snapshot, providers, destinationToken: 'USDT0', provider: 'symbiosis', sourceAmount: '1000000', sourceAmountUnits: 'baseUnits', sourceChain: 'ethereum', sourceToken: 'USDT', })[0], FundingProvider.getQuoteCandidates({ catalog: TestFunding.snapshot, providers, destinationToken: 'USDT0', provider: 'symbiosis', sourceAmount: '1000000', sourceAmountUnits: 'baseUnits', sourceChain: 'tron', sourceToken: 'USDT', })[0], ] expect(candidates.map((candidate) => candidate?.id)).toEqual([ 'base-usdc-tempo-usdce-symbiosis', 'ethereum-usdt-tempo-usdt0-symbiosis', 'tron-usdt-tempo-usdt0-symbiosis', ]) }) }) describe('getProviders', () => { test('lists supplied providers', () => { expect(FundingProvider.getProviders({ catalog: TestFunding.snapshot, providers })) .toMatchInlineSnapshot(` [ { "id": "across", "name": "Across", }, { "id": "relay", "name": "Relay", }, { "id": "rhino", "name": "Rhino", }, { "id": "squid", "name": "Squid", }, { "id": "stargate", "name": "Stargate", }, { "id": "symbiosis", "name": "Symbiosis", }, ] `) }) test('ignores routes for unregistered provider ids', () => { const catalog = FundingCatalog.compile({ catalog: { id: 'default', updatedAt: '2026-01-01T00:00:00.000Z', version: 'test', }, ...TestFunding.data, routes: [{ ...TestFunding.data.routes[0]!, providerId: 'unknown' }], }) expect({ chains: FundingProvider.getChains({ catalog, providers: [providers[1]!] }), providers: FundingProvider.getProviders({ catalog, providers: [providers[1]!] }), }).toEqual({ chains: [], providers: [] }) }) })