@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
text/typescript
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)
}