@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
text/typescript
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',
}
}
}