UNPKG

@openocean.finance/widget-sdk

Version:

OpenOcean Any-to-Any Cross-Chain-Swap SDK

426 lines 21.4 kB
import { estimateGas, getAddresses, sendTransaction, signTypedData, } from 'viem/actions'; import { sendCalls } from 'viem/experimental'; import { getAction } from 'viem/utils'; import { config } from '../../config.js'; import { OpenOceanErrorCode } from '../../errors/constants.js'; import { TransactionError } from '../../errors/errors.js'; import { getRelayerQuote, getStepTransaction, relayTransaction, } from '../../services/api.js'; import { BaseStepExecutor } from '../BaseStepExecutor.js'; import { checkBalance } from '../checkBalance.js'; import { stepComparison } from '../stepComparison.js'; import { waitForDestinationChainTransaction } from '../waitForDestinationChainTransaction.js'; import { checkAllowance } from './checkAllowance.js'; import { isBatchingSupported } from './isBatchingSupported.js'; import { parseEVMErrors } from './parseEVMErrors.js'; import { encodeNativePermitData } from './permits/encodeNativePermitData.js'; import { encodePermit2Data } from './permits/encodePermit2Data.js'; import { signPermit2Message } from './permits/signPermit2Message.js'; import { switchChain } from './switchChain.js'; import { isRelayerStep } from './typeguards.js'; import { convertExtendedChain, getMaxPriorityFeePerGas } from './utils.js'; import { waitForBatchTransactionReceipt, } from './waitForBatchTransactionReceipt.js'; import { waitForRelayedTransactionReceipt } from './waitForRelayedTransactionReceipt.js'; import { waitForTransactionReceipt } from './waitForTransactionReceipt.js'; export class EVMStepExecutor extends BaseStepExecutor { client; constructor(options) { super(options); this.client = options.client; } // Ensure that we are using the right chain and wallet when executing transactions. checkClient = async (step, process) => { const updatedClient = await switchChain(this.client, this.statusManager, step, this.allowUserInteraction, this.executionOptions?.switchChainHook); if (updatedClient) { this.client = updatedClient; } // Prevent execution of the quote by wallet different from the one which requested the quote let accountAddress = this.client.account?.address; if (!accountAddress) { const accountAddresses = (await getAction(this.client, getAddresses, 'getAddresses')(undefined)); accountAddress = accountAddresses?.[0]; } if (accountAddress?.toLowerCase() !== step.action.fromAddress?.toLowerCase()) { let processToUpdate = process; if (!processToUpdate) { // We need to create some process if we don't have one so we can show the error processToUpdate = this.statusManager.findOrCreateProcess({ step, type: 'TRANSACTION', }); } const errorMessage = 'The wallet address that requested the quote does not match the wallet address attempting to sign the transaction.'; this.statusManager.updateProcess(step, processToUpdate.type, 'FAILED', { error: { code: OpenOceanErrorCode.WalletChangedDuringExecution, message: errorMessage, }, }); this.statusManager.updateExecution(step, 'FAILED'); throw await parseEVMErrors(new TransactionError(OpenOceanErrorCode.WalletChangedDuringExecution, errorMessage), step, process); } return updatedClient; }; waitForTransaction = async ({ step, process, fromChain, toChain, txType, txHash, isBridgeExecution, }) => { let transactionReceipt; switch (txType) { case 'batched': transactionReceipt = await waitForBatchTransactionReceipt(this.client, txHash); break; case 'relayed': transactionReceipt = await waitForRelayedTransactionReceipt(txHash, step); break; default: transactionReceipt = await waitForTransactionReceipt({ client: this.client, chainId: fromChain.id, txHash, onReplaced: (response) => { this.statusManager.updateProcess(step, process.type, 'PENDING', { txHash: response.transaction.hash, txLink: `${fromChain.metamask.blockExplorerUrls[0]}tx/${response.transaction.hash}`, }); }, }); } // Update pending process if the transaction hash from the receipt is different. // This might happen if the transaction was replaced. if (transactionReceipt?.transactionHash && transactionReceipt.transactionHash !== txHash) { process = this.statusManager.updateProcess(step, process.type, 'PENDING', { txHash: transactionReceipt.transactionHash, txLink: `${fromChain.metamask.blockExplorerUrls[0]}tx/${transactionReceipt.transactionHash}`, }); } if (isBridgeExecution) { process = this.statusManager.updateProcess(step, process.type, 'DONE'); } await waitForDestinationChainTransaction(step, process, fromChain, toChain, this.statusManager); }; executeStep = async (step) => { step.execution = this.statusManager.initExecutionObject(step); // Find if it's bridging and the step is waiting for a transaction on the destination chain const destinationChainProcess = step.execution?.process.find((process) => process.type === 'RECEIVING_CHAIN'); // Make sure that the chain is still correct // If the step is waiting for a transaction on the destination chain, we do not switch the chain // All changes are already done from the source chain // Return the step if (destinationChainProcess?.substatus !== 'WAIT_DESTINATION_TRANSACTION') { const updatedClient = await this.checkClient(step); if (!updatedClient) { return step; } } const fromChain = await config.getChainById(step.action.fromChainId); const toChain = await config.getChainById(step.action.toChainId); // Check if the wallet supports atomic batch transactions (EIP-5792) const calls = []; const batchingSupported = await isBatchingSupported({ client: this.client, chainId: fromChain.id, }); const isBridgeExecution = fromChain.id !== toChain.id; const currentProcessType = isBridgeExecution ? 'CROSS_CHAIN' : 'SWAP'; // Find existing swap/bridge process const existingProcess = step.execution.process.find((p) => p.type === currentProcessType); const isFromNativeToken = fromChain.nativeToken.address === step.action.fromToken.address; // Check if step requires permit signature and will be used with relayer service const isRelayerTransaction = isRelayerStep(step); // Check if the wallet supports message signing - useful for smart contract wallets const disableMessageSigning = this.executionOptions?.disableMessageSigning; // Check if chain has Permit2 contract deployed. Permit2 should not be available for atomic batch. const permit2Supported = !!fromChain.permit2 && !!fromChain.permit2Proxy && !batchingSupported && !isFromNativeToken && !disableMessageSigning; const checkForAllowance = // No existing swap/bridge transaction is pending !existingProcess?.txHash && // Token is not native (address is not zero) !isFromNativeToken; let signedNativePermitTypedData; if (checkForAllowance) { // Check if token needs approval and get approval transaction or message data when available const allowanceResult = await checkAllowance({ client: this.client, chain: fromChain, step, statusManager: this.statusManager, executionOptions: this.executionOptions, allowUserInteraction: this.allowUserInteraction, batchingSupported, permit2Supported, disableMessageSigning, }); if (allowanceResult.status === 'BATCH_APPROVAL') { // Create approval transaction call // No value needed since we're only approving ERC20 tokens if (batchingSupported) { calls.push(allowanceResult.data); } } if (allowanceResult.status === 'NATIVE_PERMIT') { signedNativePermitTypedData = allowanceResult.data; } if (allowanceResult.status === 'ACTION_REQUIRED' && !this.allowUserInteraction) { return step; } } let process = this.statusManager.findProcess(step, currentProcessType); if (process?.status === 'DONE') { await waitForDestinationChainTransaction(step, process, fromChain, toChain, this.statusManager); return step; } try { if (process?.txHash) { // Make sure that the chain is still correct const updatedClient = await this.checkClient(step, process); if (!updatedClient) { return step; } // Wait for exiting transaction const txHash = process.txHash; const txType = process.txType; await this.waitForTransaction({ step, process, fromChain, toChain, txType, txHash, isBridgeExecution, }); return step; } const permitRequired = !batchingSupported && !signedNativePermitTypedData && permit2Supported; process = this.statusManager.findOrCreateProcess({ step, type: permitRequired ? 'PERMIT' : currentProcessType, status: 'STARTED', chainId: fromChain.id, }); // Check balance await checkBalance(this.client.account.address, step); // Create new transaction request if (!step.transactionRequest) { const { execution, ...stepBase } = step; let updatedStep; if (isRelayerTransaction) { const updatedRelayedStep = await 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 { updatedStep = await getStepTransaction(stepBase); } const comparedStep = await stepComparison(this.statusManager, step, updatedStep, this.allowUserInteraction, this.executionOptions); Object.assign(step, { ...comparedStep, execution: step.execution, }); } if (!step.transactionRequest) { throw new TransactionError(OpenOceanErrorCode.TransactionUnprepared, 'Unable to prepare transaction.'); } let transactionRequest = { 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, // gasPrice: step.transactionRequest.gasPrice // ? BigInt(step.transactionRequest.gasPrice as string) // : undefined, // maxFeePerGas: step.transactionRequest.maxFeePerGas // ? BigInt(step.transactionRequest.maxFeePerGas as string) // : undefined, maxPriorityFeePerGas: this.client.account?.type === 'local' ? await getMaxPriorityFeePerGas(this.client) : step.transactionRequest.maxPriorityFeePerGas ? BigInt(step.transactionRequest.maxPriorityFeePerGas) : undefined, }; if (this.executionOptions?.updateTransactionRequestHook) { const customizedTransactionRequest = await this.executionOptions.updateTransactionRequestHook({ requestType: 'transaction', ...transactionRequest, }); transactionRequest = { ...transactionRequest, ...customizedTransactionRequest, }; } // Make sure that the chain is still correct const updatedClient = await this.checkClient(step, process); if (!updatedClient) { return step; } process = this.statusManager.updateProcess(step, process.type, 'ACTION_REQUIRED'); if (!this.allowUserInteraction) { return step; } let txHash; let txType = 'standard'; if (batchingSupported) { const transferCall = { chainId: fromChain.id, data: transactionRequest.data, to: transactionRequest.to, value: transactionRequest.value, }; calls.push(transferCall); const { id } = await getAction(this.client, sendCalls, 'sendCalls')({ account: this.client.account, calls, }); txHash = id; txType = 'batched'; } else if (isRelayerTransaction) { const relayerTypedData = step.typedData.find((p) => p.primaryType === 'PermitWitnessTransferFrom' || p.primaryType === 'Order'); if (!relayerTypedData) { throw new TransactionError(OpenOceanErrorCode.TransactionUnprepared, 'Unable to prepare transaction. Typed data for transfer is not found.'); } const signature = await getAction(this.client, signTypedData, 'signTypedData')({ account: this.client.account, primaryType: relayerTypedData.primaryType, domain: relayerTypedData.domain, types: relayerTypedData.types, message: relayerTypedData.message, }); this.statusManager.updateProcess(step, process.type, 'DONE'); process = this.statusManager.findOrCreateProcess({ step, type: currentProcessType, status: 'PENDING', chainId: fromChain.id, }); const signedTypedData = [ { ...relayerTypedData, signature: signature, }, ]; // Add native permit if available as first element, order is important if (signedNativePermitTypedData) { signedTypedData.unshift(signedNativePermitTypedData); } const { execution, ...stepBase } = step; const relayedTransaction = await relayTransaction({ ...stepBase, typedData: signedTypedData, }); txHash = relayedTransaction.taskId; txType = 'relayed'; } else { if (signedNativePermitTypedData) { transactionRequest.data = encodeNativePermitData(step.action.fromToken.address, BigInt(step.action.fromAmount), signedNativePermitTypedData.message.deadline, signedNativePermitTypedData.signature, transactionRequest.data); } else if (permit2Supported) { const permit2Signature = await signPermit2Message({ client: this.client, chain: fromChain, tokenAddress: step.action.fromToken.address, amount: BigInt(step.action.fromAmount), data: transactionRequest.data, }); this.statusManager.updateProcess(step, process.type, 'DONE'); process = this.statusManager.findOrCreateProcess({ step, type: currentProcessType, status: 'PENDING', chainId: fromChain.id, }); transactionRequest.data = encodePermit2Data(step.action.fromToken.address, BigInt(step.action.fromAmount), permit2Signature.message.nonce, permit2Signature.message.deadline, transactionRequest.data, permit2Signature.signature); } if (signedNativePermitTypedData || permit2Supported) { try { // Target address should be the Permit2 proxy contract in case of native permit or Permit2 transactionRequest.to = fromChain.permit2Proxy; // Try to re-estimate the gas due to additional Permit data const estimatedGas = await estimateGas(this.client, { account: this.client.account, to: transactionRequest.to, data: transactionRequest.data, value: transactionRequest.value, }); transactionRequest.gas = transactionRequest.gas && transactionRequest.gas > estimatedGas ? transactionRequest.gas : estimatedGas; } catch { // Let the wallet estimate the gas in case of failure transactionRequest.gas = undefined; } finally { this.statusManager.updateProcess(step, process.type, 'DONE'); } } process = this.statusManager.updateProcess(step, process.type, 'ACTION_REQUIRED'); txHash = await getAction(this.client, sendTransaction, 'sendTransaction')({ to: transactionRequest.to, account: this.client.account, data: transactionRequest.data, value: transactionRequest.value, gas: transactionRequest.gas, gasPrice: transactionRequest.gasPrice, maxFeePerGas: transactionRequest.maxFeePerGas, maxPriorityFeePerGas: transactionRequest.maxPriorityFeePerGas, chain: convertExtendedChain(fromChain), }); } process = this.statusManager.updateProcess(step, process.type, 'PENDING', // When atomic batch or relayer are supported, txHash represents the batch hash or taskId rather than an individual transaction hash { txHash, txType, txLink: txType === 'standard' ? `${fromChain.metamask.blockExplorerUrls[0]}tx/${txHash}` : undefined, }); await this.waitForTransaction({ step, process, fromChain, toChain, txHash, txType, isBridgeExecution, }); // DONE return step; } catch (e) { const error = await 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; } }; } //# sourceMappingURL=EVMStepExecutor.js.map