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.

275 lines (246 loc) 9.31 kB
import { ChainId } from '@openocean.finance/widget-sdk' import { Currency } from '../constants/index.js' import { WalletClient, formatUnits } from 'viem' import { CROSS_CHAIN_FEE_RECEIVER, ZERO_ADDRESS } from '../constants/index.js' import { Quote } from '../registry.js' import { BaseSwapAdapter, Chain, NonEvmChain, NormalizedQuote, NormalizedTxResponse, QuoteParams, SwapStatus, } from './BaseSwapAdapter.js' const OPTIMEX_API = 'https://ks-provider.optimex.xyz/v1' interface OptimexToken { id: number network_id: 'ethereum' | 'bitcoin' token_id: string network_name: string network_symbol: string network_type: 'EVM' | 'BTC' token_name: string token_symbol: string token_address: string token_decimals: number token_logo_uri: string network_logo_uri: string active: boolean } export class OptimexAdapter extends BaseSwapAdapter { private tokens: OptimexToken[] constructor() { super() this.tokens = [] } private async getTokens() { try { const res = await fetch(`${OPTIMEX_API}/tokens`) const { data } = await res.json() this.tokens = data.tokens } catch (error) { console.error('Failed to initialize Optimex tokens:', error) // Handle error appropriately } } getName(): string { return 'Optimex' } getIcon(): string { return 'https://storage.googleapis.com/ks-setting-1d682dca/464ce79e-a906-4590-bf78-9054e606aa041749023419612.png' } getSupportedChains(): Chain[] { return [NonEvmChain.Bitcoin, ChainId.ETH] } getSupportedTokens(_sourceChain: Chain, _destChain: Chain): Currency[] { return [] } async getQuote(params: QuoteParams): Promise<NormalizedQuote> { if (!this.tokens?.length) { await this.getTokens() } const isFromBtc = params.fromChain === NonEvmChain.Bitcoin const isToBtc = params.toChain === NonEvmChain.Bitcoin const fromToken = isFromBtc ? { token_id: 'BTC', token_symbol: 'BTC' } : this.tokens.find(item => { const address = (params.fromToken as any).isNative ? 'native' : (params.fromToken as any).wrapped.address return item.network_id === 'ethereum' && address.toLowerCase() === item.token_address.toLowerCase() }) const fromTokenId = fromToken?.token_id const toToken = isToBtc ? { token_id: 'BTC', token_symbol: 'BTC' } : this.tokens.find(item => { const address = (params.toToken as any).isNative ? 'native' : (params.toToken as any).wrapped.address return item.network_id === 'ethereum' && address.toLowerCase() === item.token_address.toLowerCase() }) const toTokenId = toToken?.token_id if (!fromTokenId || !toTokenId) { console.log('optimex tokens', this.tokens) throw new Error(`Optimex does not support ${!fromTokenId ? params.fromToken.symbol : params.toToken.symbol}`) } const [quoteRes, estimateRes, token0Usd, token1Usd] = await Promise.all([ fetch(`${OPTIMEX_API}/solver/indicative-quote`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ debug: false, from_token_amount: params.amount, from_token_id: fromTokenId, to_token_id: toTokenId, affiliate_fee_bps: params.feeBps.toString(), }), }).then(res => res.json()), fetch(`${OPTIMEX_API}/trades/estimate?from_token=${fromTokenId}&to_token=${toTokenId}`).then(res => res.json()), fetch(`https://api.optimex.xyz/v1/tokens/${fromToken.token_symbol}`) .then(res => res.json()) .then(res => res?.data?.current_price || 0), fetch(`https://api.optimex.xyz/v1/tokens/${toToken.token_symbol}`) .then(res => res.json()) .then(res => res?.data?.current_price || 0), ]) let txData: { deposit_address: string; payload?: string; trade_id: string } | null = null if (params.sender && params.recipient && (isFromBtc ? params.publicKey : true)) { const tradeTimeout = new Date() tradeTimeout.setHours(tradeTimeout.getHours() + 2) const scriptTimeout = new Date() scriptTimeout.setHours(scriptTimeout.getHours() + 24) const res = await fetch(`${OPTIMEX_API}/trades/initiate`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ session_id: quoteRes.data.session_id, from_user_address: params.sender, amount_in: params.amount, min_amount_out: ( (BigInt(quoteRes.data.best_quote_after_fees) * (10_000n - BigInt(params.slippage))) / 10_000n ).toString(), to_user_address: params.recipient, user_refund_pubkey: params.fromChain === NonEvmChain.Bitcoin ? params.publicKey : params.sender, user_refund_address: params.sender, creator_public_key: params.fromChain === NonEvmChain.Bitcoin ? params.publicKey : params.sender, from_wallet_address: params.sender, trade_timeout: Math.floor(tradeTimeout.getTime() / 1000), script_timeout: Math.floor(scriptTimeout.getTime() / 1000), affiliate_info: [ { provider: 'KyberSwap', rate: params.feeBps.toString(), receiver: CROSS_CHAIN_FEE_RECEIVER, network: 'ethereum', }, ], }), }).then(res => res.json()) if (res.data.deposit_address) { txData = res.data } } const formattedOutputAmount = formatUnits(BigInt(quoteRes.data.best_quote_after_fees), params.toToken.decimals) const formattedInputAmount = formatUnits(BigInt(params.amount), params.fromToken.decimals) const inputUsd = token0Usd * +formattedInputAmount const outputUsd = token1Usd * +formattedOutputAmount return { quoteParams: params, outputAmount: BigInt(quoteRes.data.best_quote_after_fees), formattedOutputAmount, inputUsd, outputUsd, priceImpact: !inputUsd || !outputUsd ? NaN : ((inputUsd - outputUsd) * 100) / inputUsd, rate: +formattedOutputAmount / +formattedInputAmount, gasFeeUsd: 0, timeEstimate: estimateRes.data.estimated_time, contractAddress: txData?.deposit_address || ZERO_ADDRESS, rawQuote: { ...quoteRes.data, txData }, protocolFee: 0, platformFeePercent: (params.feeBps * 100) / 10000, } } async executeSwap( { quote }: Quote, walletClient: WalletClient, _nearWallet: any, sendBtcFn?: (params: { recipient: string; amount: string | number }) => Promise<string>, ): Promise<NormalizedTxResponse> { const params = { sender: quote.quoteParams.sender, id: quote.rawQuote.txData.trade_id, adapter: this.getName(), sourceChain: quote.quoteParams.fromChain, targetChain: quote.quoteParams.toChain, inputAmount: quote.quoteParams.amount, outputAmount: quote.outputAmount.toString(), sourceToken: quote.quoteParams.fromToken, targetToken: quote.quoteParams.toToken, timestamp: new Date().getTime(), } if (quote.quoteParams.fromChain === NonEvmChain.Bitcoin) { if (!sendBtcFn) throw new Error('sendBtcFn is not defined') const res = await sendBtcFn({ recipient: quote.rawQuote.txData.deposit_address, amount: quote.quoteParams.amount, }).catch(e => { throw e }) await fetch(`${OPTIMEX_API}/trades/${quote.rawQuote.txData.trade_id}/submit-tx`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ tx_id: res, }), }).catch(e => { console.log('submit tx error for optimex', e) }) return { ...params, sourceTxHash: res, } } if (!walletClient || !walletClient.account) throw new Error('Not connected') const account = walletClient.account?.address as `0x${string}` const hash = await walletClient.sendTransaction({ to: quote.rawQuote.txData.deposit_address, value: (quote.quoteParams.fromToken as any).isNative ? BigInt(quote.quoteParams.amount) : undefined, data: quote.rawQuote.txData.payload, chain: undefined, account, kzg: undefined, }) await fetch(`${OPTIMEX_API}/trades/${quote.rawQuote.txData.trade_id}/submit-tx`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ tx_id: hash, }), }).catch(e => { console.log('submit tx error for optimex', e) }) return { ...params, sourceTxHash: hash, } } async getTransactionStatus(p: NormalizedTxResponse): Promise<SwapStatus> { const res = await fetch(`${OPTIMEX_API}/trades/${p.id}`).then(res => res.json()) return { txHash: res.data?.payment_bundle?.settlement_tx || '', status: ['Done', 'PaymentConfirmed'].includes(res?.data?.state) ? 'Success' : ['Aborted', 'ToBeAborted', 'Failed', 'Failure', 'UserCancelled'].includes(res?.data?.state) ? 'Failed' : res?.data?.state === 'Refunded' ? 'Refunded' : 'Processing', } } }