@lifi/sdk
Version:
LI.FI Any-to-Any Cross-Chain-Swap SDK
519 lines • 26.6 kB
JavaScript
"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