import Mpc from '../../../mpc' import { waitForEvmTxConfirmation } from '../../../internal/waitForEvmTxConfirmation' import { normalizeChainToNetwork } from '../lifi' import { ZeroExOptions, ZeroExPriceRequest, ZeroExPriceResponse, ZeroExQuoteRequest, ZeroExQuoteResponse, ZeroExSourcesResponse, ZeroXTradeAssetOptions, ZeroXTradeAssetParams, ZeroXTradeAssetResult, } from '../../../shared/types/zero-x' const LOG_PREFIX = '[ZeroX]' export interface ZeroXOptions extends ZeroXTradeAssetOptions { zeroXApiKey?: string } export interface IZeroX { getQuote( args: ZeroExQuoteRequest, options?: ZeroExOptions, ): Promise getSources( chainId: string, options?: ZeroExOptions, ): Promise getPrice( args: ZeroExPriceRequest, options?: ZeroExOptions, ): Promise tradeAsset( params: ZeroXTradeAssetParams, options?: ZeroXTradeAssetOptions & ZeroExOptions, ): Promise } function mergeZeroXOptions( instance: (ZeroXTradeAssetOptions & ZeroExOptions) | undefined, perCall: (ZeroXTradeAssetOptions & ZeroExOptions) | undefined, ): ZeroXTradeAssetOptions & ZeroExOptions { return { ...instance, ...perCall } } export default class ZeroX implements IZeroX { private mpc: Mpc private readonly defaults: ZeroXTradeAssetOptions & ZeroExOptions constructor({ mpc, ...defaults }: { mpc: Mpc } & ZeroXOptions) { this.mpc = mpc this.defaults = defaults } public async getQuote( args: ZeroExQuoteRequest, options?: ZeroExOptions, ): Promise { const merged = { ...this.defaults, ...options } const key = merged.zeroXApiKey return this.mpc?.getSwapsQuoteV2( args, key != null && key !== '' ? { zeroXApiKey: key } : undefined, ) } public async getSources( chainId: string, options?: ZeroExOptions, ): Promise { const merged = { ...this.defaults, ...options } const key = merged.zeroXApiKey return this.mpc?.getSwapsSourcesV2( { chainId }, key != null && key !== '' ? { zeroXApiKey: key } : undefined, ) } public async getPrice( args: ZeroExPriceRequest, options?: ZeroExOptions, ): Promise { const merged = { ...this.defaults, ...options } const key = merged.zeroXApiKey return this.mpc?.getSwapsPrice( args, key != null && key !== '' ? { zeroXApiKey: key } : undefined, ) } public async tradeAsset( params: ZeroXTradeAssetParams, options?: ZeroXTradeAssetOptions & ZeroExOptions, ): Promise { const o = mergeZeroXOptions(this.defaults, options) const signAndSend = o.signAndSendTransaction if (!signAndSend) { throw new Error(`${LOG_PREFIX} tradeAsset requires signAndSendTransaction`) } if (!o.waitForConfirmation && !o.evmRequestFn) { throw new Error( `${LOG_PREFIX} tradeAsset requires waitForConfirmation (instance default or per-call option), or evmRequestFn fallback.`, ) } const apiOpts = { zeroXApiKey: o.zeroXApiKey ?? params.zeroXApiKey } params.onProgress?.('fetching_quote') const quoteReq: ZeroExQuoteRequest = { chainId: params.chainId, buyToken: params.buyToken, sellToken: params.sellToken, sellAmount: params.sellAmount, txOrigin: params.fromAddress, swapFeeRecipient: params.swapFeeRecipient, swapFeeBps: params.swapFeeBps, swapFeeToken: params.swapFeeToken, tradeSurplusRecipient: params.tradeSurplusRecipient, gasPrice: params.gasPrice, slippageBps: params.slippageBps, excludedSources: params.excludedSources, sellEntireBalance: params.sellEntireBalance, } const quote = await this.mpc.getSwapsQuoteV2(quoteReq, apiOpts) const err = quote.error?.trim() if (err) { throw new Error(`${LOG_PREFIX} Quote error: ${err}`) } const raw = quote.data?.rawResponse if (!raw) { const msg = 'Quote response missing data.rawResponse' params.onProgress?.('failed', { errorMessage: msg }) throw new Error(`${LOG_PREFIX} ${msg}`) } const transaction = raw.transaction const to = transaction !== null && transaction !== undefined && typeof transaction === 'object' && 'to' in transaction ? (transaction as { to?: unknown }).to : undefined if ( typeof to !== 'string' || to.trim() === '' || transaction === null || transaction === undefined || typeof transaction !== 'object' ) { const msg = 'Quote response missing valid transaction (expected object with non-empty string "to")' params.onProgress?.('failed', { errorMessage: msg }) throw new Error(`${LOG_PREFIX} ${msg}`) } const tx = transaction const network = normalizeChainToNetwork(params.chainId) params.onProgress?.('signing', { buyAmount: quote.data?.rawResponse?.buyAmount, sellAmount: quote.data?.rawResponse?.sellAmount, transaction: tx, }) let txHash: string try { txHash = await signAndSend(tx, network) } catch (e) { const msg = e instanceof Error ? e.message : String(e) params.onProgress?.('failed', { errorMessage: msg }) throw e } if (typeof txHash !== 'string' || txHash.trim() === '') { const msg = 'signAndSendTransaction returned empty or invalid transaction hash' params.onProgress?.('failed', { errorMessage: msg }) throw new Error(`${LOG_PREFIX} ${msg}`) } params.onProgress?.('submitted', { txHash }) if (o.waitForConfirmation) { params.onProgress?.('confirming', { txHash }) const waiterResult = await o.waitForConfirmation(txHash, network) const ok = waiterResult === true if (!ok) { const msg = `${LOG_PREFIX} on-chain confirmation did not complete (waitForConfirmation did not return true) for ${txHash} on ${network}` params.onProgress?.('failed', { errorMessage: msg, txHash }) throw new Error(msg) } params.onProgress?.('confirmed', { txHash }) } else if (o.evmRequestFn) { params.onProgress?.('confirming', { txHash }) await waitForEvmTxConfirmation(txHash, network, o.evmRequestFn, { pollIntervalMs: o.evmPollerOptions?.pollIntervalMs, timeoutMs: o.evmPollerOptions?.timeoutMs, onTimeout: 'throw', }) params.onProgress?.('confirmed', { txHash }) } return { hashes: [txHash] } } }