@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
JavaScript
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