@openocean.finance/widget-sdk
Version:
OpenOcean Any-to-Any Cross-Chain-Swap SDK
129 lines • 7.28 kB
JavaScript
import { VersionedTransaction } from '@solana/web3.js';
import { withTimeout } from 'viem';
import { config } from '../../config.js';
import { OpenOceanErrorCode } from '../../errors/constants.js';
import { TransactionError } from '../../errors/errors.js';
import { getStepTransaction } from '../../services/api.js';
import { base64ToUint8Array } from '../../utils/base64ToUint8Array.js';
import { BaseStepExecutor } from '../BaseStepExecutor.js';
import { checkBalance } from '../checkBalance.js';
import { stepComparison } from '../stepComparison.js';
import { waitForDestinationChainTransaction } from '../waitForDestinationChainTransaction.js';
import { callSolanaWithRetry } from './connection.js';
import { parseSolanaErrors } from './parseSolanaErrors.js';
import { sendAndConfirmTransaction } from './sendAndConfirmTransaction.js';
export class SolanaStepExecutor extends BaseStepExecutor {
walletAdapter;
constructor(options) {
super(options);
this.walletAdapter = options.walletAdapter;
}
checkWalletAdapter = (step) => {
// Prevent execution of the quote by wallet different from the one which requested the quote
if (this.walletAdapter.publicKey.toString() !== step.action.fromAddress) {
throw new TransactionError(OpenOceanErrorCode.WalletChangedDuringExecution, 'The wallet address that requested the quote does not match the wallet address attempting to sign the transaction.');
}
};
executeStep = async (step) => {
step.execution = this.statusManager.initExecutionObject(step);
const fromChain = await config.getChainById(step.action.fromChainId);
const toChain = await config.getChainById(step.action.toChainId);
const isBridgeExecution = fromChain.id !== toChain.id;
const currentProcessType = isBridgeExecution ? 'CROSS_CHAIN' : 'SWAP';
let process = this.statusManager.findOrCreateProcess({
step,
type: currentProcessType,
chainId: fromChain.id,
});
if (process.status !== 'DONE') {
try {
process = this.statusManager.updateProcess(step, process.type, 'STARTED');
// Check balance
await checkBalance(this.walletAdapter.publicKey.toString(), step);
// Create new transaction
if (!step.transactionRequest) {
const { execution, ...stepBase } = step;
const 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?.data) {
throw new TransactionError(OpenOceanErrorCode.TransactionUnprepared, 'Unable to prepare transaction.');
}
process = this.statusManager.updateProcess(step, process.type, 'ACTION_REQUIRED');
if (!this.allowUserInteraction) {
return step;
}
let transactionRequest = {
data: step.transactionRequest.data,
};
if (this.executionOptions?.updateTransactionRequestHook) {
const customizedTransactionRequest = await this.executionOptions.updateTransactionRequestHook({
requestType: 'transaction',
...transactionRequest,
});
transactionRequest = {
...transactionRequest,
...customizedTransactionRequest,
};
}
if (!transactionRequest.data) {
throw new TransactionError(OpenOceanErrorCode.TransactionUnprepared, 'Unable to prepare transaction.');
}
const versionedTransaction = VersionedTransaction.deserialize(base64ToUint8Array(transactionRequest.data));
this.checkWalletAdapter(step);
// We give users 2 minutes to sign the transaction or it should be considered expired
const signedTx = await withTimeout(() => this.walletAdapter.signTransaction(versionedTransaction), {
// https://solana.com/docs/advanced/confirmation#transaction-expiration
// Use 2 minutes to account for fluctuations
timeout: 120_000,
errorInstance: new TransactionError(OpenOceanErrorCode.TransactionExpired, 'Transaction has expired: blockhash is no longer recent enough.'),
});
process = this.statusManager.updateProcess(step, process.type, 'PENDING');
const simulationResult = await callSolanaWithRetry((connection) => connection.simulateTransaction(signedTx, {
commitment: 'confirmed',
replaceRecentBlockhash: true,
}));
if (simulationResult.value.err) {
throw new TransactionError(OpenOceanErrorCode.TransactionSimulationFailed, 'Transaction simulation failed');
}
const confirmedTx = await sendAndConfirmTransaction(signedTx);
if (!confirmedTx.signatureResult) {
throw new TransactionError(OpenOceanErrorCode.TransactionExpired, 'Transaction has expired: The block height has exceeded the maximum allowed limit.');
}
if (confirmedTx.signatureResult.err) {
const reason = typeof confirmedTx.signatureResult.err === 'object'
? JSON.stringify(confirmedTx.signatureResult.err)
: confirmedTx.signatureResult.err;
throw new TransactionError(OpenOceanErrorCode.TransactionFailed, `Transaction failed: ${reason}`);
}
// Transaction has been confirmed and we can update the process
process = this.statusManager.updateProcess(step, process.type, 'PENDING', {
txHash: confirmedTx.txSignature,
txLink: `${fromChain.metamask.blockExplorerUrls[0]}tx/${confirmedTx.txSignature}`,
});
if (isBridgeExecution) {
process = this.statusManager.updateProcess(step, process.type, 'DONE');
}
}
catch (e) {
const error = await parseSolanaErrors(e, step, process);
process = this.statusManager.updateProcess(step, process.type, 'FAILED', {
error: {
message: error.cause.message,
code: error.code,
},
});
this.statusManager.updateExecution(step, 'FAILED');
throw error;
}
}
await waitForDestinationChainTransaction(step, process, fromChain, toChain, this.statusManager);
// DONE
return step;
};
}
//# sourceMappingURL=SolanaStepExecutor.js.map