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