import { Address } from 'ox' import * as z from 'zod/mini' import * as FundingChain from '../Chain.js' import * as FundingProvider from '../Provider.js' const defaultBaseUrl = 'https://app.across.to/api' const defaultFetch: FundingProvider.Fetch = (input, init) => globalThis.fetch(input, init) namespace schema { const IntegerString = z.string().check(z.regex(/^\d+$/)) const Token = z.object({ address: z.string(), chainId: z.number().check(z.int(), z.nonnegative()), decimals: z.number().check(z.int(), z.nonnegative()), }) export const Quote = z.object({ amountType: z.literal('exactInput'), expectedFillTime: z.number().check(z.int(), z.nonnegative()), expectedOutputAmount: IntegerString, inputAmount: IntegerString, inputToken: Token, minOutputAmount: IntegerString, outputToken: Token, quoteExpiryTimestamp: z.number().check(z.int(), z.nonnegative()), }) } /** Creates an Across funding provider backed by `/swap/approval`. */ export function across(options: across.Options) { if (options.apiKey.length === 0 || !/^0x[\da-fA-F]{4}$/.test(options.integratorId)) throw new FundingProvider.ProviderConfigurationError() if (!Address.validate(options.userAddress, { strict: false })) throw new FundingProvider.ProviderConfigurationError() const endpoint = new URL(options.baseUrl ?? defaultBaseUrl) endpoint.pathname = `${endpoint.pathname.replace(/\/+$/, '')}/swap/approval` const fetch = options.fetch ?? defaultFetch const now = options.now ?? (() => new Date()) return FundingProvider.from({ id: 'across', name: 'Across', async getQuote({ candidate, route }, signal) { const destinationChainId = FundingChain.eip155Id(route.destination.chain) const sourceChainId = FundingChain.eip155Id(route.source.chain) const url = new URL(endpoint) url.search = new URLSearchParams({ amount: candidate.sourceAmount.amount, depositor: options.userAddress, destinationChainId, inputToken: candidate.sourceToken.address, integratorId: options.integratorId, originChainId: sourceChainId, outputToken: candidate.destinationToken.address, recipient: options.userAddress, skipOriginTxEstimation: 'true', strictTradeType: 'true', tradeType: 'exactInput', }).toString() try { const quote = await FundingProvider.requestJson(url, { fetch, headers: { Authorization: `Bearer ${options.apiKey}` }, signal, }) const parsed = schema.Quote.safeParse(quote) if (!parsed.success) throw new FundingProvider.ProviderPayloadError() const response = parsed.data const expiresAt = new Date(response.quoteExpiryTimestamp * 1_000) const sampledAt = now() if ( response.inputAmount !== candidate.sourceAmount.amount || !matchesToken(response.inputToken, sourceChainId, candidate.sourceToken) || !matchesToken(response.outputToken, destinationChainId, candidate.destinationToken) || BigInt(response.expectedOutputAmount) <= 0n || BigInt(response.minOutputAmount) <= 0n || BigInt(response.minOutputAmount) > BigInt(response.expectedOutputAmount) || Number.isNaN(expiresAt.getTime()) || response.quoteExpiryTimestamp * 1_000 <= sampledAt.getTime() ) throw new FundingProvider.ProviderPayloadError() return { expiresAt: expiresAt.toISOString(), destinationAmountMin: response.minOutputAmount, destinationAmount: response.expectedOutputAmount, quality: { estimatedSeconds: response.expectedFillTime, liquiditySource: 'providerQuote', sourceDetail: 'across:swap-approval', tier: 'liquid', }, sampledAt: sampledAt.toISOString(), status: 'available', } } catch (cause) { const unavailable = FundingProvider.unavailable(cause, [ 'AMOUNT_TOO_HIGH', 'AMOUNT_TOO_LOW', 'INSUFFICIENT_LIQUIDITY', 'NO_QUOTES', 'NO_ROUTE', 'UNSUPPORTED_ROUTE', ]) if (!unavailable) throw cause return FundingProvider.unavailableResult({ detail: 'across:swap-approval', message: unavailable, now: now(), source: 'providerQuote', }) } }, }) } export declare namespace across { /** Across provider options. */ type Options = { /** Across API key sent as a bearer token. */ apiKey: string /** Override for tests or Across test environments. */ baseUrl?: string | undefined /** Fetch implementation override for tests. */ fetch?: FundingProvider.Fetch | undefined /** Across integrator id sent as the required query parameter. */ integratorId: string /** Clock override for deterministic tests. */ now?: (() => Date) | undefined /** Quote wallet address used as the Across depositor and recipient. */ userAddress: string } } function matchesToken( token: z.output['inputToken'], chainId: string, expected: FundingProvider.TokenRef, ) { return ( token.address.toLowerCase() === expected.address.toLowerCase() && token.chainId === Number(chainId) && token.decimals === expected.decimals ) }