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.

1,257 lines (1,139 loc) 40.1 kB
import type { Account } from '@openocean.finance/wallet-management' import type { ExecutionStatus, OpenOceanStep, Process, Route, } from '@openocean.finance/widget-sdk' import { adaptSolanaWallet } from '@reservoir0x/relay-solana-wallet-adapter' import { Connection, Transaction, VersionedTransaction } from '@solana/web3.js' import { ethers } from 'ethers' import { getPublicClient, getWalletClient } from 'wagmi/actions' import { bridgeExecuteSwap } from '../cross/crossChainQuote.js' import { useSettingsStore } from '../stores/settings/useSettingsStore.js' import { sendAndConfirmSolanaTransaction } from './SendAndConfirmSolanaTransaction.js' import { adaptBitcoinWallet } from '@relayprotocol/relay-bitcoin-wallet-adapter' import * as bitcoin from 'bitcoinjs-lib'; import { OpenOceanService } from './OpenOceanService.js' type DynamicSignPsbtParams = { allowedSighash: number[]; unsignedPsbtBase64: string; signature: Array<{ address: string; signingIndexes: number[]; }>; }; 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))) } /** * Convert amount with precision to actual amount * @param amount Amount with precision * @param decimals Precision * @returns Actual amount */ function decimals2Amount(amount: string | number, decimals = 18): number { return Number(amount) / 10 ** decimals } /** * Swap response type */ 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 } /** * Adjust transaction parameters based on dynamic slippage to provide MEV protection */ 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 as string, 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 as any ) 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 { return response } } 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 solanaWallet?: any // Near wallet-selector 实例从 React 层通过 options 传入,避免在此文件中直接调用 Hook nearWallet?: any } interface ExtendedOpenOceanStep extends OpenOceanStep { quoteData?: any 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') } const rpcUrl = await OpenOceanService.getRpcUrl() let transaction: any = '' const connection = new Connection(rpcUrl) const { transactionRequest, type, quoteData } = step || {} if ((type as any) === 'bridge') { // Define signAndSendTransaction function const signAndSendTransaction = async (transaction: Transaction | VersionedTransaction) => { try { // Ensure transaction is properly formatted if ( transaction instanceof VersionedTransaction || transaction instanceof Transaction ) { const connector = options.account.connector as any if (typeof connector.signTransaction !== 'function') { throw new Error('Wallet does not support transaction signing') } // Sign transaction const signature = await connector.signTransaction(transaction) const serializedTransaction = signature.serialize({ verifySignatures: false, requireAllSignatures: false, }) const txid = await connection.sendRawTransaction( serializedTransaction, { skipPreflight: true, } ) return { signature: txid } } throw new Error('Invalid transaction type') } catch (error) { console.error('Transaction sending failed:', error) throw error } } const adaptedWallet: any = adaptSolanaWallet( options.account.address?.toString() || '1nc1nerator11111111111111111111111111111111', 792703809, //chain id that Relay uses to identify solana connection, signAndSendTransaction ) // Expose connection and sendTransaction method for adapters to use adaptedWallet.connection = connection adaptedWallet.sendTransaction = signAndSendTransaction const signedTx = await bridgeExecuteSwap({ quoteData: quoteData, walletClient: adaptedWallet, nearWallet: options.nearWallet, }) if (!signedTx) { throw new Error('Failed to sign transaction') } const hash = signedTx.sourceTxHash process.status = 'DONE' process.doneAt = Date.now() process.txHash = hash process.message = 'Transaction confirmed' options.updateRouteHook?.(route) // process.status = 'PENDING' // process.txHash = hash // process.message = 'Transaction pending' // options.updateRouteHook?.(route) } else { const txData: any = transactionRequest?.data || '' const dexId = transactionRequest?.type || 0 if (step.action.fromChainId === step.action.toChainId) { if (dexId === 6 || dexId === 7 || dexId === 9 || dexId === 10) { 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 console.log( 'walletClient', walletClient, walletClient.chain, walletClient.getChainId ) const currentChainId = await walletClient.getChainId() const targetChainId = step.action.fromChainId if (currentChainId != targetChainId && targetChainId != 20000000000001) { 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 const currentChainId2 = await walletClient.getChainId() if (!walletClient || currentChainId2 !== 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 && step.estimate.approvalAddress !== '0x0000000000000000000000000000000000000000' ) { let allowance = 0n try { allowance = (await (publicClient as any).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 || undefined, 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, type, quoteData } = step || {} let hash: any = '' if ((type as any) === 'bridge') { const result = await bridgeExecuteSwap({ quoteData: quoteData, walletClient: walletClient, nearWallet: options.nearWallet, }) hash = result?.sourceTxHash || '' } else { 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, } // Check if dynamicSlippage is enabled in useSettingsStore to determine whether to call swap_quote_mev const { dynamicSlippage } = useSettingsStore.getState() if (dynamicSlippage) { try { // Build response object 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', } // Call swap_quote_mev to get adjusted transaction data const adjustedResponse = await swapQuoteMEV(response, { publicClient, }) // If swap_quote_mev returns modified data, update transaction request if (adjustedResponse && adjustedResponse.data !== response.data) { txRequest.data = adjustedResponse.data as `0x${string}` txRequest.value = BigInt(adjustedResponse.value || '0') // Applied MEV protection with dynamic slippage } } catch (error) { console.error('Failed to apply MEV protection:', error) // Continue with original transaction request on error } } // Estimate gas const estimatedGas = await publicClient.estimateGas(txRequest) // Add estimated gas to transaction request (using 2x the estimated value to ensure transaction success) const finalTxRequest = { ...txRequest, gas: estimatedGas * 2n, } 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: any) { console.error('EVM swap execution failed:', error) process.status = 'FAILED' process.error = error?.details || error?.message ? { code: 'EXECUTION_ERROR', message: error?.details || error?.message, htmlMessage: error?.details || error?.message, } : { code: 'UNKNOWN_ERROR', message: 'Unknown error occurred', htmlMessage: 'Unknown error occurred', } step.execution!.status = 'FAILED' options.updateRouteHook?.(route) throw new Error(error?.details || error?.message) } } const createPsbtOptions = (_: any, request: any) => { var _a; const psbtSignOptions: any = { autoFinalized: false, }; if (request.signature) { // validatePsbt(psbt, request.allowedSighash, request.signature); const toSignInputs = []; for (const signature of request.signature) { if ((_a = signature.signingIndexes) === null || _a === void 0 ? void 0 : _a.length) { for (const index of signature.signingIndexes) { toSignInputs.push({ address: signature.address, disableAddressValidation: signature.disableAddressValidation, index, sighashTypes: request.allowedSighash, }); } } } psbtSignOptions.toSignInputs = toSignInputs; } return psbtSignOptions; }; // Helper function to convert hex response to base64 function convertHexToBase64(hexString: string): string { try { const buffer = Buffer.from(hexString, 'hex'); return buffer.toString('base64'); } catch (error) { // If conversion fails, it might already be base64 return hexString; } } // Helper function to extract signed PSBT from wallet response function extractSignedPsbt(response: any): string | null { if (!response) return null; if (typeof response === 'string') return response; return response.signedPsbtHex || response.signedPsbtBase64 || response.signedPsbt || null; } // BTC 主网 const network = bitcoin.networks.bitcoin; export async function sendBTCWithPhantom(sender: string, recipient: string, amount: number) { const phantom = (window as any).phantom?.bitcoin; if (!phantom) throw new Error("Phantom Bitcoin provider not found"); // 1. 获取 UTXO(这里使用 mempool.space API,你也可以换自己的) const utxos = await fetch( `https://mempool.space/api/address/${sender}/utxo` ).then((r) => r.json()); if (!utxos.length) throw new Error("No UTXO available"); // ====== 费用参数 ====== // amount / fee 单位都按 satoshi 处理,调用方需要保证一致 const fee = 5000; // 手续费(可以根据当前网络费率动态调整) const required = BigInt(amount + fee); // 2. 简单的累加选币:从小到大选 UTXO,直到覆盖 amount + fee const sortedUtxos = [...utxos].sort( (a: any, b: any) => Number(a.value) - Number(b.value) ); const selectedUtxos: any[] = []; let totalInput = 0n; for (const utxo of sortedUtxos) { selectedUtxos.push(utxo); totalInput += BigInt(utxo.value); if (totalInput >= required) break; } if (totalInput < required) { throw new Error("Insufficient balance: UTXOs do not cover amount + fee"); } const changeValue = totalInput - required; // 可选:避免输出过小(dust),这里简单判断 >0 即可,如需更严格可改成 > 546 if (changeValue <= 0n) { throw new Error("UTXOs too small to cover amount and fee with change"); } // 3. 构造 PSBT(多输入) const psbt = new bitcoin.Psbt({ network }); selectedUtxos.forEach((utxo) => { psbt.addInput({ hash: utxo.txid, index: utxo.vout, witnessUtxo: { // bitcoinjs-lib 期望 bigint,这里将 mempool 返回的 number 转成 bigint value: BigInt(utxo.value), script: bitcoin.address.toOutputScript(sender, network), }, }); }); // 目标地址 output psbt.addOutput({ address: recipient, value: BigInt(amount), }); // 找零 output psbt.addOutput({ address: sender, value: changeValue, }); // 4. 调用 Phantom.signPSBT const psbtBase64 = psbt.toBase64(); const signOptions = { autoFinalized: false, toSignInputs: selectedUtxos.map((_, index) => ({ address: sender, index, // SIGHASH_ALL (0x01) 通常足够,如有需要可根据业务调整 sighashTypes: [0, 1], // 保持和原来一致 })), }; const signed = await phantom.signPSBT(psbtBase64, signOptions); // Phantom 返回 Base64 const signedPsbt = bitcoin.Psbt.fromBase64(signed); // 5. Finalize + 提取原始交易 signedPsbt.finalizeAllInputs(); const rawTx = signedPsbt.extractTransaction().toHex(); // 6. 广播 Raw TX const txid = await fetch("https://mempool.space/api/tx", { method: "POST", body: rawTx, }).then((r) => r.text()); console.log("Broadcasted TXID:", txid); return txid; } async function executeBitcoinSwap( step: ExtendedOpenOceanStep, options: ExecuteRouteOptions, process: Process, route: ExtendedRoute ): Promise<void> { try { const { quoteData } = step || {} const adaptedWallet: any = adaptBitcoinWallet( options.account.address?.toString() || '', async (_: any, __: any, dynamicParams: DynamicSignPsbtParams) => { const psbtFromBase64 = bitcoin.Psbt.fromBase64(dynamicParams.unsignedPsbtBase64); const psbtHex = psbtFromBase64.toHex(); const connector = options.account.connector; const anyWindow = typeof window !== 'undefined' ? (window as any) : undefined switch (connector?.name) { case 'OKX Wallet': { const response = await anyWindow.okxwallet?.bitcoin?.signPsbt( psbtHex, createPsbtOptions(psbtFromBase64, dynamicParams) ); const signedPsbt = extractSignedPsbt(response); if (!signedPsbt) { throw new Error('Missing psbt response from OKX wallet'); } return convertHexToBase64(signedPsbt); } case 'Unisat': { const response = await anyWindow.unisat?.signPsbt( psbtHex, createPsbtOptions(psbtFromBase64, dynamicParams) ); const signedPsbt = extractSignedPsbt(response); if (!signedPsbt) { throw new Error('Missing psbt response from Unisat wallet'); } return convertHexToBase64(signedPsbt); } case 'Xverse': { const response = await anyWindow.BitcoinProvider?.request('signPsbt', { psbt: psbtHex, finalize: true, }); const signedPsbt = extractSignedPsbt(response); if (!signedPsbt) { throw new Error('Missing psbt response from Xverse wallet'); } return convertHexToBase64(signedPsbt); } case 'Phantom': { const phantom = anyWindow.phantom?.bitcoin; if (!phantom?.signPSBT) throw new Error('Phantom wallet does not support signPSBT'); const inputsToSign = []; console.log("Phantom options = ", JSON.stringify(createPsbtOptions(psbtFromBase64, dynamicParams))); console.log("psbtHex = ", psbtHex); console.log("psbtBase64 = ", psbtFromBase64.toBase64()); for (const sig of dynamicParams.signature || []) { for (const index of sig.signingIndexes || []) { inputsToSign.push({ index, address: sig.address, sighashTypes: dynamicParams.allowedSighash, }); } } const response = await phantom.signPSBT( psbtFromBase64.toBase64(), // ✅ Phantom only accepts base64 { autoFinalize: false, // ✅ correct name inputsToSign, // ✅ correct name } ); const signedPsbt = extractSignedPsbt(response); if (!signedPsbt) throw new Error('Missing psbt response from Phantom wallet'); return signedPsbt; // already base64 } default: throw new Error(`Unsupported wallet: ${connector?.name || 'Unknown'}`); } } ); adaptedWallet.sendTransaction = async (params: { recipient: string; amount: string | number }) => { const connector = options.account.connector; const anyWindow = typeof window !== 'undefined' ? (window as any) : undefined // Convert amount to satoshis (BTC amount * 100000000) const amountInSatoshis = Number(params.amount) switch (connector?.name) { case 'OKX Wallet': { // OKX wallet sendBitcoin method if (anyWindow.okxwallet?.bitcoin?.sendBitcoin) { const txid = await anyWindow.okxwallet.bitcoin.sendBitcoin( params.recipient, amountInSatoshis ); return txid; } throw new Error('OKX wallet does not support sendBitcoin'); } case 'Unisat': { // Unisat wallet sendBitcoin method if (anyWindow.unisat?.sendBitcoin) { const txid = await anyWindow.unisat.sendBitcoin( params.recipient, amountInSatoshis ); return txid; } throw new Error('Unisat wallet does not support sendBitcoin'); } case 'Xverse': { // Xverse wallet sendBitcoin method if (anyWindow.BitcoinProvider?.request) { const response = await anyWindow.BitcoinProvider.request('sendBitcoin', { address: params.recipient, amount: amountInSatoshis, }); return response.txid || response; } throw new Error('Xverse wallet does not support sendBitcoin'); } case 'Phantom': { // Phantom wallet sendBitcoin method const txid = await sendBTCWithPhantom( options.account.address?.toString() || '', params.recipient, amountInSatoshis ); return txid; } default: throw new Error(`Unsupported wallet: ${connector?.name}`); } } const signedTx = await bridgeExecuteSwap({ quoteData: quoteData, walletClient: adaptedWallet, nearWallet: options.nearWallet, }); if (!signedTx) { throw new Error('Failed to sign transaction') } const hash = signedTx.sourceTxHash process.status = 'DONE' process.doneAt = Date.now() process.txHash = hash process.message = 'Transaction confirmed' options.updateRouteHook?.(route) // process.status = 'PENDING' // process.txHash = hash // process.message = 'Transaction pending' // options.updateRouteHook?.(route) } catch (error) { process.status = 'FAILED' process.error = error instanceof Error || (error && (error as any)?.message) ? { code: 'EXECUTION_ERROR', message: error instanceof Error ? error.message : 'Unknown error occurred', htmlMessage: error instanceof Error ? error.message : 'Unknown error occurred', } : { code: 'UNKNOWN_ERROR', message: 'Unknown error occurred', htmlMessage: 'Unknown error occurred', } step.execution!.status = 'FAILED' options.updateRouteHook?.(route) throw error } } // Execute Near transaction via Near Intents adapter async function executeNearSwap( step: ExtendedOpenOceanStep, options: ExecuteRouteOptions, process: Process, route: ExtendedRoute ): Promise<void> { try { const { type, transactionRequest } = step || {} if ((type as any) === 'bridge') { const { quoteData } = step || {} if (!quoteData) { throw new Error('Missing Near quote data.') } // NearIntentsAdapter 的 Near 分支只依赖 nearWallet,不依赖 walletClient, // 这里传一个占位对象即可。 const dummyWalletClient: any = {} const result = await bridgeExecuteSwap({ quoteData, walletClient: dummyWalletClient, nearWallet: options.nearWallet, }) if (!result) { throw new Error('Failed to execute Near swap.') } const hash = result.sourceTxHash process.status = 'DONE' process.doneAt = Date.now() process.txHash = hash process.message = 'Transaction confirmed' step.execution!.status = 'DONE' options.updateRouteHook?.(route) } else { let transactions = JSON.parse(Buffer.from(transactionRequest.data, 'base64').toString()) const wallet = options.nearWallet const txs = transactions.map((t: any, i: number) => { return { receiverId: t.receiverId, // nonceOffset: i + 1, signerId: wallet.signedAccountId, actions: t.functionCalls.map((fc: any) => { if (fc.deposit) { const depositNum = typeof fc.deposit === 'string' ? parseFloat(fc.deposit) : fc.deposit if (depositNum > 0) { const depositStr = depositNum.toFixed(24) fc.deposit = depositStr.replace('.', '').replace(/^0+/, '') || '0' } else { fc.deposit = '0' } } return { type: 'FunctionCall', params: { methodName: fc.methodName, args: { ...fc.args, }, gas: fc.gas, deposit: fc.deposit, }, } }) }; }) const txResult = await wallet.signAndSendTransactions({ transactions: txs }); let transaction: any = { hash: "" }; if (txResult && txResult.length === 1) { transaction = txResult[txResult.length - 1].transaction || {}; } else if (txResult && txResult.length > 1) { transaction = txResult.filter((item: any) => { const { actions = [] } = item && item.transaction || {}; const _actions = actions.filter((fc: any) => { const { FunctionCall = {} } = fc; const { method_name } = FunctionCall; return method_name === 'ft_transfer_call'; }); return _actions && _actions.length > 0; }); if (transaction && transaction.length) { transaction = transaction[0].transaction; } else { transaction = txResult[txResult.length - 1].transaction || {}; } } console.log('signAndSendTransactions', transaction); const { hash } = transaction; process.status = 'DONE' process.doneAt = Date.now() process.txHash = hash process.message = 'Transaction confirmed' step.execution!.status = 'DONE' options.updateRouteHook?.(route) } } catch (error) { console.error('Near swap execution failed:', error) process.status = 'FAILED' process.error = error instanceof Error || (error && (error as any)?.message) ? { code: 'EXECUTION_ERROR', message: error instanceof Error ? error.message : 'Unknown error occurred', htmlMessage: error instanceof Error ? error.message : 'Unknown error occurred', } : { code: 'UNKNOWN_ERROR', message: 'Unknown error occurred', htmlMessage: 'Unknown error occurred', } step.execution!.status = 'FAILED' options.updateRouteHook?.(route) throw error } } // 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 if (currentStep.action?.fromChainId === 20000000000001) { await executeBitcoinSwap(currentStep, options, process, updatedRoute) } else if (currentStep.action?.fromChainId === 20000000000006) { await executeNearSwap(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) }