@openocean.finance/widget
Version:
Openocean Widget for cross-chain bridging and swapping. It will drive your multi-chain strategy and attract new users from everywhere.
538 lines • 22.2 kB
JavaScript
import { Connection, Transaction, VersionedTransaction } from '@solana/web3.js';
import { ethers } from 'ethers';
import { getPublicClient, getWalletClient } from 'wagmi/actions';
import { useSettingsStore } from '../stores/settings/useSettingsStore.js';
import { sendAndConfirmSolanaTransaction } from './SendAndConfirmSolanaTransaction.js';
const OpenOceanABI = [
{
inputs: [
{
internalType: 'contract IOpenOceanCaller',
name: 'caller',
type: 'address',
},
{
components: [
{
internalType: 'contract IERC20',
name: 'srcToken',
type: 'address',
},
{
internalType: 'contract IERC20',
name: 'dstToken',
type: 'address',
},
{
internalType: 'address',
name: 'srcReceiver',
type: 'address',
},
{
internalType: 'address',
name: 'dstReceiver',
type: 'address',
},
{
internalType: 'uint256',
name: 'amount',
type: 'uint256',
},
{
internalType: 'uint256',
name: 'minReturnAmount',
type: 'uint256',
},
{
internalType: 'uint256',
name: 'guaranteedAmount',
type: 'uint256',
},
{
internalType: 'uint256',
name: 'flags',
type: 'uint256',
},
{
internalType: 'address',
name: 'referrer',
type: 'address',
},
{
internalType: 'bytes',
name: 'permit',
type: 'bytes',
},
],
internalType: 'struct OpenOceanExchange.SwapDescription',
name: 'desc',
type: 'tuple',
},
{
components: [
{
internalType: 'uint256',
name: 'target',
type: 'uint256',
},
{
internalType: 'uint256',
name: 'gasLimit',
type: 'uint256',
},
{
internalType: 'uint256',
name: 'value',
type: 'uint256',
},
{
internalType: 'bytes',
name: 'data',
type: 'bytes',
},
],
internalType: 'struct IOpenOceanCaller.CallDescription[]',
name: 'calls',
type: 'tuple[]',
},
],
name: 'swap',
outputs: [
{
internalType: 'uint256',
name: 'returnAmount',
type: 'uint256',
},
],
stateMutability: 'payable',
type: 'function',
},
];
// Add helper function for handling hexadecimal conversion
function hexToUint8Array(hexString) {
const pairs = hexString.match(/[\dA-F]{2}/gi) || [];
return new Uint8Array(pairs.map((s) => Number.parseInt(s, 16)));
}
// Execute Solana transaction
async function executeSolanaSwap(step, options, process, route) {
try {
// Check wallet connection status
if (!options.account || !options.account.connector) {
throw new Error('Wallet not connected or connector not initialized');
}
let transaction = '';
const connection = new Connection('https://burned-practical-bird.solana-mainnet.quiknode.pro/33f4786133c252415e194b29ee69ffc7671480ab');
const txData = step.transactionRequest?.data || '';
const dexId = step.transactionRequest?.type || 0;
if (step.action.fromChainId === step.action.toChainId) {
if (dexId == 6 || dexId == 7 || dexId == 9) {
transaction = VersionedTransaction.deserialize(hexToUint8Array(txData));
}
else {
transaction = Transaction.from(hexToUint8Array(txData));
}
}
else {
transaction = VersionedTransaction.deserialize(hexToUint8Array(txData.slice(2)));
const { blockhash } = await connection.getLatestBlockhash();
transaction.message.recentBlockhash = blockhash;
}
// Check signTransaction method exists
const connector = options.account.connector;
if (typeof connector.signTransaction !== 'function') {
throw new Error('Wallet does not support transaction signing');
}
// Sign transaction
const signedTx = await connector.signTransaction(transaction);
if (!signedTx) {
throw new Error('Failed to sign transaction');
}
// Serialize signed transaction
const serializedTransaction = signedTx.serialize({
verifySignatures: false,
requireAllSignatures: false,
});
// Use improved transaction sender
await sendAndConfirmSolanaTransaction({
connection,
serializedTransaction,
process,
updateRouteHook: (updatedProcess) => {
// Update process status
Object.assign(process, updatedProcess);
// Update step execution status
if (process.status === 'DONE') {
step.execution.status = 'DONE';
}
else if (process.status === 'FAILED') {
step.execution.status = 'FAILED';
}
else {
step.execution.status = 'PENDING';
}
// Call original updateRouteHook
options.updateRouteHook?.(route);
},
});
}
catch (error) {
console.error('Solana swap execution failed:', error);
process.status = 'FAILED';
process.error =
error instanceof Error
? {
code: 'EXECUTION_ERROR',
message: error.message,
htmlMessage: error.message,
}
: {
code: 'UNKNOWN_ERROR',
message: 'Unknown error occurred',
htmlMessage: 'Unknown error occurred',
};
step.execution.status = 'FAILED';
options.updateRouteHook?.(route);
throw error;
}
}
// Execute EVM transaction
async function executeEvmSwap(step, options, process, route) {
try {
let walletClient = await getWalletClient(options.wagmiConfig);
// Check if wallet is connected
if (!walletClient) {
if (options.account?.connector && options.onDisconnect) {
await options.onDisconnect(options.account);
}
if (options.onOpenWalletMenu) {
options.onOpenWalletMenu();
}
throw new Error('Please connect wallet first');
}
// Check if current chain matches target chain
const currentChainId = walletClient.chain.id;
const targetChainId = step.action.fromChainId;
if (currentChainId !== targetChainId) {
try {
// Try to switch to target chain
await walletClient.switchChain({ id: targetChainId });
// Get updated walletClient after chain switch
walletClient = (await getWalletClient(options.wagmiConfig));
if (!walletClient || walletClient.chain.id !== targetChainId) {
throw new Error('Failed to switch chain');
}
}
catch (error) {
console.error('Failed to switch chain:', error);
throw new Error(`Please manually switch to chain ID: ${targetChainId}`);
}
}
const publicClient = getPublicClient(options.wagmiConfig);
if (!publicClient) {
// If disconnect callback exists, disconnect the current connection first
if (options.account?.connector && options.onDisconnect) {
await options.onDisconnect(options.account);
}
// Open wallet menu to let user reconnect
if (options.onOpenWalletMenu) {
options.onOpenWalletMenu();
}
throw new Error('Public client not found');
}
console.log('Current Chain:', publicClient.chain?.id, publicClient.chain?.name);
console.log('Token Address:', step.action.fromToken.address);
console.log('Token Chain ID:', step.action.fromToken.chainId);
console.log('Owner Address:', walletClient.account.address);
console.log('Spender Address:', step.estimate.approvalAddress);
// Check token approval
if ([
'0x0000000000000000000000000000000000000000',
'0x0000000000000000000000000000000000001010',
].indexOf(step.action.fromToken.address) === -1) {
let allowance = 0n;
try {
allowance = (await publicClient.readContract({
address: step.action.fromToken.address,
abi: [
{
constant: true,
inputs: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
],
name: 'allowance',
outputs: [{ name: '', type: 'uint256' }],
type: 'function',
},
],
functionName: 'allowance',
args: [
walletClient.account.address,
step.estimate.approvalAddress,
],
}));
}
catch (error) {
console.error('Failed to read allowance:', error);
// Log additional context
console.error('Context - Step:', JSON.stringify(step, null, 2));
console.error('Context - Route:', JSON.stringify(route, null, 2));
console.error('Context - Public Client Chain:', publicClient.chain);
// Re-throw the error or handle it appropriately
// throw new Error(`Failed to read allowance for token ${step.action.fromToken.address}: ${error instanceof Error ? error.message : String(error)}`);
}
const amount = BigInt(step.action.fromAmount) +
BigInt(route?.prependedOperatingExpenseCost ||
route?.data?.prependedOperatingExpenseCost ||
'0');
if (allowance < BigInt(amount)) {
const approvalAmount = options.infiniteApproval
? BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff')
: BigInt(amount);
const hash = await walletClient.writeContract({
chain: walletClient.chain,
account: walletClient.account,
address: step.action.fromToken.address,
abi: [
{
constant: false,
inputs: [
{ name: 'spender', type: 'address' },
{ name: 'amount', type: 'uint256' },
],
name: 'approve',
outputs: [{ name: '', type: 'bool' }],
type: 'function',
},
],
functionName: 'approve',
args: [
step.estimate.approvalAddress,
approvalAmount,
],
});
await publicClient.waitForTransactionReceipt({ hash });
}
}
const { transactionRequest } = step || {};
const txRequest = {
chain: publicClient.chain,
to: transactionRequest?.to,
data: transactionRequest?.data || '0x',
value: BigInt(transactionRequest?.value || '0x0'),
account: walletClient.account.address,
};
// 在这里根据useSettingsStore 中的 dynamicSlippage 参数来判断是否调用下面的函数 swap_quote_mev
const { dynamicSlippage } = useSettingsStore.getState();
if (dynamicSlippage) {
try {
// 构建 response 对象
const response = {
inAmount: step.action.fromAmount || '0',
inToken: step.action.fromToken,
data: transactionRequest?.data,
from: walletClient.account.address,
to: transactionRequest?.to,
value: transactionRequest?.value || '0',
};
// 调用 swap_quote_mev 获取调整后的交易数据
const adjustedResponse = await swapQuoteMEV(response, { publicClient });
// 如果 swap_quote_mev 返回了修改后的数据,更新交易请求
if (adjustedResponse && adjustedResponse.data !== response.data) {
txRequest.data = adjustedResponse.data;
txRequest.value = BigInt(adjustedResponse.value || '0');
console.log('Applied MEV protection with dynamic slippage');
}
}
catch (error) {
console.error('Failed to apply MEV protection:', error);
// 错误时继续使用原始交易请求
}
}
// Estimate gas
const estimatedGas = await publicClient.estimateGas(txRequest);
console.log('estimatedGas', estimatedGas);
// Add estimated gas to transaction request (using 2x the estimated value to ensure transaction success)
const finalTxRequest = {
...txRequest,
gas: estimatedGas * 2n,
};
const hash = await walletClient.sendTransaction({
...finalTxRequest,
kzg: undefined,
});
process.status = 'PENDING';
process.txHash = hash;
process.message = 'Transaction pending';
options.updateRouteHook?.(route);
const receipt = await publicClient.waitForTransactionReceipt({ hash });
process.status = receipt.status === 'success' ? 'DONE' : 'FAILED';
process.doneAt = Date.now();
process.message =
receipt.status === 'success'
? 'Transaction confirmed'
: 'Transaction failed';
step.execution.status = receipt.status === 'success' ? 'DONE' : 'FAILED';
options.updateRouteHook?.(route);
if (receipt.status !== 'success') {
throw new Error('Transaction failed');
}
}
catch (error) {
console.error('EVM swap execution failed:', error);
process.status = 'FAILED';
process.error =
error instanceof Error
? {
code: 'EXECUTION_ERROR',
message: error.message,
htmlMessage: error.message,
}
: {
code: 'UNKNOWN_ERROR',
message: 'Unknown error occurred',
htmlMessage: 'Unknown error occurred',
};
step.execution.status = 'FAILED';
options.updateRouteHook?.(route);
throw error;
}
}
/**
* 将带精度的金额转换为实际金额
* @param amount 带精度的金额
* @param decimals 精度
* @returns 实际金额
*/
function decimals2Amount(amount, decimals = 18) {
return Number(amount) / Math.pow(10, decimals);
}
/**
* 根据动态滑点调整交易参数,提供 MEV 保护
*/
async function swapQuoteMEV(response, options) {
try {
const inAmount = response?.inAmount;
const inTokenDecimals = response?.inToken?.decimals || 18;
const inTokenPrice = Number(response?.inToken?.priceUSD || 0);
const amount = decimals2Amount(inAmount, inTokenDecimals) * inTokenPrice;
if (amount < 1) {
return response;
}
const { publicClient } = options;
const OPENOCEAN_CONTRACT = new ethers.Contract('0x6352a56caadC4F1E25CD6c75970Fa768A3304e64', OpenOceanABI);
if (ethers.hexlify(ethers.getBytes(response?.data || '0x').slice(0, 4)) !==
OPENOCEAN_CONTRACT.interface.getFunction('swap').selector) {
return response;
}
const oldCallData = OPENOCEAN_CONTRACT.interface.decodeFunctionData('swap', response?.data);
const callData = [...oldCallData];
callData[1] = [...oldCallData[1]];
const minOutAmount = BigInt(callData[1][5] || 0);
const outAmount = BigInt(callData[1][6] || 0);
const slippageAmount = outAmount - minOutAmount;
const minOutAmounts = await Promise.all([1, 2, 3].map(async (i) => {
const mockMinOutAmount = minOutAmount + (slippageAmount / 4n) * BigInt(i);
callData[1][5] = mockMinOutAmount;
const params = {
from: response?.from,
to: response?.to,
data: OPENOCEAN_CONTRACT.interface.encodeFunctionData('swap', callData),
value: BigInt(response?.value || '0'),
};
try {
await publicClient.estimateGas(params);
return mockMinOutAmount;
}
catch (error) {
console.error('Failed to estimate gas:', error);
return undefined;
}
}));
let [min1, min2] = minOutAmounts
.filter((value) => value !== undefined)
.sort((a, b) => (BigInt(b || 0) > BigInt(a || 0) ? 1 : -1))
.slice(0, 2);
min1 = min1 ?? minOutAmount;
min2 = min2 ?? minOutAmount;
const randomFactor = BigInt(Math.floor(Math.random() * 10000));
const minOutAmountDiff = BigInt(min1 || 0) - BigInt(min2 || 0);
const finalMinOutAmount = BigInt(min2 || 0) + (minOutAmountDiff * randomFactor) / BigInt(10000);
if (finalMinOutAmount < minOutAmount) {
return response;
}
callData[1][5] = finalMinOutAmount;
const finalCallData = OPENOCEAN_CONTRACT.interface.encodeFunctionData('swap', callData);
response.minOutAmount = finalMinOutAmount.toString();
response.data = finalCallData;
return response;
}
catch (error) {
return response;
}
}
// Execute transaction
async function executeSwap(route, options) {
const updatedRoute = { ...route };
try {
// Execute transactions for each step
for (const step of updatedRoute.steps) {
// Update status to start execution
const process = {
type: 'SWAP',
status: 'STARTED',
startedAt: Date.now(),
message: 'Preparing swap transaction',
txHash: '',
};
step.execution = {
status: 'PENDING',
process: [process],
};
options.updateRouteHook?.(updatedRoute);
// Check wallet connection status
if (!options.account || !options.account.connector) {
// If connector exists but not connected, disconnect first
if (options.account?.connector && options.onDisconnect) {
await options.onDisconnect(options.account);
}
// Open wallet menu
if (options.onOpenWalletMenu) {
options.onOpenWalletMenu();
}
throw new Error('Please connect wallet first');
}
// Execute different transaction logic based on chain type
const currentStep = route.steps[0];
if (currentStep.action?.fromChainId === 1151111081099710) {
await executeSolanaSwap(currentStep, options, process, updatedRoute);
}
else {
await executeEvmSwap(currentStep, options, process, updatedRoute);
}
}
return updatedRoute;
}
catch (error) {
console.error('Swap execution failed:', error);
updatedRoute.steps.forEach((step) => {
if (step.execution?.status === 'PENDING') {
step.execution.status = 'FAILED';
step.execution.process[0].status = 'FAILED';
step.execution.process[0].message =
error instanceof Error ? error.message : 'Transaction failed';
}
});
options.updateRouteHook?.(updatedRoute);
throw error;
}
}
// Export main function
export async function executeRoute(route, options) {
// Check wallet connection status
if (!options.account.isConnected) {
throw new Error('Wallet not connected');
}
return executeSwap(route, options);
}
//# sourceMappingURL=ExecuteRoute.js.map