import { Base64, Hash, Hex } from 'ox' import { decodeAbiParameters, decodeFunctionData, parseAbi, parseAbiParameters } from 'viem' import * as z from 'zod/mini' import * as Provider from '../Provider.js' const defaultBaseUrl = 'https://trade-api.gateway.uniswap.org/v1' const tempoMainnetChainId = 4217 const permit2 = '0x000000000022d473030f116ddee9f6b43ac78ba3' const universalRouter = '0x1febb76be10aaf3a1402f04e8e835f2c382f7914' const universalRouterVersion = '2.0' const continuationTtl = 30 const approveAbi = parseAbi(['function approve(address spender, uint256 amount) returns (bool)']) const universalRouterAbi = parseAbi([ 'function execute(bytes commands, bytes[] inputs)', 'function execute(bytes commands, bytes[] inputs, uint256 deadline)', ]) const bytesAndInputs = parseAbiParameters('bytes, bytes[]') const allowedRouterCommands = new Set([0x00, 0x01, 0x02, 0x04, 0x05, 0x08, 0x09, 0x0a, 0x0e]) const allowedV4Actions = new Set([0x06, 0x07, 0x08, 0x09, 0x0b, 0x0c, 0x0e, 0x0f]) const executeSubPlan = 0x21 const v4Swap = 0x10 const maximumSubPlanDepth = 8 const maxUint48 = 2n ** 48n - 1n const maxUint160 = 2n ** 160n - 1n const maxUint256 = 2n ** 256n - 1n const schema = { Address: z.templateLiteral(['0x', z.string()]).check(z.regex(/^0x[\da-fA-F]{40}$/)), Amount: z.string().check(z.regex(/^\d+$/)), Hex: z.templateLiteral(['0x', z.string()]).check(z.regex(/^0x[\da-fA-F]+$/)), Quantity: z.union([ z.string().check(z.regex(/^\d+$/)), z.templateLiteral(['0x', z.string()]).check(z.regex(/^0x[\da-fA-F]+$/)), ]), Uint48: uint(maxUint48), Uint160: uint(maxUint160), Uint256: uint(maxUint256), } const Transaction = z.object({ chainId: z.number().check(z.int(), z.positive()), data: schema.Hex, from: schema.Address, gasLimit: z.optional(schema.Amount), to: schema.Address, value: schema.Quantity, }) const PermitDetailsTypes = z.tuple([ z.strictObject({ name: z.literal('token'), type: z.literal('address') }), z.strictObject({ name: z.literal('amount'), type: z.literal('uint160') }), z.strictObject({ name: z.literal('expiration'), type: z.literal('uint48') }), z.strictObject({ name: z.literal('nonce'), type: z.literal('uint48') }), ]) const PermitSingleTypes = z.tuple([ z.strictObject({ name: z.literal('details'), type: z.literal('PermitDetails') }), z.strictObject({ name: z.literal('spender'), type: z.literal('address') }), z.strictObject({ name: z.literal('sigDeadline'), type: z.literal('uint256') }), ]) const Permit = z.strictObject({ domain: z.strictObject({ chainId: z.number().check(z.int(), z.positive()), name: z.literal('Permit2'), verifyingContract: schema.Address, }), types: z.strictObject({ PermitDetails: PermitDetailsTypes, PermitSingle: PermitSingleTypes, }), values: z.strictObject({ details: z.strictObject({ amount: schema.Uint160, expiration: schema.Uint48, nonce: schema.Uint48, token: schema.Address, }), sigDeadline: schema.Uint256, spender: schema.Address, }), }) const ClassicQuote = z.looseObject({ chainId: z.number().check(z.int(), z.positive()), gasUseEstimate: schema.Amount, input: z.object({ amount: schema.Amount, maximumAmount: z.optional(schema.Amount), token: schema.Address, }), output: z.object({ amount: schema.Amount, minimumAmount: z.optional(schema.Amount), recipient: schema.Address, token: schema.Address, }), priceImpact: z.optional(z.number().check(z.nonnegative())), swapper: schema.Address, tradeType: z.enum(['EXACT_INPUT', 'EXACT_OUTPUT']), }) const QuoteResponse = z.object({ permitData: z.nullable(Permit), quote: ClassicQuote, routing: z.literal('CLASSIC'), }) const ApprovalResponse = z.object({ approval: z.nullable(Transaction), cancel: z.nullable(Transaction), }) const SwapResponse = z.object({ swap: Transaction }) const Continuation = z.object({ account: schema.Address, chainId: z.number().check(z.int(), z.positive()), expiresAt: z.number().check(z.int(), z.positive()), permitData: Permit, quote: ClassicQuote, }) /** Creates a Uniswap Trading API provider restricted to executable AMM routes. */ export function uniswap(options: uniswap.Options) { if (!options.apiKey.trim()) throw new Provider.ProviderConfigurationError() return Provider.from({ id: 'uniswap', async finalizeQuote(input, signal) { if (input.chainId !== tempoMainnetChainId) throw new Provider.RouteUnavailableError() const continuation = decodeContinuation(input.continuation, { account: input.account, chainId: input.chainId, secret: options.apiKey, }) const response = await requestJson(new URL(`${defaultBaseUrl}/swap`), { apiKey: options.apiKey, body: { permitData: continuation.permitData, quote: continuation.quote, refreshGasPrice: true, signature: input.signature, }, operation: 'swap', signal, }) const parsed = SwapResponse.safeParse(response) if (!parsed.success) throw new Provider.ProviderPayloadError({ code: Error.getCode({ path: parsed.error.issues[0]?.path, prefix: 'swap.response' }), }) Error.boundary({ code: 'swap.transaction', fn: () => assertSwapTransaction(parsed.data.swap, { account: input.account, chainId: input.chainId, }), }) return { calls: [call(parsed.data.swap)], execution: execution(continuation.quote), } }, async quote(input, signal) { if (input.chainId !== tempoMainnetChainId) throw new Provider.RouteUnavailableError() const response = await requestJson(new URL(`${defaultBaseUrl}/quote`), { apiKey: options.apiKey, body: { amount: input.amount, permitAmount: 'EXACT', protocols: ['V2', 'V3', 'V4'], slippageTolerance: input.slippageBps / 100, swapper: input.account, tokenIn: input.sourceToken, tokenInChainId: input.chainId, tokenOut: input.destinationToken, tokenOutChainId: input.chainId, type: input.mode === 'exactSource' ? 'EXACT_INPUT' : 'EXACT_OUTPUT', }, operation: 'quote', signal, }) const parsed = QuoteResponse.safeParse(response) if (!parsed.success) throw new Provider.ProviderPayloadError({ code: Error.getCode({ path: parsed.error.issues[0]?.path, prefix: 'quote.response' }), }) const quote = parsed.data.quote Error.boundary({ code: 'quote.invariants', fn: () => assertQuote(quote, input) }) const approvalAmount = input.mode === 'exactSource' ? quote.input.amount : quote.input.maximumAmount! const approvalResponse = await requestJson(new URL(`${defaultBaseUrl}/check_approval`), { apiKey: options.apiKey, body: { amount: approvalAmount, chainId: input.chainId, // Tempo estimates returned approval calls locally, so upstream gas-fee estimation is unnecessary. includeGasInfo: false, token: input.sourceToken, walletAddress: input.account, }, operation: 'check_approval', signal, }) const approval = validateApprovalResponse(approvalResponse) const calls = [ ...(approval.cancel ? [ Error.boundary({ code: 'check_approval.cancel', fn: () => approvalCall(approval.cancel!, { account: input.account, amount: { exact: 0n }, chainId: input.chainId, token: input.sourceToken, }), }), ] : []), ...(approval.approval ? [ Error.boundary({ code: 'check_approval.approval', fn: () => approvalCall(approval.approval!, { account: input.account, amount: { minimum: BigInt(approvalAmount) }, chainId: input.chainId, token: input.sourceToken, }), }), ] : []), ] const result = { destinationAmount: quote.output.amount, ...(quote.output.minimumAmount ? { minimumDestinationAmount: quote.output.minimumAmount } : {}), ...(quote.input.maximumAmount ? { maximumSourceAmount: quote.input.maximumAmount } : {}), sourceAmount: quote.input.amount, } as const if (calls.length > 0) return { ...result, approval: { calls }, gasUnits: callsGas(approval), status: 'approvalRequired', } const permitData = parsed.data.permitData if (permitData) Error.boundary({ code: 'quote.permit', fn: () => assertPermit(permitData, { input, requiredAmount: BigInt(approvalAmount), }), }) if (permitData) return { ...result, continuation: encodeContinuation( { account: input.account, chainId: input.chainId, expiresAt: continuationExpiry(permitData), permitData, quote, }, { secret: options.apiKey }, ), gasUnits: quote.gasUseEstimate, status: 'signatureRequired', typedData: typedData(permitData), } const swapResponse = await requestJson(new URL(`${defaultBaseUrl}/swap`), { apiKey: options.apiKey, body: { quote, refreshGasPrice: true }, operation: 'swap', signal, }) const swap = SwapResponse.safeParse(swapResponse) if (!swap.success) throw new Provider.ProviderPayloadError({ code: Error.getCode({ path: swap.error.issues[0]?.path, prefix: 'swap.response' }), }) Error.boundary({ code: 'swap.transaction', fn: () => assertSwapTransaction(swap.data.swap, { account: input.account, chainId: input.chainId, }), }) return { ...result, gasUnits: quote.gasUseEstimate, status: 'ready', transaction: { calls: [call(swap.data.swap)] }, } }, name: 'Uniswap', }) } export declare namespace uniswap { /** Uniswap Trading API provider options. */ type Options = { /** Uniswap Trading API key. */ apiKey: string } } /** Validates a Uniswap approval response before its calls are returned to a wallet. */ export function validateApprovalResponse(response: unknown) { const parsed = ApprovalResponse.safeParse(response) if (!parsed.success) throw new Provider.ProviderPayloadError({ code: Error.getCode({ path: parsed.error.issues[0]?.path, prefix: 'check_approval.response', }), }) return parsed.data } namespace Error { export function boundary(options: boundary.Options): result { try { return options.fn() } catch (cause) { if (cause instanceof Provider.ProviderPayloadError && !cause.code) throw new Provider.ProviderPayloadError({ code: options.code }) throw cause } } export declare namespace boundary { /** Bounded validation context for one provider payload boundary. */ type Options = { /** Stable validation code safe for operational reporting. */ code: string /** Provider payload validation to execute. */ fn: () => result } } export function getCode(options: getCode.Options) { if (!options.path || options.path.length === 0) return options.prefix return `${options.prefix}.${options.path.map((part) => (typeof part === 'number' ? '*' : String(part))).join('.')}` } export declare namespace getCode { /** Schema issue context used to build a bounded validation code. */ type Options = { /** First invalid schema path, when Zod returned one. */ path: readonly PropertyKey[] | undefined /** Stable provider operation and response prefix. */ prefix: string } } } function uint(maximum: bigint) { return z.string().check( z.regex(/^\d+$/), z.refine((value) => BigInt(value) <= maximum), ) } /** Provider-neutral EIP-712 payload accepted by Privy and common wallet clients. */ type TypedData = { /** EIP-712 signing domain. */ domain: z.output['domain'] /** Permit message values. */ message: z.output['values'] /** Root EIP-712 type. */ primaryType: 'PermitSingle' /** EIP-712 type definitions. */ types: z.output['types'] } function assertQuote(quote: z.output, input: Provider.QuoteInput) { const denominator = 10_000n const maximum = quote.input.maximumAmount ? BigInt(quote.input.maximumAmount) : undefined const slippage = BigInt(input.slippageBps) const source = BigInt(quote.input.amount) if (input.mode === 'exactSource') { validateExactSourceMaximum({ amount: input.amount, maximumAmount: quote.input.maximumAmount }) validateExactSourceMinimum({ amount: quote.output.amount, minimumAmount: quote.output.minimumAmount, slippageBps: input.slippageBps, }) } if ( quote.chainId !== input.chainId || quote.input.token.toLowerCase() !== input.sourceToken.toLowerCase() || quote.output.token.toLowerCase() !== input.destinationToken.toLowerCase() || quote.output.recipient.toLowerCase() !== input.account.toLowerCase() || quote.swapper.toLowerCase() !== input.account.toLowerCase() || quote.tradeType !== (input.mode === 'exactSource' ? 'EXACT_INPUT' : 'EXACT_OUTPUT') || (input.mode === 'exactSource' && quote.input.amount !== input.amount) || (input.mode === 'exactDestination' && (quote.output.amount !== input.amount || maximum === undefined || maximum < source || maximum > (source * (denominator + slippage) + denominator - 1n) / denominator)) ) throw new Provider.ProviderPayloadError() } /** Accepts exact-source minimums that preserve the requested slippage after integer rounding. */ export function validateExactSourceMinimum(options: validateExactSourceMinimum.Options): void { if (!options.minimumAmount) throw new Provider.ProviderPayloadError() const amount = BigInt(options.amount) const denominator = 10_000n const minimumAmount = BigInt(options.minimumAmount) const slippage = BigInt(options.slippageBps) if (minimumAmount < (amount * (denominator - slippage)) / denominator || minimumAmount > amount) throw new Provider.ProviderPayloadError() } export declare namespace validateExactSourceMinimum { /** Upstream destination amounts and requested slippage to compare. */ type Options = { /** Quoted destination amount in base units. */ amount: string /** Minimum destination amount in base units. */ minimumAmount?: string | undefined /** Requested slippage in basis points. */ slippageBps: number } } /** Rejects exact-source quotes that expand the wallet authorization amount. */ export function validateExactSourceMaximum(options: validateExactSourceMaximum.Options): void { if (options.maximumAmount !== undefined && options.maximumAmount !== options.amount) throw new Provider.ProviderPayloadError() } export declare namespace validateExactSourceMaximum { /** Requested and upstream source bounds to compare. */ type Options = { /** Exact source amount requested by the caller. */ amount: string /** Optional maximum source amount returned upstream. */ maximumAmount?: string | undefined } } function execution(quote: z.output): Provider.Execution { return { destinationAmount: quote.output.amount, destinationToken: quote.output.token, gasUnits: quote.gasUseEstimate, ...(quote.input.maximumAmount ? { maximumSourceAmount: quote.input.maximumAmount } : {}), ...(quote.output.minimumAmount ? { minimumDestinationAmount: quote.output.minimumAmount } : {}), mode: quote.tradeType === 'EXACT_INPUT' ? 'exactSource' : 'exactDestination', sourceAmount: quote.input.amount, sourceToken: quote.input.token, } } function assertSwapTransaction( transaction: z.output, options: { account: string; chainId: number }, ) { if ( transaction.chainId !== options.chainId || transaction.from.toLowerCase() !== options.account.toLowerCase() || transaction.to.toLowerCase() !== universalRouter || BigInt(transaction.value) !== 0n ) throw new Provider.ProviderPayloadError() validateRouterCalldata(transaction.data as Hex.Hex) } /** Rejects Universal Router calldata with capabilities outside token swaps and payments. */ export function validateRouterCalldata(data: Hex.Hex): void { try { const decoded = decodeFunctionData({ abi: universalRouterAbi, data }) const [commands, inputs] = decoded.args validateRouterPlan(commands, inputs, 0) } catch (cause) { if (cause instanceof Provider.ProviderPayloadError) throw cause throw new Provider.ProviderPayloadError() } } function validateRouterPlan(commands: Hex.Hex, inputs: readonly Hex.Hex[], depth: number): void { if (depth > maximumSubPlanDepth) throw new Provider.ProviderPayloadError() const bytes = Hex.toBytes(commands) if (bytes.length !== inputs.length) throw new Provider.ProviderPayloadError() for (const [index, byte] of bytes.entries()) { if ((byte & 0x40) !== 0) throw new Provider.ProviderPayloadError() const command = byte & 0x3f if (allowedRouterCommands.has(command)) continue if (command === executeSubPlan) { const [nestedCommands, nestedInputs] = decodeAbiParameters(bytesAndInputs, inputs[index]!) validateRouterPlan(nestedCommands, nestedInputs, depth + 1) continue } if (command === v4Swap) { validateV4Plan(inputs[index]!) continue } throw new Provider.ProviderPayloadError() } } function validateV4Plan(input: Hex.Hex): void { const [actions, parameters] = decodeAbiParameters(bytesAndInputs, input) const bytes = Hex.toBytes(actions) if (bytes.length !== parameters.length || bytes.some((action) => !allowedV4Actions.has(action))) throw new Provider.ProviderPayloadError() } function approvalCall( transaction: z.output, options: { account: string amount: { exact: bigint } | { minimum: bigint } chainId: number token: string }, ): Provider.Call { if ( transaction.chainId !== options.chainId || transaction.from.toLowerCase() !== options.account.toLowerCase() || transaction.to.toLowerCase() !== options.token.toLowerCase() || BigInt(transaction.value) !== 0n ) throw new Provider.ProviderPayloadError() try { const decoded = decodeFunctionData({ abi: approveAbi, data: transaction.data as Hex.Hex }) if ( decoded.functionName !== 'approve' || decoded.args[0].toLowerCase() !== permit2 || ('exact' in options.amount ? decoded.args[1] !== options.amount.exact : decoded.args[1] < options.amount.minimum) ) throw new Provider.ProviderPayloadError() } catch (cause) { if (cause instanceof Provider.ProviderPayloadError) throw cause throw new Provider.ProviderPayloadError() } return call(transaction) } function callsGas(approval: z.output): string { return ( BigInt(approval.cancel?.gasLimit ?? 0) + BigInt(approval.approval?.gasLimit ?? 0) ).toString() } function assertPermit( permit: z.output, options: { input: Provider.QuoteInput; requiredAmount: bigint }, ) { const { input, requiredAmount } = options if ( permit.domain.chainId !== input.chainId || permit.domain.verifyingContract.toLowerCase() !== permit2 || permit.values.details.token.toLowerCase() !== input.sourceToken.toLowerCase() || permit.values.spender.toLowerCase() !== universalRouter || BigInt(permit.values.details.amount) !== requiredAmount || BigInt(permit.values.details.expiration) <= Math.floor(Date.now() / 1_000) || BigInt(permit.values.sigDeadline) <= Math.floor(Date.now() / 1_000) ) throw new Provider.ProviderPayloadError() } function continuationExpiry(permit: z.output): number { const now = Math.floor(Date.now() / 1_000) return Number( [ BigInt(now + continuationTtl), BigInt(permit.values.details.expiration), BigInt(permit.values.sigDeadline), ].reduce((minimum, value) => (value < minimum ? value : minimum)), ) } function typedData(permit: z.output): TypedData { return { domain: permit.domain, message: permit.values, primaryType: 'PermitSingle', types: permit.types, } } function call(transaction: z.output): Provider.Call { return { data: transaction.data as Hex.Hex, to: transaction.to as `0x${string}`, value: Hex.fromNumber(BigInt(transaction.value)), } } function encodeContinuation(value: z.output, options: { secret: string }) { const body = Base64.fromString(JSON.stringify(value), { pad: false, url: true }) const signature = Hash.hmac256(Hex.fromString(options.secret), Hex.fromString(body)).slice(2) return `${body}.${signature}` } function decodeContinuation( value: string, options: { account: string; chainId: number; secret: string }, ) { try { const [body, signature, extra] = value.split('.') if (!body || !signature || extra) throw new Provider.ProviderContinuationError() const expected = Hash.hmac256(Hex.fromString(options.secret), Hex.fromString(body)).slice(2) if (!ContinuationSignature.timingSafeEqual(expected, signature)) throw new Provider.ProviderContinuationError() const parsed = Continuation.safeParse(JSON.parse(Base64.toString(body))) if ( parsed.success && parsed.data.account.toLowerCase() === options.account.toLowerCase() && parsed.data.chainId === options.chainId && parsed.data.quote.chainId === options.chainId && parsed.data.quote.swapper.toLowerCase() === options.account.toLowerCase() && parsed.data.expiresAt >= Math.floor(Date.now() / 1_000) ) return parsed.data } catch {} throw new Provider.ProviderContinuationError() } namespace ContinuationSignature { export function timingSafeEqual(left: string, right: string): boolean { if (left.length !== right.length) return false let mismatch = 0 for (let index = 0; index < left.length; index++) mismatch |= left.charCodeAt(index) ^ right.charCodeAt(index) return mismatch === 0 } } async function requestJson( url: URL, options: { apiKey: string; body: unknown; operation: string; signal: AbortSignal }, ) { const response = await fetch(url, { body: JSON.stringify(options.body), headers: { accept: 'application/json', 'content-type': 'application/json', 'x-api-key': options.apiKey, 'x-universal-router-version': universalRouterVersion, }, method: 'POST', signal: options.signal, }).catch((cause) => { if (cause instanceof TypeError) throw new Provider.ProviderNetworkError({ cause, operation: options.operation }) throw cause }) const body = await response.json().catch(() => undefined) if (response.status === 404) throw new Provider.RouteUnavailableError() if (!response.ok) throw new Provider.ProviderResponseError({ operation: options.operation, status: response.status, }) return body }