UNPKG

@lifi/sdk

Version:

LI.FI Any-to-Any Cross-Chain-Swap SDK

519 lines 26.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.EVMStepExecutor = void 0; const actions_1 = require("viem/actions"); const utils_1 = require("viem/utils"); const config_js_1 = require("../../config.js"); const constants_js_1 = require("../../constants.js"); const constants_js_2 = require("../../errors/constants.js"); const errors_js_1 = require("../../errors/errors.js"); const api_js_1 = require("../../services/api.js"); const convertQuoteToRoute_js_1 = require("../../utils/convertQuoteToRoute.js"); const isZeroAddress_js_1 = require("../../utils/isZeroAddress.js"); const BaseStepExecutor_js_1 = require("../BaseStepExecutor.js"); const checkBalance_js_1 = require("../checkBalance.js"); const stepComparison_js_1 = require("../stepComparison.js"); const waitForDestinationChainTransaction_js_1 = require("../waitForDestinationChainTransaction.js"); const checkAllowance_js_1 = require("./checkAllowance.js"); const getActionWithFallback_js_1 = require("./getActionWithFallback.js"); const isBatchingSupported_js_1 = require("./isBatchingSupported.js"); const parseEVMErrors_js_1 = require("./parseEVMErrors.js"); const encodeNativePermitData_js_1 = require("./permits/encodeNativePermitData.js"); const encodePermit2Data_js_1 = require("./permits/encodePermit2Data.js"); const isNativePermitValid_js_1 = require("./permits/isNativePermitValid.js"); const signPermit2Message_js_1 = require("./permits/signPermit2Message.js"); const switchChain_js_1 = require("./switchChain.js"); const typeguards_js_1 = require("./typeguards.js"); const utils_js_1 = require("./utils.js"); const waitForBatchTransactionReceipt_js_1 = require("./waitForBatchTransactionReceipt.js"); const waitForRelayedTransactionReceipt_js_1 = require("./waitForRelayedTransactionReceipt.js"); const waitForTransactionReceipt_js_1 = require("./waitForTransactionReceipt.js"); class EVMStepExecutor extends BaseStepExecutor_js_1.BaseStepExecutor { client; constructor(options) { super(options); this.client = options.client; } checkClient = async (step, process, targetChainId) => { const updatedClient = await (0, switchChain_js_1.switchChain)(this.client, this.statusManager, step, process, targetChainId ?? step.action.fromChainId, this.allowUserInteraction, this.executionOptions); if (updatedClient) { this.client = updatedClient; } let accountAddress = this.client.account?.address; if (!accountAddress) { const accountAddresses = (await (0, utils_1.getAction)(this.client, actions_1.getAddresses, 'getAddresses')(undefined)); accountAddress = accountAddresses?.[0]; } if (accountAddress?.toLowerCase() !== step.action.fromAddress?.toLowerCase()) { const errorMessage = 'The wallet address that requested the quote does not match the wallet address attempting to sign the transaction.'; this.statusManager.updateProcess(step, process.type, 'FAILED', { error: { code: constants_js_2.LiFiErrorCode.WalletChangedDuringExecution, message: errorMessage, }, }); this.statusManager.updateExecution(step, 'FAILED'); throw await (0, parseEVMErrors_js_1.parseEVMErrors)(new errors_js_1.TransactionError(constants_js_2.LiFiErrorCode.WalletChangedDuringExecution, errorMessage), step, process); } return updatedClient; }; waitForTransaction = async ({ step, process, fromChain, toChain, isBridgeExecution, }) => { const updateProcessWithReceipt = (transactionReceipt) => { if (transactionReceipt?.transactionHash && transactionReceipt.transactionHash !== process.txHash) { const txHash = (0, utils_1.isHex)(transactionReceipt.transactionHash, { strict: true, }) ? transactionReceipt.transactionHash : undefined; process = this.statusManager.updateProcess(step, process.type, 'PENDING', { txHash: txHash, txLink: transactionReceipt.transactionLink || (txHash ? `${fromChain.metamask.blockExplorerUrls[0]}tx/${txHash}` : undefined), }); } }; let transactionReceipt; switch (process.txType) { case 'batched': transactionReceipt = await (0, waitForBatchTransactionReceipt_js_1.waitForBatchTransactionReceipt)(this.client, process.taskId, (result) => { const receipt = result.receipts?.find((r) => r.status === 'reverted'); if (receipt) { updateProcessWithReceipt(receipt); } }); break; case 'relayed': transactionReceipt = await (0, waitForRelayedTransactionReceipt_js_1.waitForRelayedTransactionReceipt)(process.taskId, step); break; default: transactionReceipt = await (0, waitForTransactionReceipt_js_1.waitForTransactionReceipt)({ client: this.client, chainId: fromChain.id, txHash: process.txHash, onReplaced: (response) => { this.statusManager.updateProcess(step, process.type, 'PENDING', { txHash: response.transaction.hash, txLink: `${fromChain.metamask.blockExplorerUrls[0]}tx/${response.transaction.hash}`, }); }, }); } updateProcessWithReceipt(transactionReceipt); if (isBridgeExecution) { process = this.statusManager.updateProcess(step, process.type, 'DONE'); } await (0, waitForDestinationChainTransaction_js_1.waitForDestinationChainTransaction)(step, process, fromChain, toChain, this.statusManager); }; prepareUpdatedStep = async (step, process, signedTypedData) => { const { execution, ...stepBase } = step; const relayerStep = (0, typeguards_js_1.isRelayerStep)(step); const gaslessStep = (0, typeguards_js_1.isGaslessStep)(step); const contractCallStep = (0, typeguards_js_1.isContractCallStep)(step); let updatedStep; if (contractCallStep) { const contractCallsResult = await this.executionOptions?.getContractCalls?.({ fromAddress: stepBase.action.fromAddress, fromAmount: BigInt(stepBase.action.fromAmount), fromChainId: stepBase.action.fromChainId, fromTokenAddress: stepBase.action.fromToken.address, slippage: stepBase.action.slippage, toAddress: stepBase.action.toAddress, toAmount: BigInt(stepBase.estimate.toAmount), toChainId: stepBase.action.toChainId, toTokenAddress: stepBase.action.toToken.address, }); if (!contractCallsResult?.contractCalls?.length) { throw new errors_js_1.TransactionError(constants_js_2.LiFiErrorCode.TransactionUnprepared, 'Unable to prepare transaction. Contract calls are not found.'); } if (contractCallsResult.patcher) { const patchedContractCalls = await (0, api_js_1.patchContractCalls)(contractCallsResult.contractCalls.map((call) => ({ chainId: stepBase.action.toChainId, fromTokenAddress: call.fromTokenAddress, targetContractAddress: call.toContractAddress, callDataToPatch: call.toContractCallData, delegateCall: false, patches: [ { amountToReplace: constants_js_1.PatcherMagicNumber.toString(), }, ], }))); contractCallsResult.contractCalls.forEach((call, index) => { call.toContractAddress = patchedContractCalls[index].target; call.toContractCallData = patchedContractCalls[index].callData; }); } const contractCallQuote = await (0, api_js_1.getContractCallsQuote)({ fromAddress: stepBase.action.fromAddress, fromChain: stepBase.action.fromChainId, fromToken: stepBase.action.fromToken.address, fromAmount: stepBase.action.fromAmount, toChain: stepBase.action.toChainId, toToken: stepBase.action.toToken.address, contractCalls: contractCallsResult.contractCalls, toFallbackAddress: stepBase.action.toAddress, slippage: stepBase.action.slippage, }); contractCallQuote.action.toToken = stepBase.action.toToken; const customStep = contractCallQuote.includedSteps?.find((step) => step.type === 'custom'); if (customStep && contractCallsResult?.contractTool) { const toolDetails = { key: contractCallsResult.contractTool.name, name: contractCallsResult.contractTool.name, logoURI: contractCallsResult.contractTool.logoURI, }; customStep.toolDetails = toolDetails; contractCallQuote.toolDetails = toolDetails; } const route = (0, convertQuoteToRoute_js_1.convertQuoteToRoute)(contractCallQuote, { adjustZeroOutputFromPreviousStep: this.executionOptions?.adjustZeroOutputFromPreviousStep, }); updatedStep = { ...route.steps[0], id: stepBase.id, }; } else if (relayerStep && gaslessStep) { const updatedRelayedStep = await (0, api_js_1.getRelayerQuote)({ fromChain: stepBase.action.fromChainId, fromToken: stepBase.action.fromToken.address, fromAddress: stepBase.action.fromAddress, fromAmount: stepBase.action.fromAmount, toChain: stepBase.action.toChainId, toToken: stepBase.action.toToken.address, slippage: stepBase.action.slippage, toAddress: stepBase.action.toAddress, allowBridges: [stepBase.tool], }); updatedStep = { ...updatedRelayedStep, id: stepBase.id, }; } else { const filteredSignedTypedData = signedTypedData?.filter((item) => item.signature); const { typedData: _, ...restStepBase } = stepBase; const params = filteredSignedTypedData?.length ? { ...restStepBase, typedData: filteredSignedTypedData } : restStepBase; updatedStep = await (0, api_js_1.getStepTransaction)(params); } const comparedStep = await (0, stepComparison_js_1.stepComparison)(this.statusManager, step, updatedStep, this.allowUserInteraction, this.executionOptions); Object.assign(step, { ...comparedStep, execution: step.execution, typedData: updatedStep.typedData ?? step.typedData, }); if (!step.transactionRequest && !step.typedData?.length) { throw new errors_js_1.TransactionError(constants_js_2.LiFiErrorCode.TransactionUnprepared, 'Unable to prepare transaction.'); } let transactionRequest; if (step.transactionRequest) { let maxPriorityFeePerGas; if (this.client.account?.type === 'local') { const updatedClient = await this.checkClient(step, process); if (!updatedClient) { return null; } maxPriorityFeePerGas = await (0, utils_js_1.getMaxPriorityFeePerGas)(updatedClient); } else { maxPriorityFeePerGas = step.transactionRequest.maxPriorityFeePerGas ? BigInt(step.transactionRequest.maxPriorityFeePerGas) : undefined; } transactionRequest = { chainId: step.transactionRequest.chainId, to: step.transactionRequest.to, from: step.transactionRequest.from, data: step.transactionRequest.data, value: step.transactionRequest.value ? BigInt(step.transactionRequest.value) : undefined, gas: step.transactionRequest.gasLimit ? BigInt(step.transactionRequest.gasLimit) : undefined, maxPriorityFeePerGas, }; } if (this.executionOptions?.updateTransactionRequestHook && transactionRequest) { const customizedTransactionRequest = await this.executionOptions.updateTransactionRequestHook({ requestType: 'transaction', ...transactionRequest, }); transactionRequest = { ...transactionRequest, ...customizedTransactionRequest, }; } return { transactionRequest, isRelayerTransaction: (0, typeguards_js_1.isRelayerStep)(updatedStep), }; }; estimateTransactionRequest = async (client, transactionRequest) => { try { const estimatedGas = await (0, getActionWithFallback_js_1.getActionWithFallback)(client, actions_1.estimateGas, 'estimateGas', { account: client.account, to: transactionRequest.to, data: transactionRequest.data, value: transactionRequest.value, }); const baseGas = transactionRequest.gas && transactionRequest.gas > estimatedGas ? transactionRequest.gas : estimatedGas; transactionRequest.gas = baseGas + 300000n; } catch (_) { if (transactionRequest.gas) { transactionRequest.gas = transactionRequest.gas + 300000n; } } return transactionRequest; }; executeStep = async (step, atomicityNotReady = false) => { step.execution = this.statusManager.initExecutionObject(step); const destinationChainProcess = step.execution?.process.find((process) => process.type === 'RECEIVING_CHAIN'); if (destinationChainProcess && destinationChainProcess.substatus !== 'WAIT_DESTINATION_TRANSACTION') { const updatedClient = await this.checkClient(step, destinationChainProcess); if (!updatedClient) { return step; } } const fromChain = await config_js_1.config.getChainById(step.action.fromChainId); const toChain = await config_js_1.config.getChainById(step.action.toChainId); const calls = []; let signedTypedData = []; const batchingSupported = atomicityNotReady || step.tool === 'thorswap' || (0, typeguards_js_1.isRelayerStep)(step) ? false : await (0, isBatchingSupported_js_1.isBatchingSupported)({ client: this.client, chainId: fromChain.id, }); const isBridgeExecution = fromChain.id !== toChain.id; const currentProcessType = isBridgeExecution ? 'CROSS_CHAIN' : 'SWAP'; const existingProcess = step.execution.process.find((p) => p.type === currentProcessType); const isFromNativeToken = fromChain.nativeToken.address === step.action.fromToken.address && (0, isZeroAddress_js_1.isZeroAddress)(step.action.fromToken.address); const disableMessageSigning = this.executionOptions?.disableMessageSigning || step.type !== 'lifi'; const permit2Supported = !!fromChain.permit2 && !!fromChain.permit2Proxy && !batchingSupported && !isFromNativeToken && !disableMessageSigning && !!step.estimate.approvalAddress && !step.estimate.skipApproval && !step.estimate.skipPermit; const checkForAllowance = !existingProcess?.txHash && !existingProcess?.taskId && !isFromNativeToken && !!step.estimate.approvalAddress && !step.estimate.skipApproval; if (checkForAllowance) { const allowanceResult = await (0, checkAllowance_js_1.checkAllowance)({ checkClient: this.checkClient, chain: fromChain, step, statusManager: this.statusManager, executionOptions: this.executionOptions, allowUserInteraction: this.allowUserInteraction, batchingSupported, permit2Supported, disableMessageSigning, }); switch (allowanceResult.status) { case 'BATCH_APPROVAL': calls.push(...allowanceResult.data.calls); signedTypedData = allowanceResult.data.signedTypedData; break; case 'NATIVE_PERMIT': signedTypedData = allowanceResult.data; break; case 'DONE': signedTypedData = allowanceResult.data; break; default: if (!this.allowUserInteraction) { return step; } break; } } let process = this.statusManager.findProcess(step, currentProcessType); try { if (process?.status === 'DONE') { await (0, waitForDestinationChainTransaction_js_1.waitForDestinationChainTransaction)(step, process, fromChain, toChain, this.statusManager); return step; } if (process?.txHash || process?.taskId) { const updatedClient = await this.checkClient(step, process); if (!updatedClient) { return step; } await this.waitForTransaction({ step, process, fromChain, toChain, isBridgeExecution, }); return step; } process = this.statusManager.findOrCreateProcess({ step, type: currentProcessType, status: 'STARTED', chainId: fromChain.id, }); await (0, checkBalance_js_1.checkBalance)(this.client.account.address, step); const preparedStep = await this.prepareUpdatedStep(step, process, signedTypedData); if (!preparedStep) { return step; } let { transactionRequest, isRelayerTransaction } = preparedStep; process = this.statusManager.updateProcess(step, process.type, 'ACTION_REQUIRED'); if (!this.allowUserInteraction) { return step; } let txHash; let taskId; let txType = 'standard'; let txLink; if (batchingSupported && transactionRequest) { const updatedClient = await this.checkClient(step, process); if (!updatedClient) { return step; } const transferCall = { chainId: fromChain.id, data: transactionRequest.data, to: transactionRequest.to, value: transactionRequest.value, }; calls.push(transferCall); const { id } = await (0, utils_1.getAction)(this.client, actions_1.sendCalls, 'sendCalls')({ account: this.client.account, calls, }); taskId = id; txType = 'batched'; } else if (isRelayerTransaction) { const intentTypedData = step.typedData?.filter((typedData) => !signedTypedData.some((signedPermit) => (0, isNativePermitValid_js_1.isNativePermitValid)(signedPermit, typedData))); if (!intentTypedData?.length) { throw new errors_js_1.TransactionError(constants_js_2.LiFiErrorCode.TransactionUnprepared, 'Unable to prepare transaction. Typed data for transfer is not found.'); } this.statusManager.updateProcess(step, process.type, 'MESSAGE_REQUIRED'); for (const typedData of intentTypedData) { if (!this.allowUserInteraction) { return step; } const typedDataChainId = (0, utils_js_1.getDomainChainId)(typedData.domain) || fromChain.id; const updatedClient = await this.checkClient(step, process, typedDataChainId); if (!updatedClient) { return step; } const signature = await (0, utils_1.getAction)(updatedClient, actions_1.signTypedData, 'signTypedData')({ account: updatedClient.account, primaryType: typedData.primaryType, domain: typedData.domain, types: typedData.types, message: typedData.message, }); signedTypedData.push({ ...typedData, signature: signature, }); } this.statusManager.updateProcess(step, process.type, 'PENDING'); const { execution, ...stepBase } = step; const relayedTransaction = await (0, api_js_1.relayTransaction)({ ...stepBase, typedData: signedTypedData, }); taskId = relayedTransaction.taskId; txType = 'relayed'; txLink = relayedTransaction.txLink; } else { if (!transactionRequest) { throw new errors_js_1.TransactionError(constants_js_2.LiFiErrorCode.TransactionUnprepared, 'Unable to prepare transaction. Transaction request is not found.'); } const updatedClient = await this.checkClient(step, process); if (!updatedClient) { return step; } const signedNativePermitTypedData = signedTypedData.find((p) => p.primaryType === 'Permit' && (0, utils_js_1.getDomainChainId)(p.domain) === fromChain.id); if (signedNativePermitTypedData) { transactionRequest.data = (0, encodeNativePermitData_js_1.encodeNativePermitData)(step.action.fromToken.address, BigInt(step.action.fromAmount), signedNativePermitTypedData.message.deadline, signedNativePermitTypedData.signature, transactionRequest.data); } else if (permit2Supported) { this.statusManager.updateProcess(step, process.type, 'MESSAGE_REQUIRED'); const permit2Signature = await (0, signPermit2Message_js_1.signPermit2Message)({ client: updatedClient, chain: fromChain, tokenAddress: step.action.fromToken.address, amount: BigInt(step.action.fromAmount), data: transactionRequest.data, }); transactionRequest.data = (0, encodePermit2Data_js_1.encodePermit2Data)(step.action.fromToken.address, BigInt(step.action.fromAmount), permit2Signature.message.nonce, permit2Signature.message.deadline, transactionRequest.data, permit2Signature.signature); this.statusManager.updateProcess(step, process.type, 'ACTION_REQUIRED'); } if (signedNativePermitTypedData || permit2Supported) { transactionRequest.to = fromChain.permit2Proxy; transactionRequest = await this.estimateTransactionRequest(updatedClient, transactionRequest); } txHash = await (0, utils_1.getAction)(updatedClient, actions_1.sendTransaction, 'sendTransaction')({ to: transactionRequest.to, account: updatedClient.account, data: transactionRequest.data, value: transactionRequest.value, gas: transactionRequest.gas, gasPrice: transactionRequest.gasPrice, maxFeePerGas: transactionRequest.maxFeePerGas, maxPriorityFeePerGas: transactionRequest.maxPriorityFeePerGas, chain: (0, utils_js_1.convertExtendedChain)(fromChain), }); } process = this.statusManager.updateProcess(step, process.type, 'PENDING', { txHash, taskId, txType, txLink: txType === 'standard' && txHash ? `${fromChain.metamask.blockExplorerUrls[0]}tx/${txHash}` : txLink, }); await this.waitForTransaction({ step, process, fromChain, toChain, isBridgeExecution, }); return step; } catch (e) { if ((0, parseEVMErrors_js_1.isAtomicReadyWalletRejectedUpgradeError)(e) && !atomicityNotReady) { step.execution = undefined; return this.executeStep(step, true); } const error = await (0, parseEVMErrors_js_1.parseEVMErrors)(e, step, process); process = this.statusManager.updateProcess(step, process?.type || currentProcessType, 'FAILED', { error: { message: error.cause.message, code: error.code, }, }); this.statusManager.updateExecution(step, 'FAILED'); throw error; } }; } exports.EVMStepExecutor = EVMStepExecutor; //# sourceMappingURL=EVMStepExecutor.js.map