@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.
267 lines (254 loc) • 8.03 kB
text/typescript
import type { WalletClient } from 'viem'
import type { QuoteParams } from './adapters/index.js'
import { CrossChainSwapFactory } from './factory.js'
import { CrossChainSwapAdapterRegistry, type Quote } from './registry.js'
/**
* Get cross-chain aggregated quotes (supports multiple adapter comparison)
* @param params - Cross-chain quote parameters
* @returns Best quote result, compatible with useRoutes.ts requirements
*/
export async function getCrossChainQuote({
fromMsg,
toMsg,
inAmount,
slippage_tolerance,
account,
recipient,
tokenInUsd = 0,
tokenOutUsd = 0,
feeBps = 10,
walletClient,
publicKey,
nearTokens = [],
}: {
fromMsg: any
toMsg: any
inAmount: string
slippage_tolerance: string | number
account: string
recipient?: string
tokenInUsd?: number
tokenOutUsd?: number
feeBps?: number
walletClient?: any
publicKey?: string
nearTokens?: any[]
}): Promise<any | null> {
try {
// 1. Register all Adapters
const registry = new CrossChainSwapAdapterRegistry()
CrossChainSwapFactory.getAllAdapters().forEach((adapter) => {
registry.registerAdapter(adapter)
})
let slippage = typeof slippage_tolerance === 'string'
? Number.parseFloat(slippage_tolerance)
: slippage_tolerance
slippage = Math.floor(slippage * 10000) / 100
// 2. Construct QuoteParams
const params: QuoteParams = {
feeBps,
fromChain: fromMsg.chainId,
toChain: toMsg.chainId,
fromToken: fromMsg,
toToken: toMsg,
amount: inAmount,
slippage: slippage,
walletClient,
tokenInUsd,
tokenOutUsd,
sender: account,
recipient: recipient || '',
publicKey,
isNative: fromMsg.isNative || toMsg.isNative,
}
if (fromMsg.chainId === 'near' || toMsg.chainId === 'near') {
; (params as any).nearTokens = nearTokens
}
// 3. Get all quotes
const adapters = registry.getAllAdapters().filter((adapter) => {
return (
adapter.getSupportedChains().includes(params.fromChain) &&
adapter.getSupportedChains().includes(params.toChain)
)
})
if (adapters.length === 0) {
console.warn('No supported adapters found for the specified chains')
return null
}
const quoteResults: Quote[] = []
let errorMsg = ''
// Get quotes from all adapters in parallel
const quotePromises = adapters.map(async (adapter) => {
try {
const quote = await adapter.getQuote(params)
if (quote) {
quoteResults.push({ adapter, quote })
}
} catch (error) {
errorMsg = error instanceof Error ? error.message : (error && typeof error === 'object' && 'message' in error ? String(error.message) : 'Unknown error')
// Ignore individual adapter errors, continue getting quotes from other adapters
console.warn(`Failed to get quote from ${adapter.getName()}:`, error)
}
})
await Promise.all(quotePromises)
if (quoteResults.length === 0) {
console.warn('No valid quotes found from any adapter')
throw new Error(errorMsg)
// return { error: errorMsg }
}
// 4. Select best quote (highest outputAmount)
quoteResults.sort((a, b) =>
a.quote.outputAmount < b.quote.outputAmount ? 1 : -1
)
const best = quoteResults[0]
// 5. Construct return structure compatible with useRoutes.ts
return {
data: {
minOutAmount: best.quote.outputAmount || '0',
outAmount: best.quote.outputAmount || '0',
fromTokenUSD: best.quote.inputUsd?.toString() || '0',
toTokenUSD: best.quote.outputUsd?.toString() || '0',
quoteAdapterName: best.adapter?.getName() || '',
quoteAdapterKey: best.adapter?.getName()?.toLowerCase() || '',
chainId: fromMsg.chainId,
from: account,
to: best.quote.contractAddress || '0x0',
value:
best.quote.rawQuote.txParams?.value ||
best.quote.rawQuote.transactionRequest?.value ||
best.quote.rawQuote.tx?.value ||
'0x0',
data:
best.quote.rawQuote.txParams?.data ||
best.quote.rawQuote.transactionRequest?.data ||
best.quote.rawQuote.tx?.data ||
'0x',
// gasLimit: best.quote.rawQuote.tx?.gasLimit || '0',
// gas: best.quote.rawQuote.tx?.gas || '0',
// nonce: best.quote.rawQuote.tx?.nonce || '0',
approveContract: best.quote.contractAddress || '0x0',
executionDuration: best.quote.timeEstimate || 300,
// Fee information
feeCosts: [
{
name: `${best.adapter?.getName() || ''} Fee`,
description: 'Protocol fee',
token: fromMsg,
amount: '0',
amountUSD: best.quote.protocolFee,
percentage: '0',
included: false,
},
],
// Original quote data
quoteRawData: best.quote,
},
}
} catch (error) {
console.error('Error getting cross-chain quote:', error)
let errors = [
{
message: 'Please connect your wallet first.',
conditions: [
'refundTo should not be empty',
'User is required',
'fromAddress" is missing'
],
},
{
message: 'The input amount is too low.',
conditions: [
'Swap output amount is too small to cover fees required to execute swap',
'Amount is too low for bridge',
'Amount too small',
'Amount too low',
],
},
{
message: 'Insufficient balance in your wallet.',
conditions: [
'Insufficient funds',
'Failed to get quote',
'Insufficient balance',
],
},
{
message: 'Please enter valid recipient address.',
conditions: [
'recipient should not be empty',
'Recipient is required',
'Invalid toAddress:'
],
},
{
message: 'Not supported token',
conditions: [
'not supported to token',
'not supported from token',
'no routes found',
'SDK version too old for monad and solana fast mctp route'
],
},
{
message: 'Pegasus API key is invalid or missing.',
conditions: [
'Pegasus API key is missing',
'Invalid API key',
'Missing API key',
'UNAUTHORIZED',
],
},
{
message: 'There’s no routes available for the selected token pairs',
conditions: [
'No available quotes for the requested transfer',
],
}
]
if (error instanceof Error && error.message) {
const newError = errors.find(e => e.conditions.some(condition => error.message.indexOf(condition) !== -1))
if (newError) {
throw new Error(newError.message)
} else {
throw new Error(error.message)
}
}
throw new Error('Unknown error')
}
}
export const bridgeExecuteSwap = async ({
quoteData,
walletClient,
nearWallet,
}: {
quoteData: any
walletClient: WalletClient
// Near Intents adapter 需要的 Near wallet-selector 实例,其它适配器会忽略
nearWallet?: any
}) => {
const adapterName = quoteData.quoteAdapterKey
const adapter = CrossChainSwapFactory.getAdapterByName(adapterName)
if (adapter) {
const result = await (adapter as any)?.executeSwap(
{
adapter,
quote: quoteData.quoteRawData as any,
},
walletClient,
nearWallet
)
return result
}
return null
}
// Login URL mapping for each adapter
export const ADAPTER_LOGIN_URLS: Record<string, string> = {
debridge: 'https://app.debridge.finance/',
across: 'https://across.to/',
lifi: 'https://jumper.exchange/',
mayan: 'https://swap.mayan.finance/',
relay: 'https://app.relay.link/',
xyfinance: 'https://xy.finance/',
kyberswap: 'https://kyberswap.com/',
pegasus: 'https://pegasusfi.openocean.finance/',
}