@openocean.finance/widget-sdk
Version:
OpenOcean Any-to-Any Cross-Chain-Swap SDK
426 lines • 21.4 kB
JavaScript
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