import { Address, Hash, Hex } from 'ox' import * as TestApp from '../../../../test/App.js' import * as TestFunding from '../../../../test/Funding.js' import * as FundingDepositRequestObservations from '../../../db/tables/fundingDepositRequestObservations.js' import * as DepositAddress from '../DepositAddress.js' import * as FundingProvider from '../Provider.js' import * as Relay from './relay.js' const apiKey = process.env.RELAY_API_KEY const db = TestApp.database() const sender = '0x1111111111111111111111111111111111111111' const recipient = '0x2222222222222222222222222222222222222222' const webhookNow = new Date('2027-01-01T00:00:00.000Z') const webhookSecret = 'relay-secret' function webhookPayload(address: string | null = 'TJRabPrwbZy45sbavfcjinPJC18kjpRTv8') { return JSON.stringify({ data: { depositAddress: address ? { address, depositAddressType: 'open', depositor: null } : null, requestId: `0x${'aa'.repeat(32)}`, status: 'pending', updatedAt: webhookNow.getTime() - 1_000, }, event: 'request.status.updated', timestamp: webhookNow.getTime(), }) } function signedWebhook(body: string) { const timestamp = String(webhookNow.getTime()) const signature = Hash.hmac256( Hex.fromString(webhookSecret), Hex.fromString(`${timestamp}.${body}`), ).slice(2) return { body, headers: new Headers({ 'x-signature-sha256': signature, 'x-signature-timestamp': timestamp, }), } } function webhookProvider( dispatch: (message: FundingProvider.Webhook.receive.Dispatchable) => Promise, options: webhookProvider.Options = {}, ) { return Relay.relay({ apiKey: webhookSecret, webhook: { db, dispatch, ...(options.onResult ? { onResult: options.onResult } : {}) }, }) } declare namespace webhookProvider { /** Optional webhook observation hooks used by tests. */ type Options = { /** Receives bounded webhook dispositions. */ onResult?: Relay.relay.Webhook['onResult'] | undefined } } function candidateFor(provider: FundingProvider.Provider) { const candidate = FundingProvider.getQuoteCandidates({ catalog: TestFunding.snapshot, destinationToken: 'pathUSD', providers: [provider], sourceAmount: '5000000', sourceAmountUnits: 'baseUnits', sourceChain: 'base', sourceToken: 'USDC', }).find((entry) => entry.id === 'base-usdc-tempo-pathusd-relay') if (!candidate) throw new Error('Expected a relay candidate.') return candidate } function prepareInput( provider: FundingProvider.Provider, ): FundingProvider.prepareTransfer.Parameters { return { candidate: candidateFor(provider), method: 'transaction', mode: 'exactSource', recipient, sender, slippageBps: 300, } } function depositAddressInput( provider: FundingProvider.Provider, ): FundingProvider.createDepositAddress.Parameters { const candidate = FundingProvider.getDepositAddressCandidates({ catalog: TestFunding.snapshot, destinationChainId: 4217, destinationToken: '0x20c00000000000000000000014f22ca97301eb73', providers: [provider], sourceChain: 'tron', sourceToken: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', })[0]?.candidate if (!candidate) throw new Error('Expected a Relay deposit-address candidate.') return { amount: '100000000', candidate, recipient, refundAddress: 'TJRabPrwbZy45sbavfcjinPJC18kjpRTv8', subsidize: false, } } // Requests against the live Relay API (quote-only; nothing signs, submits, or // funds). Runs whenever RELAY_API_KEY is in the repo-root .env; CI without the // key skips. Live amounts change per quote, so assertions stay shape-level. describe.skipIf(!apiKey)('relay.prepareTransfer (live)', () => { let requestBody: Record let result: FundingProvider.prepareTransfer.ReturnType beforeAll(async () => { const provider = Relay.relay({ apiKey, fetch: async (input, init) => { if (typeof init?.body !== 'string') throw new Error('Expected a JSON request body.') requestBody = JSON.parse(init.body) as Record return globalThis.fetch(input, init) }, }) result = await provider.prepareTransfer!(prepareInput(provider), new AbortController().signal) }) test('forwards the caller terms', () => { expect(requestBody).toMatchObject({ amount: '5000000', destinationChainId: 4217, originChainId: 8453, recipient, slippageTolerance: '300', tradeType: 'EXACT_INPUT', user: sender, }) }) test('prepares bounded executable calls with private correlation', () => { expect(result.action.type).toBe('evm:calls') expect(result.action.calls.length).toBeGreaterThan(0) for (const call of result.action.calls) { expect(call.to).toMatch(/^0x[0-9a-fA-F]{40}$/) expect(call.data).toMatch(/^0x[0-9a-fA-F]*$/) expect(call.value).toBe('0x0') } expect(result.correlation?.requestId).toMatch(/^0x[0-9a-f]{64}$/) expect(result.correlation?.checkEndpoint).toContain('/intents/status/v3') expect(BigInt(result.destinationAmount) > 0n).toBe(true) expect(BigInt(result.destinationAmountMin) > 0n).toBe(true) expect(BigInt(result.destinationAmountMin) <= BigInt(result.destinationAmount)).toBe(true) // Relay returns no quote validity window; the fan-out applies the default. expect(result.expiresAt).toBeUndefined() expect(Number.isNaN(Date.parse(result.sampledAt))).toBe(false) }) }) describe.skipIf(!apiKey)('relay.createDepositAddress (live)', () => { let requestBody: Record let result: FundingProvider.createDepositAddress.ReturnType beforeAll(async () => { const provider = Relay.relay({ apiKey, fetch: async (input, init) => { if (typeof init?.body !== 'string') throw new Error('Expected a JSON request body.') requestBody = JSON.parse(init.body) as Record return globalThis.fetch(input, init) }, subsidies: { maxAmount: '100' }, }) result = await provider.createDepositAddress!( { ...depositAddressInput(provider), subsidize: true }, new AbortController().signal, ) }) test('forwards the reusable exact-input route', () => { expect(requestBody).toMatchInlineSnapshot(` { "amount": "100000000", "destinationChainId": 4217, "destinationCurrency": "0x20c00000000000000000000014f22ca97301eb73", "originChainId": 728126428, "originCurrency": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", "recipient": "0x2222222222222222222222222222222222222222", "refundTo": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8", "subsidizeFees": true, "tradeType": "EXACT_INPUT", "useDepositAddress": true, "user": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8", } `) }) test('provisions the curated Tron USDT to Tempo USDT0 route', () => { expect(result).toMatchInlineSnapshot( { address: expect.any(String), correlation: { checkEndpoint: expect.any(String), requestId: expect.any(String), }, destinationAmount: expect.any(String), destinationAmountMin: expect.any(String), fees: expect.any(Array), sampledAt: expect.any(String), }, ` { "address": Any, "correlation": { "checkEndpoint": Any, "requestId": Any, }, "destinationAmount": Any, "destinationAmountMin": Any, "fees": Any, "sampledAt": Any, } `, ) }) test('provisions the curated Solana USDT to Tempo USDT0 route', async () => { let requestBody: Record const provider = Relay.relay({ apiKey, fetch: async (input, init) => { if (typeof init?.body !== 'string') throw new Error('Expected a JSON request body.') requestBody = JSON.parse(init.body) as Record return globalThis.fetch(input, init) }, }) const candidate = FundingProvider.getDepositAddressCandidates({ catalog: TestFunding.snapshot, destinationChainId: 4217, destinationToken: '0x20c00000000000000000000014f22ca97301eb73', providers: [provider], sourceChain: 'solana', sourceToken: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', })[0]?.candidate if (!candidate) throw new Error('Expected a Relay Solana deposit-address candidate.') const result = await provider.createDepositAddress!( { amount: '100000000', candidate, recipient, refundAddress: '11111111111111111111111111111111', subsidize: false, }, new AbortController().signal, ) expect({ requestBody: requestBody!, result }).toMatchInlineSnapshot( { result: { address: expect.any(String), correlation: { checkEndpoint: expect.any(String), requestId: expect.any(String), }, destinationAmount: expect.any(String), destinationAmountMin: expect.any(String), fees: expect.any(Array), sampledAt: expect.any(String), }, }, ` { "requestBody": { "amount": "100000000", "destinationChainId": 4217, "destinationCurrency": "0x20c00000000000000000000014f22ca97301eb73", "originChainId": 792703809, "originCurrency": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", "recipient": "0x2222222222222222222222222222222222222222", "refundTo": "11111111111111111111111111111111", "subsidizeFees": false, "tradeType": "EXACT_INPUT", "useDepositAddress": true, "user": "11111111111111111111111111111111", }, "result": { "address": Any, "correlation": { "checkEndpoint": Any, "requestId": Any, }, "destinationAmount": Any, "destinationAmountMin": Any, "fees": Any, "sampledAt": Any, }, } `, ) }) test('lists child requests for the reusable address', async () => { let requestUrl: URL | undefined const provider = Relay.relay({ apiKey, fetch: (input, init) => { requestUrl = new URL(input instanceof Request ? input.url : input) return globalThis.fetch(input, init) }, }) const requests = await provider.listDepositAddressRequests!( { address: result.address }, new AbortController().signal, ) requestUrl?.searchParams.set('depositAddress', '
') expect({ requestUrl: requestUrl?.toString(), requests }).toMatchInlineSnapshot(` { "requestUrl": "https://api.relay.link/requests/v3?depositAddress=%3Caddress%3E&includeChildRequests=true&limit=50&sortBy=updatedAt&sortDirection=desc", "requests": { "requests": [], }, } `) }) }) describe('parseDepositAddressRequests', () => { test('accepts nullable fields from in-flight requests', () => { expect( Relay.parseDepositAddressRequests({ requests: [ { createdAt: '2026-08-05T06:13:34.181Z', data: { outTxs: [] }, depositAddress: { depositTxHash: null }, id: 'request-waiting', protocol: null, status: 'waiting', updatedAt: '2026-08-05T06:13:37.650Z', }, { createdAt: '2026-08-05T06:13:34.181Z', data: { outTxs: [] }, depositAddress: null, id: 'request-depositing', protocol: null, status: 'depositing', updatedAt: '2026-08-05T06:13:37.650Z', }, ], }), ).toMatchInlineSnapshot(` { "requests": [ { "createdAt": "2026-08-05T06:13:34.181Z", "data": { "outTxs": [], }, "depositAddress": { "depositTxHash": null, }, "id": "request-waiting", "protocol": null, "status": "waiting", "updatedAt": "2026-08-05T06:13:37.650Z", }, { "createdAt": "2026-08-05T06:13:34.181Z", "data": { "outTxs": [], }, "depositAddress": null, "id": "request-depositing", "protocol": null, "status": "depositing", "updatedAt": "2026-08-05T06:13:37.650Z", }, ], } `) }) test('reports the invalid provider field', () => { expect(() => Relay.parseDepositAddressRequests({ requests: null })) .toThrowErrorMatchingInlineSnapshot(` [FundingProvider.ProviderPayloadError: ✖ Invalid input: expected array, received null → at requests] `) }) }) describe('relay.createDepositAddress', () => { test('rejects subsidized provisioning outside the configured limit', async () => { const provider = Relay.relay({ subsidies: { maxAmount: '1' } }) await expect( provider.createDepositAddress!( { ...depositAddressInput(provider), subsidize: true }, new AbortController().signal, ), ).rejects.toThrowErrorMatchingInlineSnapshot(`[FundingProvider.ProviderUnavailableError]`) }) }) describe('relay.prepareTransfer', () => { test('refuses methods and modes it cannot prepare', async () => { const provider = Relay.relay() await expect( provider.prepareTransfer!( { ...prepareInput(provider), method: 'depositAddress' }, new AbortController().signal, ), ).rejects.toThrow(FundingProvider.ProviderConfigurationError) }) }) describe('verifyWebhook', () => { beforeEach(() => vi.useFakeTimers({ now: webhookNow })) afterEach(() => vi.useRealTimers()) test('authenticates the raw body and returns only the reconciliation hint', () => { const { webhook } = webhookProvider(async () => {}) if (!webhook) throw new Error('Expected Relay webhook support.') expect(webhook.verify(signedWebhook(webhookPayload()))).toMatchInlineSnapshot(` { "address": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8", "providerStatus": "pending", "providerUpdatedAt": "2026-12-31T23:59:59.000Z", "requestId": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "sentAt": "2027-01-01T00:00:00.000Z", } `) }) test('rejects changed bodies and stale timestamps', () => { const { webhook } = webhookProvider(async () => {}) if (!webhook) throw new Error('Expected Relay webhook support.') const valid = signedWebhook(webhookPayload()) expect(() => webhook.verify({ ...valid, body: `${valid.body} ` }), ).toThrowErrorMatchingInlineSnapshot(`[Funding.Provider.Relay.WebhookSignatureError]`) valid.headers.set( 'x-signature-timestamp', String(Number(valid.headers.get('x-signature-timestamp')) - 300_001), ) expect(() => webhook.verify(valid)).toThrowErrorMatchingInlineSnapshot( `[Funding.Provider.Relay.WebhookSignatureError]`, ) }) }) describe('receiveWebhook', () => { beforeEach(() => vi.useFakeTimers({ now: webhookNow })) afterEach(() => vi.useRealTimers()) test('resolves the stored address and queues a bounded hint', async () => { const route = TestFunding.transferSnapshot() const providerAddress = Address.checksum(`0x${'ab'.repeat(20)}`) const record = await DepositAddress.create(db, { apiKeyId: 'key_relay_webhook', deliveryStrategy: 'provider', environment: 'production', now: webhookNow, orgId: 'org_relay_webhook', providerOutputToken: TestFunding.providerOutputToken(), snapshot: TestFunding.depositAddressSnapshot({ address: providerAddress, provider: { id: 'relay', name: 'Relay' }, sourceChain: route.sourceChain, sourceToken: route.sourceToken, }), }) const dispatched: FundingProvider.Webhook.receive.Dispatchable[] = [] const { webhook } = webhookProvider(async (message) => { dispatched.push(message) }) if (!webhook) throw new Error('Expected Relay webhook support.') const result = await webhook.receive( signedWebhook(webhookPayload(Address.checksum(record.address))), ) const observation = await FundingDepositRequestObservations.get(db, { depositAddressId: record.id, providerRequestId: `0x${'aa'.repeat(32)}`, }) expect({ dispatched, observation, result }).toMatchInlineSnapshot( { dispatched: [{ addressId: expect.any(String) }], observation: { depositAddressId: expect.any(String) }, result: { addressId: expect.any(String) }, }, ` { "dispatched": [ { "addressId": Any, "requestId": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "trigger": "webhook", "type": "funding:deposit-address:reconcile", "webhookTiming": { "enqueuedAt": "2027-01-01T00:00:00.000Z", "providerStatus": "pending", "providerUpdatedAt": "2026-12-31T23:59:59.000Z", "receivedAt": "2027-01-01T00:00:00.000Z", "sentAt": "2027-01-01T00:00:00.000Z", }, }, ], "observation": { "depositAddressId": Any, "pollObservedAt": null, "providerRequestId": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "webhookReceivedAt": "2027-01-01T00:00:00.000Z", }, "result": { "addressId": Any, "type": "queued", }, } `, ) }) test('acknowledges unattributed and unknown addresses without dispatching', async () => { const dispatched: FundingProvider.Webhook.receive.Dispatchable[] = [] const outcomes: Relay.relay.WebhookResult[] = [] const { webhook } = webhookProvider( async (message) => { dispatched.push(message) }, { onResult: (outcome) => outcomes.push(outcome) }, ) if (!webhook) throw new Error('Expected Relay webhook support.') const ignored = await webhook.receive(signedWebhook(webhookPayload(null))) const unknown = await webhook.receive(signedWebhook(webhookPayload('TUnknownRelayAddress'))) expect({ dispatched, ignored, outcomes, unknown }).toMatchInlineSnapshot(` { "dispatched": [], "ignored": { "type": "ignored", }, "outcomes": [ "ignored", "unknown_address", ], "unknown": { "type": "unknown_address", }, } `) }) })