UNPKG

@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
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