import { Hono } from 'hono' import * as z from 'zod/mini' import type * as App from '../../../App.js' import * as Auth from '../../../internal/Auth.js' import * as FundingCatalog from '../../../internal/funding/Catalog.js' import * as FundingProvider from '../../../internal/funding/Provider.js' import * as OpenApi from '../../../internal/OpenApi.js' import * as Response from '../../../internal/Response.js' import * as Schema from '../../../internal/Schema.js' import * as Value from '../../../internal/Value.js' import * as ProviderAttempts from '../ProviderAttempts.js' /** Zod schemas owned by funding quote resources. */ export namespace schema { /** Successful live funding quote. */ export const FundingQuote = FundingProvider.schema.FundingQuote /** Schemas for the quoteFunding operation. */ export namespace quoteFunding { /** Query parameters for live funding quotes. */ export const Query = FundingProvider.schema.QuoteRequest /** Non-paginated list of successful funding quotes. */ export const Response = OpenApi.component( Schema.describe( z.object({ data: z.array(FundingQuote).check(z.describe('Successful live funding quotes.')), }), 'A non-paginated list of successful live funding quotes.', ), 'FundingQuoteList', ) } } const baseChainExample = { addressFormat: 'hex', id: 'eip155:8453', kind: 'evm', name: 'Base', } as const const baseUsdcExample = { address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', currency: 'USD', decimals: 6, name: 'USD Coin', standard: 'ERC-20', symbol: 'USDC', tokenKey: 'eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', verified: true, } as const const fundingQuoteResponseExample = { data: [ { destinationAmount: Value.tokenAmount({ baseUnits: '979295', currency: 'USD', decimals: 6, }), destinationAmountMin: Value.tokenAmount({ baseUnits: '939242', currency: 'USD', decimals: 6, }), destinationChain: { addressFormat: 'hex', id: 'eip155:4217', kind: 'evm', name: 'Tempo', }, destinationToken: { address: '0x20c000000000000000000000b9537d11c60e8b50', currency: 'USD', decimals: 6, name: 'Bridged USDC (Stargate)', standard: 'TIP-20', symbol: 'USDC.e', tokenKey: 'eip155:4217/erc20:0x20c000000000000000000000b9537d11c60e8b50', verified: true, }, id: 'base-usdc-tempo-usdce-relay', provider: { id: 'relay', name: 'Relay' }, quality: { estimatedSeconds: 1, liquiditySource: 'providerQuote', sourceDetail: 'relay:quote-v2', tier: 'liquid', }, quote: { sampledAt: '2026-01-01T00:00:00.000Z', }, sourceAmount: Value.tokenAmount({ baseUnits: '1000000', currency: 'USD', decimals: 6, }), sourceChain: baseChainExample, sourceToken: baseUsdcExample, }, ], } satisfies z.output /** Mounts live funding quote routes. */ export function quotes() { return new Hono().get( '/v1/funding/quotes', Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }), OpenApi.validate('query', schema.quoteFunding.Query, { code: 'query_invalid', message: 'Invalid query parameters', }), OpenApi.describeRoute({ description: 'Returns live quotes for transferring stablecoins to Tempo.', operationId: 'quoteFunding', responses: OpenApi.responses({ errors: { 400: { codes: ['query_invalid'] }, 502: { codes: ['upstream_error'], description: 'No quote succeeded and at least one matching provider failed unexpectedly.', }, }, success: { description: 'Live funding quotes. An empty list means no provider returned a quote.', example: fundingQuoteResponseExample, schema: schema.quoteFunding.Response, }, }), summary: 'Get quotes', tags: ['Quotes'], }), async (c) => { if (Auth.narrowAccess) return Auth.paidAccessError(c) if (OpenApi.narrowValidation) return OpenApi.validationError(c, { code: 'query_invalid', message: 'Invalid query parameters', }) const query = c.req.valid('query') const attempts: FundingProvider.ProviderAttempt[] = [] const catalog = await FundingCatalog.snapshot(c) const providers = c.get('providers').filter(FundingProvider.is) try { c.header('Cache-Control', 'no-store') const data = await FundingProvider.getQuotes({ cache: { bypass: cacheBypass(c.req.raw) }, catalog, destinationToken: query.destinationToken, onAttempt: (attempt) => attempts.push(attempt), provider: query.provider, providers, sourceAmount: query.sourceAmount, sourceAmountUnits: query.sourceAmountUnits, sourceChain: query.sourceChain, sourceToken: query.sourceToken, }) if (attempts.length > 0) c.set('fundingProviderAttempts', attempts.map(ProviderAttempts.format)) return c.json( Response.validated(schema.quoteFunding.Response, { data, }), 200, ) } catch (cause) { const message = requestErrorMessage(cause) if (message) return Response.error(c, { code: 'query_invalid', message, status: 400 }) if (cause instanceof FundingProvider.ProviderQuoteError) { if (cause.fundingProviderAttempts.length > 0) c.set('fundingProviderAttempts', cause.fundingProviderAttempts) if (cause.providerFailure) c.set('providerFailure', cause.providerFailure) if (cause.providerFailures) c.set('providerFailures', cause.providerFailures) return Response.error(c, { code: 'upstream_error', message: 'Quote providers could not complete the request.', status: 502, }) } throw cause } }, ) } // Maps caller-input errors to a specific 400 message; anything else rethrows. function requestErrorMessage(cause: unknown) { if (cause instanceof FundingProvider.InvalidAmountError) return 'Invalid `sourceAmount`.' if (cause instanceof FundingProvider.UnsupportedDestinationTokenError) return 'Unsupported `destinationToken`.' if (cause instanceof FundingProvider.UnsupportedProviderError) return 'Unsupported `provider`.' if (cause instanceof FundingProvider.UnsupportedSourceChainError) return 'Unsupported `sourceChain`.' return undefined } function cacheBypass(request: Request) { const directives = ( request.headers.get('Cache-Control') ?? request.headers.get('Pragma') ?? '' ).toLowerCase() return directives.includes('no-cache') || directives.includes('no-store') }