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.

347 lines 15.7 kB
export class OpenOceanService { // Get OpenOcean supported chain name static getChainName(chainId) { return this.CHAIN_ID_MAP[chainId] || chainId.toString(); } // Get API URL based on chain ID static getApiUrl(chainId) { // 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}`; } static getSolanaAddress(chain, tokenAddress) { if (chain === '1151111081099710' && tokenAddress === 'So11111111111111111111111111111111111111112') { return '11111111111111111111111111111111'; } else if (chain === '1151111081099710' && tokenAddress === '11111111111111111111111111111111') { return 'So11111111111111111111111111111111111111112'; } return tokenAddress; } static async parseApiResponse(response, fallbackMessage) { 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) { 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) { 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 = { 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) { 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) => { 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) { const apiUrl = this.getApiUrl(chain); const response = await fetch(`${apiUrl}/dexList`); return response.json(); } static async getGasPrice(chain) { 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, tokenAddress) { 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, tokenAddresses) { 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, token) => { 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; } } // private static readonly API_V1_URL = 'https://ethapi.openocean.finance/v1' //private static readonly API_V2_URL = 'https://ethapi.openocean.finance/v2' OpenOceanService.API_V3_URL = 'https://open-api.openocean.finance/v3'; OpenOceanService.API_V4_URL = 'https://open-api.openocean.finance/v4'; OpenOceanService.solanaRpcUrl = ''; // Chain ID to OpenOcean chain name mapping OpenOceanService.CHAIN_ID_MAP = { 1151111081099710: 'solana', // Solana mainnet 20000000000001: 'bitcoin', // Bitcoin mainnet 20000000000006: 'near', // Near mainnet }; OpenOceanService.getRpcUrl().then((rpcUrl) => { }); //# sourceMappingURL=OpenOceanService.js.map