import {z} from 'zod' export type DistributionFeeEntry = {percentage: number, receiver: string}; export type QuoteBridgesEnum = string; export type QuoteExchangesEnum = string; export type GetApiV1QuoteArgs = {fromChain: string, toChain: string, fromToken: string, toToken: string, fromAddress: string, toAddress?: string | undefined, fromAmount: string, order?: ('FASTEST' | 'CHEAPEST') | undefined, slippage?: number | undefined, slippageScope?: ('step' | 'route') | undefined, integrator?: string | undefined, fee?: number | undefined, distributionFees?: Array | undefined, referrer?: string | undefined, allowBridges?: Array | undefined, allowExchanges?: Array | undefined, denyBridges?: Array | undefined, denyExchanges?: Array | undefined, denyPools?: Array | undefined, preferBridges?: Array | undefined, preferExchanges?: Array | undefined, allowDestinationCall?: boolean | undefined, fromAmountForGas?: string | undefined, maxPriceImpact?: number | undefined, swapStepTimingStrategies?: Array | undefined, routeTimingStrategies?: Array | undefined, skipSimulation?: boolean | undefined, preset?: string | undefined, svmMaxSupportedTransactionVersion?: (number | null) | undefined, svmPriorityFeeLevel?: ('NORMAL' | 'FAST' | 'ULTRA') | undefined, 'x-lifi-api-key'?: string | undefined}; export type GetApiV1QuoteOptions = {baseUrl?: string | undefined, fetch?: typeof fetch | undefined}; export const token = z.object({address: z.string(), decimals: z.number(), symbol: z.string(), chainId: z.number(), coinKey: z.string().optional(), name: z.string(), logoURI: z.string().optional(), priceUSD: z.string().optional()}); export const action = z.object({fromChainId: z.number(), fromAmount: z.string(), fromToken: token, toChainId: z.number(), toToken: token, slippage: z.number().optional(), fromAddress: z.string().optional(), toAddress: z.string().optional()}); export const feeCost = z.object({name: z.string(), description: z.string().optional(), percentage: z.string(), token: token, amount: z.string().optional(), amountUSD: z.string(), included: z.boolean(), feeSplit: z.object({lifiFee: z.string().optional(), integratorFee: z.string().optional(), intermediaryFee: z.string().optional(), recipients: z.array(z.object({name: z.string(), fee: z.string(), type: z.string().optional(), walletAddress: z.string().optional()})).optional()}).optional()}); export const gasCost = z.object({type: z.enum(["SUM", "APPROVE", "SEND", "FEE"]), price: z.string().optional(), estimate: z.string().optional(), limit: z.string().optional(), amount: z.string(), amountUSD: z.string().optional(), token: token}); export const estimate = z.object({tool: z.string(), fromAmount: z.string(), fromAmountUSD: z.string().optional(), toAmount: z.string(), toAmountMin: z.string(), toAmountUSD: z.string().optional(), approvalAddress: z.string(), skipApproval: z.boolean().optional(), feeCosts: z.array(feeCost).optional(), gasCosts: z.array(gasCost).optional(), executionDuration: z.number(), data: z.object({bid: z.object({user: z.string().optional(), router: z.string().optional(), initiator: z.string().optional(), sendingChainId: z.number().optional(), sendingAssetId: z.string().optional(), amount: z.string().optional(), receivingChainId: z.number().optional(), receivingAssetId: z.string().optional(), amountReceived: z.string().optional(), receivingAddress: z.string().optional(), transactionId: z.string().optional(), expiry: z.number().optional(), callDataHash: z.string().optional(), callTo: z.string().optional(), encryptedCallData: z.string().optional(), sendingChainTxManagerAddress: z.string().optional(), receivingChainTxManagerAddress: z.string().optional(), bidExpiry: z.number().optional()}).optional(), bidSignature: z.string().optional(), gasFeeInReceivingToken: z.string().optional(), totalFee: z.string().optional(), metaTxRelayerFee: z.string().optional(), routerFee: z.string().optional()}).optional()}); export const routeSlippageCommitment = z.object({scope: z.literal("route"), slippage: z.string(), referenceToAmount: z.string(), minimumToAmount: z.string(), toChainId: z.number().int(), toTokenAddress: z.string(), toAddress: z.string().optional(), settlement: z.enum(["atomic", "conditional"]), checkpoints: z.array(z.object({stepId: z.string(), chainId: z.number().int(), tokenAddress: z.string(), minimumAmount: z.string()}))}); export const includedStep = z.object({id: z.string(), type: z.enum(["swap", "cross", "lifi", "protocol"]), tool: z.string(), toolDetails: z.object({key: z.string().optional(), name: z.string().optional(), logoURI: z.string().optional()}), action: action, estimate: estimate}); export const step = z.object({id: z.string(), type: z.enum(["swap", "cross", "lifi", "protocol"]), tool: z.string(), toolDetails: z.object({key: z.string().optional(), name: z.string().optional(), logoURI: z.string().optional()}).optional(), action: action, estimate: estimate.optional(), routeSlippage: routeSlippageCommitment.optional(), integrator: z.string().optional(), includedSteps: z.array(includedStep).optional(), referrer: z.string().optional(), execution: z.unknown(), transactionRequest: z.object({svmTransactionVersion: z.union([z.literal(0), z.literal(1)]).optional()}).optional()}); export const validateResponse = async ( schema: { readonly '~standard': { readonly validate: (value: unknown) => ({ readonly value: Output; readonly issues?: undefined } | { readonly issues: ReadonlyArray<{ readonly message: string; readonly path?: ReadonlyArray | undefined }> }) | Promise<{ readonly value: Output; readonly issues?: undefined } | { readonly issues: ReadonlyArray<{ readonly message: string; readonly path?: ReadonlyArray | undefined }> }> } }, value: unknown ): Promise => { const result = await schema['~standard'].validate(value) if (result.issues !== undefined) { const detail = result.issues .map(({ message, path }) => path?.length ? `${path.map((segment) => String(typeof segment === 'object' ? segment.key : segment)).join('.')}: ${message}` : message ) .join('; ') throw new Error(`Response does not match the schema: ${detail}`, { cause: { issues: result.issues } }) } return result.value }; /** * Get a quote for a token transfer * * GET /v1/quote */ export const getApiV1Quote = async (args: GetApiV1QuoteArgs, options?: GetApiV1QuoteOptions) => { const {baseUrl = "https://li.quest", fetch: fetchFn = fetch} = options ?? {} const query = new URLSearchParams() const headers = new Headers() headers.set('accept', "application/json") if (args.fromChain != null) query.append("fromChain", String(args.fromChain)) if (args.toChain != null) query.append("toChain", String(args.toChain)) if (args.fromToken != null) query.append("fromToken", String(args.fromToken)) if (args.toToken != null) query.append("toToken", String(args.toToken)) if (args.fromAddress != null) query.append("fromAddress", String(args.fromAddress)) if (args.toAddress != null) query.append("toAddress", String(args.toAddress)) if (args.fromAmount != null) query.append("fromAmount", String(args.fromAmount)) if (args.order != null) query.append("order", String(args.order)) if (args.slippage != null) query.append("slippage", String(args.slippage)) if (args.slippageScope != null) query.append("slippageScope", String(args.slippageScope)) if (args.integrator != null) query.append("integrator", String(args.integrator)) if (args.fee != null) query.append("fee", String(args.fee)) for (const item of args.distributionFees ?? []) query.append("distributionFees", String(item)) if (args.referrer != null) query.append("referrer", String(args.referrer)) for (const item of args.allowBridges ?? []) query.append("allowBridges", String(item)) for (const item of args.allowExchanges ?? []) query.append("allowExchanges", String(item)) for (const item of args.denyBridges ?? []) query.append("denyBridges", String(item)) for (const item of args.denyExchanges ?? []) query.append("denyExchanges", String(item)) for (const item of args.denyPools ?? []) query.append("denyPools", String(item)) for (const item of args.preferBridges ?? []) query.append("preferBridges", String(item)) for (const item of args.preferExchanges ?? []) query.append("preferExchanges", String(item)) if (args.allowDestinationCall != null) query.append("allowDestinationCall", String(args.allowDestinationCall)) if (args.fromAmountForGas != null) query.append("fromAmountForGas", String(args.fromAmountForGas)) if (args.maxPriceImpact != null) query.append("maxPriceImpact", String(args.maxPriceImpact)) for (const item of args.swapStepTimingStrategies ?? []) query.append("swapStepTimingStrategies", String(item)) for (const item of args.routeTimingStrategies ?? []) query.append("routeTimingStrategies", String(item)) if (args.skipSimulation != null) query.append("skipSimulation", String(args.skipSimulation)) if (args.preset != null) query.append("preset", String(args.preset)) if (args.svmMaxSupportedTransactionVersion != null) query.append("svmMaxSupportedTransactionVersion", String(args.svmMaxSupportedTransactionVersion)) if (args.svmPriorityFeeLevel != null) query.append("svmPriorityFeeLevel", String(args.svmPriorityFeeLevel)) if (args['x-lifi-api-key'] != null) headers.set("x-lifi-api-key", String(args['x-lifi-api-key'])) const url = `${baseUrl.replace(/\/+$/, '')}/v1/quote${query.size > 0 ? `?${query}` : ''}` const res = await fetchFn(url, {method: "GET", headers}) if (!res.ok) throw new Error(`Request failed with status ${res.status}`, {cause: {status: res.status, body: await res.text()}}) return validateResponse(step, await res.json()) };