import { Schema } from 'tapimo' import * as FundingProvider from '../../../internal/funding/Provider.js' import * as Across from '../../../internal/funding/providers/across.js' import * as Relay from '../../../internal/funding/providers/relay.js' import * as Rhino from '../../../internal/funding/providers/rhino.js' import * as Squid from '../../../internal/funding/providers/squid.js' import * as Stargate from '../../../internal/funding/providers/stargate.js' import * as Symbiosis from '../../../internal/funding/providers/symbiosis.js' import type * as Log from '../../../internal/Log.js' import * as TestApp from '../../../../test/App.js' import * as TestFunding from '../../../../test/Funding.js' import * as Funding from './quotes.js' const db = TestApp.database() 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 beforeAll(() => TestFunding.publish(db)) function create(options: TestApp.create.Options = {}) { return TestApp.create({ ...options, db }) } describe('GET /funding/quotes', () => { test('publishes alphabetically ordered quote example fields', async () => { const app = create({ auth: false }) const spec = await (await app.request('/openapi.json')).json() const operation = spec.paths['/v1/funding/quotes'].get const response = operation.responses['200'].content['application/json'] const fields = Object.keys(response.example.data[0]) expect(fields).toEqual([...fields].sort()) }) test('returns successful live quotes with output amounts', async () => { const app = create({ providers: [relayProvider()] }) const response = await app.request(quoteUrl(), { headers: TestApp.auth.headers }) expect(response.status).toBe(200) expect(response.headers.get('cache-control')).toBe('no-store') const body = await TestApp.json(response, Funding.schema.quoteFunding.Response) expect(body.data).toHaveLength(1) expect(body.data[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', provider: { id: 'relay' }, quality: { liquiditySource: 'providerQuote', tier: 'liquid' }, quote: { sampledAt: '2026-01-01T00:00:00Z' }, sourceAmount: { baseUnits: '1000000', currency: 'USD', decimals: 6, formatted: '1' }, }) expect(body.data[0]?.sourceToken).not.toHaveProperty('amount') expect(body.data[0]?.destinationToken).not.toHaveProperty('amount') }) test('resolves token identifiers through the composed route', async () => { const app = create({ providers: [relayProvider()] }) const identifiers = [ { destinationToken: 'usdc.e', name: 'symbol', sourceToken: 'USDC' }, { destinationToken: 'usdce', name: 'slug', sourceToken: 'usdc' }, { destinationToken: '0x20c000000000000000000000b9537d11c60e8b50', name: 'address', sourceToken: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', }, { destinationToken: 'eip155:4217/erc20:0x20c000000000000000000000b9537d11c60e8b50', name: 'token key', sourceToken: 'eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', }, ] as const const results = [] for (const identifier of identifiers) { const response = await app.request( quoteUrl({ destinationToken: identifier.destinationToken, sourceAmount: '1000000', sourceChain: 'base', sourceToken: identifier.sourceToken, }), { headers: TestApp.auth.headers }, ) const body = await TestApp.json(response, Funding.schema.quoteFunding.Response) results.push({ destinationToken: body.data[0]?.destinationToken.symbol, name: identifier.name, sourceToken: body.data[0]?.sourceToken.symbol, status: response.status, }) } expect(results).toMatchInlineSnapshot(` [ { "destinationToken": "USDC.e", "name": "symbol", "sourceToken": "USDC", "status": 200, }, { "destinationToken": "USDC.e", "name": "slug", "sourceToken": "USDC", "status": 200, }, { "destinationToken": "USDC.e", "name": "address", "sourceToken": "USDC", "status": 200, }, { "destinationToken": "USDC.e", "name": "token key", "sourceToken": "USDC", "status": 200, }, ] `) }) test('queries only the requested provider', async () => { let acrossCalls = 0 let relayCalls = 0 const app = create({ providers: [ acrossProvider(() => { acrossCalls += 1 }), relayProvider(() => { relayCalls += 1 }), ], }) const response = await app.request(quoteUrl({ ...baseUsdcQuery, provider: 'across' }), { headers: TestApp.auth.headers, }) expect(response.status).toBe(200) expect(acrossCalls).toBe(1) expect(relayCalls).toBe(0) const body = await TestApp.json(response, Funding.schema.quoteFunding.Response) expect(body.data[0]?.quote.expiresAt).toBe('2026-01-01T00:01:00Z') }) test('returns Stargate quotes for Base USDC to Tempo USDC.e', async () => { const app = create({ providers: [stargateProvider()] }) const response = await app.request(quoteUrl({ ...baseUsdcQuery, provider: 'stargate' }), { headers: TestApp.auth.headers, }) expect(response.status).toBe(200) const body = await TestApp.json(response, Funding.schema.quoteFunding.Response) expect(body.data[0]).toMatchObject({ destinationAmount: { baseUnits: '999600' }, destinationAmountMin: { baseUnits: '999100' }, id: 'base-usdc-tempo-usdce-stargate', provider: { id: 'stargate', name: 'Stargate' }, quality: { liquiditySource: 'onchain', sourceDetail: 'stargate:oft-v2:taxi', tier: 'liquid', }, }) }) test('returns non-EVM Rhino quotes with stable chain and token ids', async () => { const app = create({ providers: [rhinoProvider()] }) const response = await app.request( quoteUrl({ destinationToken: 'USDC.e', provider: 'rhino', sourceAmount: '1000000', sourceChain: 'solana', sourceToken: 'USDC', }), { headers: TestApp.auth.headers }, ) expect(response.status).toBe(200) const body = await TestApp.json(response, Funding.schema.quoteFunding.Response) expect(body.data[0]).toMatchObject({ id: 'solana-usdc-tempo-usdce-rhino', provider: { id: 'rhino' }, sourceChain: { addressFormat: 'base58', id: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', kind: 'solana', }, sourceToken: { tokenKey: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', }, }) }) test('returns an empty list when no provider is available', async () => { const response = await create().request(quoteUrl(), { headers: TestApp.auth.headers, }) expect(response.status).toBe(200) const body = await TestApp.json(response, Funding.schema.quoteFunding.Response) expect(body.data).toEqual([]) }) test('returns an empty list when the provider reports no quote', async () => { const entries: Log.Entry[] = [] const app = create({ logger: (entry) => { entries.push(entry) }, providers: [ withGetQuote('relay', async () => ({ sampledAt: '2026-01-01T00:00:00Z', status: 'unavailable', })), ], }) const response = await app.request(quoteUrl(), { headers: TestApp.auth.headers }) expect(response.status).toBe(200) const body = await TestApp.json(response, Funding.schema.quoteFunding.Response) expect(body.data).toEqual([]) expect(entries[0]?.fundingProviderAttempts).toEqual([ { durationMs: expect.any(Number), id: 'relay', operation: 'getQuote', outcome: 'unavailable', }, ]) }) test('returns 502 when every matching provider fails', async () => { const app = create({ providers: [ withGetQuote('relay', async () => { throw new Error('upstream unavailable') }), ], }) const response = await app.request(quoteUrl(), { headers: TestApp.auth.headers }) expect(response.status).toBe(502) const body = await TestApp.json(response, Schema.ErrorResponse) expect(body.error.code).toBe('upstream_error') }) test('logs provider quote deadlines as timeouts', async () => { vi.useFakeTimers() const entries: Log.Entry[] = [] const app = create({ logger: (entry) => { entries.push(entry) }, providers: [ withGetQuote('across', () => new Promise(() => {})), ], }) try { const response_promise = app.request(quoteUrl({ ...baseUsdcQuery, provider: 'across' }), { headers: TestApp.auth.headers, }) await vi.advanceTimersByTimeAsync(5_000) const response = await response_promise expect(response.status).toBe(502) expect(entries[0]?.provider).toEqual({ failure: 'timeout', id: 'across', operation: 'getQuote', }) expect(entries[0]?.fundingProviderAttempts).toEqual([ { durationMs: expect.any(Number), failure: 'timeout', id: 'across', operation: 'getQuote', outcome: 'failed', }, ]) } finally { vi.useRealTimers() } }) test('logs every failure when multiple providers fail', async () => { const entries: Log.Entry[] = [] const app = create({ logger: (entry) => { entries.push(entry) }, providers: [ withGetQuote('across', async () => { throw new TypeError('Across network failure') }), withGetQuote('relay', async () => { throw new Error('Relay failure') }), ], }) const response = await app.request(quoteUrl(), { headers: TestApp.auth.headers }) expect(response.status).toBe(502) expect(entries[0]?.provider).toEqual({ failure: 'network', id: 'across', operation: 'getQuote', }) expect(entries[0]?.providerFailures).toEqual([ { failure: 'network', id: 'across', operation: 'getQuote' }, { failure: 'unknown', id: 'relay', operation: 'getQuote' }, ]) expect(entries[0]?.fundingProviderAttempts).toEqual([ { durationMs: expect.any(Number), failure: 'network', id: 'across', operation: 'getQuote', outcome: 'failed', }, { durationMs: expect.any(Number), failure: 'unknown', id: 'relay', operation: 'getQuote', outcome: 'failed', }, ]) }) test('logs failed providers when another provider returns a quote', async () => { const entries: Log.Entry[] = [] const app = create({ logger: (entry) => { entries.push(entry) }, providers: [ withGetQuote('across', async () => { throw new TypeError(`wallet ${'0xaa'.repeat(20)} could not reach secret-provider`) }), relayProvider(), ], }) const response = await app.request(quoteUrl(), { headers: TestApp.auth.headers }) expect(response.status).toBe(200) expect(entries[0]?.fundingProviderAttempts).toEqual([ { durationMs: expect.any(Number), failure: 'network', id: 'across', operation: 'getQuote', outcome: 'failed', }, { durationMs: expect.any(Number), id: 'relay', operation: 'getQuote', outcome: 'available', }, ]) expect(JSON.stringify(entries[0])).not.toContain('secret-provider') expect(JSON.stringify(entries[0])).not.toContain('0xaa') }) test('rejects successful provider results without output amounts', async () => { const app = create({ providers: [ withGetQuote('relay', async () => ({ sampledAt: '2026-01-01T00:00:00Z', status: 'available', })), ], }) const response = await app.request(quoteUrl(), { headers: TestApp.auth.headers }) expect(response.status).toBe(502) }) test('rejects invalid query parameters', async () => { const response = await create().request( quoteUrl({ sourceAmount: '1000000', sourceChain: 'base' }), { headers: TestApp.auth.headers }, ) expect(response.status).toBe(400) const body = await TestApp.json(response, Schema.ErrorResponse) expect(body.error.code).toBe('query_invalid') }) test('rejects unsupported providers', async () => { const response = await create().request(quoteUrl({ ...baseUsdcQuery, provider: 'bridge' }), { headers: TestApp.auth.headers, }) expect(response.status).toBe(400) const body = await TestApp.json(response, Schema.ErrorResponse) expect(body.error.code).toBe('query_invalid') }) test('rejects unsupported source chains', async () => { const response = await create().request( quoteUrl({ ...baseUsdcQuery, sourceChain: 'bitcoin' }), { headers: TestApp.auth.headers, }, ) expect(response.status).toBe(400) const body = await TestApp.json(response, Schema.ErrorResponse) expect(body.error.code).toBe('query_invalid') }) test('does not expose the removed routes endpoint', async () => { const response = await create().request( '/v1/funding/routes?sourceChain=base&sourceToken=USDC&sourceAmount=1000000', { headers: TestApp.auth.headers }, ) expect(response.status).toBe(404) }) }) const baseUsdcQuery = { sourceAmount: '1000000', sourceChain: 'base', sourceToken: 'USDC', } as const type QuoteQuery = Record function quoteUrl(query: QuoteQuery = baseUsdcQuery) { return `/v1/funding/quotes?${new URLSearchParams( Object.entries(query).map(([key, value]) => [key, String(value)]), )}` } function relayProvider(onGetQuote: () => void = () => {}) { return withGetQuote('relay', async (input, signal) => { onGetQuote() expect(signal.aborted).toBe(false) expect(input.candidate.id).toBe('base-usdc-tempo-usdce-relay') return { destinationAmountMin: '980000', destinationAmount: '990000', quality: { estimatedSeconds: 4, liquiditySource: 'providerQuote', sourceDetail: 'relay:quote-v2', tier: 'liquid', }, sampledAt: '2026-01-01T00:00:00Z', status: 'available', } }) } function acrossProvider(onGetQuote: () => void = () => {}) { return withGetQuote('across', async (input, signal) => { onGetQuote() expect(signal.aborted).toBe(false) expect(input.candidate.id).toBe('base-usdc-tempo-usdce-across') return { expiresAt: '2026-01-01T00:01:00Z', destinationAmountMin: '988000', destinationAmount: '990000', quality: { estimatedSeconds: 2, liquiditySource: 'providerQuote', sourceDetail: 'across:swap-approval', tier: 'liquid', }, sampledAt: '2026-01-01T00:00:00Z', status: 'available', } }) } function rhinoProvider() { return withGetQuote('rhino', async (input, signal) => { expect(signal.aborted).toBe(false) expect(input.candidate.id).toBe('solana-usdc-tempo-usdce-rhino') return { destinationAmount: '983000', quality: { liquiditySource: 'providerQuote', sourceDetail: 'rhino:bridge-swap-public', tier: 'liquid', }, sampledAt: '2026-01-01T00:00:00Z', status: 'available', } }) } function stargateProvider() { return withGetQuote('stargate', async (input, signal) => { expect(signal.aborted).toBe(false) expect(input.candidate.id).toBe('base-usdc-tempo-usdce-stargate') return { destinationAmountMin: '999100', destinationAmount: '999600', quality: { liquiditySource: 'onchain', sourceDetail: 'stargate:oft-v2:taxi', tier: 'liquid', }, sampledAt: '2026-01-01T00:00:00Z', status: 'available', } }) } 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 }) }