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.

680 lines (626 loc) 20.6 kB
import type { Account } from '@openocean.finance/wallet-management' import type { ExecutionStatus, OpenOceanStep, Process, Route, } from '@openocean.finance/widget-sdk' 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: string): Uint8Array { const pairs = hexString.match(/[\dA-F]{2}/gi) || [] return new Uint8Array(pairs.map((s) => Number.parseInt(s, 16))) } interface ExecuteRouteOptions { updateRouteHook?: (route: Route) => void acceptExchangeRateUpdateHook?: (params: any) => Promise<boolean> infiniteApproval?: boolean executeInBackground?: boolean account: Account wagmiConfig: any // Use any to avoid deep type instantiation onDisconnect?: (account: Account) => Promise<void> onOpenWalletMenu?: () => void } interface ExtendedOpenOceanStep extends OpenOceanStep { execution?: { status: ExecutionStatus process: Process[] } toolData?: { data: string } } interface ExtendedRoute extends Route { steps: ExtendedOpenOceanStep[] prependedOperatingExpenseCost?: string data?: { prependedOperatingExpenseCost?: string } } // Execute Solana transaction async function executeSolanaSwap( step: ExtendedOpenOceanStep, options: ExecuteRouteOptions, process: Process, route: ExtendedRoute ): Promise<void> { try { // Check wallet connection status if (!options.account || !options.account.connector) { throw new Error('Wallet not connected or connector not initialized') } let transaction: any = '' const connection = new Connection( 'https://burned-practical-bird.solana-mainnet.quiknode.pro/33f4786133c252415e194b29ee69ffc7671480ab' ) const txData: any = 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 as any 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: ExtendedOpenOceanStep, options: ExecuteRouteOptions, process: Process, route: ExtendedRoute ): Promise<void> { 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)) as any 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 as `0x${string}`, 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 as `0x${string}`, ], })) as bigint } 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 as `0x${string}`, 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 as `0x${string}`, approvalAmount, ], }) await publicClient.waitForTransactionReceipt({ hash }) } } const { transactionRequest } = step || {} const txRequest = { chain: publicClient.chain, to: transactionRequest?.to as `0x${string}`, data: (transactionRequest?.data as `0x${string}`) || '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 as `0x${string}` 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: string | number, decimals = 18): number { return Number(amount) / Math.pow(10, decimals) } /** * 交易响应类型 */ interface SwapResponse { inAmount?: string inToken?: { decimals?: number price?: string | number priceUSD?: string | number address?: string } data?: string from?: string to?: string value?: string minOutAmount?: string } /** * 根据动态滑点调整交易参数,提供 MEV 保护 */ async function swapQuoteMEV( response: SwapResponse, options?: { publicClient?: any } ): Promise<SwapResponse> { 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 as `0x${string}`, to: response?.to as `0x${string}`, data: OPENOCEAN_CONTRACT.interface.encodeFunctionData( 'swap', callData ) as `0x${string}`, 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: ExtendedRoute, options: ExecuteRouteOptions ): Promise<Route> { const updatedRoute = { ...route } try { // Execute transactions for each step for (const step of updatedRoute.steps) { // Update status to start execution const process: 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: unknown) { 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: Route, options: ExecuteRouteOptions ): Promise<Route> { // Check wallet connection status if (!options.account.isConnected) { throw new Error('Wallet not connected') } return executeSwap(route as ExtendedRoute, options) }