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://v2.api.squidrouter.com/v2' 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.union([z.string(), z.number().check(z.int(), z.nonnegative())]), decimals: z.number().check(z.int(), z.nonnegative()), }) export const Quote = z.object({ route: z.object({ estimate: z.object({ estimatedRouteDuration: z.number().check(z.nonnegative()), fromAmount: IntegerString, fromToken: Token, toAmount: IntegerString, toAmountMin: IntegerString, toToken: Token, }), }), }) export const AmountUnavailable = z.object({ message: z.literal('Please increase the swap amount and try again.'), type: z.literal('BAD_REQUEST'), }) } /** Creates a Squid funding provider backed by `/v2/route`. */ export function squid(options: squid.Options) { if (options.integratorId.length === 0) 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(/\/+$/, '')}/route` const fetch = options.fetch ?? defaultFetch const now = options.now ?? (() => new Date()) return FundingProvider.from({ id: 'squid', name: 'Squid', async getQuote({ candidate, route }, signal) { const destinationChainId = FundingChain.eip155Id(route.destination.chain) const sourceChainId = FundingChain.eip155Id(route.source.chain) try { const quote = await FundingProvider.requestJson(endpoint, { body: { fromAddress: options.userAddress, fromAmount: candidate.sourceAmount.amount, fromChain: sourceChainId, fromToken: candidate.sourceToken.address, quoteOnly: true, // V1 quotes use a fixed 1% tolerance; callers cannot customize execution policy. slippage: 1, toAddress: options.userAddress, toChain: destinationChainId, toToken: candidate.destinationToken.address, }, fetch, headers: { 'x-integrator-id': options.integratorId }, method: 'POST', signal, }) const parsed = schema.Quote.safeParse(quote) if (!parsed.success) throw new FundingProvider.ProviderPayloadError() const response = parsed.data.route.estimate if ( response.fromAmount !== candidate.sourceAmount.amount || !matchesToken(response.fromToken, sourceChainId, candidate.sourceToken) || !matchesToken(response.toToken, destinationChainId, candidate.destinationToken) || BigInt(response.toAmount) <= 0n || BigInt(response.toAmountMin) <= 0n || BigInt(response.toAmountMin) > BigInt(response.toAmount) ) throw new FundingProvider.ProviderPayloadError() return { destinationAmountMin: response.toAmountMin, destinationAmount: response.toAmount, quality: { estimatedSeconds: Math.ceil(response.estimatedRouteDuration), liquiditySource: 'providerQuote', sourceDetail: 'squid:route-v2', tier: 'liquid', }, sampledAt: now().toISOString(), status: 'available', } } catch (cause) { const unavailable = FundingProvider.unavailable(cause, [ 'NO_ROUTE', 'NO_ROUTE_FOUND', 'ROUTE_NOT_FOUND', 'UNSUPPORTED_ROUTE', ]) ?? amountUnavailable(cause) if (!unavailable) throw cause return FundingProvider.unavailableResult({ detail: 'squid:route-v2', message: unavailable, now: now(), source: 'providerQuote', }) } }, }) } export declare namespace squid { /** Squid provider options. */ type Options = { /** Override for tests or Squid test environments. */ baseUrl?: string | undefined /** Fetch implementation override for tests. */ fetch?: FundingProvider.Fetch | undefined /** Squid integrator id sent in the required request header. */ integratorId: string /** Clock override for deterministic tests. */ now?: (() => Date) | undefined /** Quote wallet address used as the Squid sender and recipient. */ userAddress: string } } function amountUnavailable(cause: unknown) { if (!(cause instanceof FundingProvider.ProviderResponseError) || cause.status !== 400) return undefined return schema.AmountUnavailable.safeParse(cause.body).success ? 'AMOUNT_TOO_LOW' : undefined } function matchesToken( token: z.output['route']['estimate']['fromToken'], chainId: string, expected: FundingProvider.TokenRef, ) { return ( token.address.toLowerCase() === expected.address.toLowerCase() && String(token.chainId) === chainId && token.decimals === expected.decimals ) }