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.

426 lines (392 loc) 14.9 kB
interface OpenOceanToken { address: string symbol: string decimals: number name: string icon?: string usd?: string chainId?: number } export class OpenOceanService { // private static readonly API_V1_URL = 'https://ethapi.openocean.finance/v1' //private static readonly API_V2_URL = 'https://ethapi.openocean.finance/v2' private static readonly API_V3_URL = 'https://open-api.openocean.finance/v3' private static readonly API_V4_URL = 'https://open-api.openocean.finance/v4' static solanaRpcUrl = '' // Chain ID to OpenOcean chain name mapping private static readonly CHAIN_ID_MAP: Record<string | number, string> = { 1151111081099710: 'solana', // Solana mainnet 20000000000001: 'bitcoin', // Bitcoin mainnet 20000000000006: 'near', // Near mainnet } // Get OpenOcean supported chain name private static getChainName(chainId: string | number): string { return this.CHAIN_ID_MAP[chainId] || chainId.toString() } // Get API URL based on chain ID private static getApiUrl(chainId: string | number): string { // If chainId exists in CHAIN_ID_MAP, use V1 API const chainName = this.getChainName(chainId) return Object.keys(this.CHAIN_ID_MAP).includes(chainId.toString()) ? `${this.API_V4_URL}/${chainName}` : `${this.API_V4_URL}/${chainId}` } private static getSolanaAddress(chain: string, tokenAddress: string): string { if (chain === '1151111081099710' && tokenAddress === 'So11111111111111111111111111111111111111112') { return '11111111111111111111111111111111' } else if (chain === '1151111081099710' && tokenAddress === '11111111111111111111111111111111') { return 'So11111111111111111111111111111111111111112' } return tokenAddress } private static async parseApiResponse( response: Response, fallbackMessage: string ) { const data = await response.json() if (!response.ok) { const message = (typeof data?.error === 'string' && data.error.trim()) || (typeof data?.message === 'string' && data.message.trim()) || fallbackMessage throw new Error(message) } if (data?.code !== undefined && data.code !== 200) { const message = (typeof data?.error === 'string' && data.error.trim()) || (typeof data?.message === 'string' && data.message.trim()) || fallbackMessage throw new Error(message) } return data } static async getQuote(params: { chain: string inTokenAddress: string inTokenSymbol: string outTokenAddress: string outTokenSymbol: string amount: string slippage?: string gasPrice?: string disabledDexIds?: string enabledDexIds?: string referrer?: string account?: string inTokenDecimals?: number outTokenDecimals?: number }) { const isNearChain = params.chain === '20000000000006' || params.chain === 'near' // Near 链使用特殊的 API 端点和参数格式 if (isNearChain) { const nearApiUrl = 'https://ethapi.openocean.finance/v1/near' // 计算 amountAll(格式化后的金额,用于显示) let amountAll = '' if (params.inTokenDecimals !== undefined) { const amountBigInt = BigInt(params.amount) const decimals = BigInt(10 ** params.inTokenDecimals) const wholePart = amountBigInt / decimals const fractionalPart = amountBigInt % decimals if (fractionalPart === 0n) { amountAll = wholePart.toString() } else { const fractionalStr = fractionalPart.toString().padStart(params.inTokenDecimals, '0') // 移除尾部的 0,但保留至少一位小数 const trimmedFractional = fractionalStr.replace(/0+$/, '') || '0' amountAll = `${wholePart}.${trimmedFractional}` } } // 转换 slippage:从 0.01 (1%) 转为 100 (百分比格式) const slippagePercent = params.slippage ? Math.floor(Number(params.slippage) * 100).toString() : '100' // 默认 1% const queryParams = new URLSearchParams({ quoteType: 'swap', inTokenSymbol: params.inTokenSymbol, inTokenAddress: params.inTokenAddress, outTokenSymbol: params.outTokenSymbol, outTokenAddress: params.outTokenAddress, amount: params.amount, ...(amountAll && { amountAll }), slippage: slippagePercent, gasPrice: params.gasPrice || '5000000000', referrer: params.referrer || '0x3487ef9f9b36547e43268b8f0e2349a226c70b53', disabledDexIds: params.disabledDexIds || '', disableRfq: '', // Near API 特有参数 ...(params.account && { account: params.account }), }) const response = await fetch(`${nearApiUrl}/quote?${queryParams.toString()}`) const data = await this.parseApiResponse( response, 'Failed to fetch quote' ) return { data, } } // 其他链使用原有逻辑 const apiUrl = this.getApiUrl(params.chain) const slippage = (Number(params.slippage || 0.01)).toString(); const queryParams = new URLSearchParams({ quoteType: 'quote', inTokenSymbol: params.inTokenSymbol, inTokenAddress: this.getSolanaAddress(params.chain, params.inTokenAddress), outTokenSymbol: params.outTokenSymbol, outTokenAddress: this.getSolanaAddress(params.chain, params.outTokenAddress), amountDecimals: params.amount, slippage, gasPriceDecimals: params.gasPrice || '', disabledDexIds: params.disabledDexIds || '', enabledDexIds: params.enabledDexIds || '', referrer: params.referrer || '0x3487ef9f9b36547e43268b8f0e2349a226c70b53', }) const response = await fetch(`${apiUrl}/quote?${queryParams.toString()}`) return this.parseApiResponse(response, 'Failed to fetch quote') } static async getSwapQuote(params: { chain: string inTokenAddress: string inTokenSymbol: string outTokenAddress: string outTokenSymbol: string amount: string account: string slippage?: string gasPrice?: string disabledDexIds?: string enabledDexIds?: string referrer?: string, referrerFee?: string, referrerFeeShare?: string inTokenDecimals?: number }) { const isNearChain = params.chain === '20000000000006' || params.chain === 'near' // Near 链使用特殊的 API 端点和参数格式 if (isNearChain) { const nearApiUrl = 'https://ethapi.openocean.finance/v1/near' // 计算 amountAll(格式化后的金额,用于显示) let amountAll = '' if (params.inTokenDecimals !== undefined) { const amountBigInt = BigInt(params.amount) const decimals = BigInt(10 ** params.inTokenDecimals) const wholePart = amountBigInt / decimals const fractionalPart = amountBigInt % decimals if (fractionalPart === 0n) { amountAll = wholePart.toString() } else { const fractionalStr = fractionalPart.toString().padStart(params.inTokenDecimals, '0') // 移除尾部的 0,但保留至少一位小数 const trimmedFractional = fractionalStr.replace(/0+$/, '') || '0' amountAll = `${wholePart}.${trimmedFractional}` } } // 转换 slippage:从 0.01 (1%) 转为 100 (百分比格式) const slippagePercent = params.slippage ? Math.floor(Number(params.slippage) * 100).toString() : '100' // 默认 1% const queryParams = new URLSearchParams({ quoteType: 'swap', inTokenSymbol: params.inTokenSymbol, inTokenAddress: params.inTokenAddress, outTokenSymbol: params.outTokenSymbol, outTokenAddress: params.outTokenAddress, amount: params.amount, ...(amountAll && { amountAll }), gasPrice: params.gasPrice || '5000000000', disabledDexIds: params.disabledDexIds || '', slippage: slippagePercent, account: params.account, referrer: params.referrer || '0x3487ef9f9b36547e43268b8f0e2349a226c70b53', flags: '0', // Near API 特有参数 disableRfq: '', // Near API 特有参数 }) const response = await fetch(`${nearApiUrl}/swap-quote?${queryParams.toString()}`) const data = await this.parseApiResponse( response, 'Failed to fetch swap quote' ) return { data: { ...data, data: data.transaction, price_impact: 0 } } } // 其他链使用原有逻辑 const apiUrl = this.getApiUrl(params.chain) const slippage = (Number(params.slippage || 0.01)).toString(); const referrer: any = { referrer: params.referrer || '0x3487ef9f9b36547e43268b8f0e2349a226c70b53', } if (params.referrerFee) { referrer.referrerFee = Number(params.referrerFee); // referrer.referrerFeeShare = params.referrerFeeShare || '1500'; } const queryParams = new URLSearchParams({ quoteType: 'swap', inTokenSymbol: params.inTokenSymbol, inTokenAddress: this.getSolanaAddress(params.chain, params.inTokenAddress), outTokenSymbol: params.outTokenSymbol, outTokenAddress: this.getSolanaAddress(params.chain, params.outTokenAddress), amountDecimals: params.amount, account: params.account, slippage, gasPriceDecimals: params.gasPrice || '', disabledDexIds: params.disabledDexIds || '', enabledDexIds: params.enabledDexIds || '', ...referrer, }) // const isV1Api = Object.keys(this.CHAIN_ID_MAP).includes(params.chain.toString()) // const swapEndpoint = isV1Api ? 'swap-quote' : 'swap' const response = await fetch(`${apiUrl}/swap?${queryParams.toString()}`) return this.parseApiResponse(response, 'Failed to fetch swap quote') } static async getTokenList(chain: string) { const chainName = this.getChainName(chain) const response = await fetch(`${this.API_V4_URL}/${chainName}/tokenList`) const data = await response.json() if (data.code !== 200) { if (chain === '20000000000001') { data.data = [ { address: 'bitcoin', symbol: 'BTC', decimals: 8, isNative: true, name: 'Bitcoin', icon: 'https://assets.coingecko.com/coins/images/1/standard/bitcoin.png', usd: '109559', } ] } else { throw new Error('Failed to fetch token list') } } return data.data.map((token: OpenOceanToken) => { let address = token.address // Convert Solana native token address if (chain === '1151111081099710' && address === 'So11111111111111111111111111111111111111112') { address = '11111111111111111111111111111111' } // Convert Base chain native token address if (address === '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE') { address = '0x0000000000000000000000000000000000000000' } return { address, symbol: token.symbol, decimals: token.decimals, name: token.name, logoURI: token.icon, priceUSD: token.usd, chainId: Number(chain), amount: 0n, featured: false, popular: false } }) } static async getDexList(chain: string) { const apiUrl = this.getApiUrl(chain) const response = await fetch(`${apiUrl}/dexList`) return response.json() } static async getGasPrice(chain: string) { if (!chain || chain === '1151111081099710' || chain === '20000000000001' || chain === '20000000000006') { return { data: { gasPrice: '1000000000000000000', }, } } // const apiUrl = this.getApiUrl(chain) const response = await fetch(`${this.API_V4_URL}/${chain}/gasPrice`) const { data } = await response.json() if (chain == '1') { return { standard: data?.standard?.maxFeePerGas || '101021713', instant: data?.instant?.maxFeePerGas || '101021713', fast: data?.fast?.maxFeePerGas || '101021713', } } return data } static async getTokenInfo(chain: string, tokenAddress: string) { const chainName = this.getChainName(chain) // Check if the address is valid if (!tokenAddress || (!/^0x[a-fA-F0-9]{40}$/.test(tokenAddress) && !/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(tokenAddress))) { throw new Error('Invalid token address') } const response = await fetch(`${this.API_V4_URL}/${chainName}/getTokenInfo?tokenAddress=${tokenAddress}`) const data = await response.json() if (!data || !data.address || !data.symbol || !data.decimals) { throw new Error('Failed to fetch token info') } return { address: data.address, symbol: data.symbol, decimals: data.decimals, name: data.name, logoURI: data.icon, priceUSD: data.usd, chainId: Number(chain), amount: 0n, featured: false, popular: false } } /** * Get prices for specified tokens * @param chain chain name, e.g. 'arbitrum' * @param tokenAddresses array of token addresses * @returns Promise<Record<string, string>> returns an object where key is token address and value is price */ static async getTokensPrice(chain: string, tokenAddresses: string[]): Promise<Record<string, string>> { let chainName = this.getChainName(chain) if (chain === '20000000000001') { tokenAddresses = ['0x2260fac5e5542a773aa44fbcfedf7c193bc2c599'] chainName = '1' } const tokenAddressesStr = tokenAddresses.join(',') const response = await fetch(`${this.API_V3_URL}/${chainName}/designated_tokenList?tokens=${tokenAddressesStr}`) const data = await response.json() if (data.code !== 200) { throw new Error('Failed to fetch token prices') } const prices = data.data.reduce((acc: Record<string, string>, token: OpenOceanToken) => { if (token.address && token.usd) { if (chain === '20000000000001') { acc.bitcoin = token.usd } else { acc[token.address.toLowerCase()] = token.usd } } return acc }, {}) return prices } static async getRpcUrl() { if (this.solanaRpcUrl) { return this.solanaRpcUrl } let url = `${this.API_V3_URL}/solana/getRpc` const response = await fetch(url) const data = await response.json() if (data.data?.openapi_v1) { let rpcUrl = data.data?.openapi_v1?.[0] || '' this.solanaRpcUrl = rpcUrl } if (data.data?.openapi_v2) { let rpcUrl = data.data?.openapi_v2?.[0] || '' this.solanaRpcUrl = rpcUrl } if (data.data?.openapi_v3) { let rpcUrl = data.data?.openapi_v3?.[0] || '' this.solanaRpcUrl = rpcUrl } return this.solanaRpcUrl } } OpenOceanService.getRpcUrl().then((rpcUrl) => { })