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.

272 lines (243 loc) 9.87 kB
import { Currency } from '../constants/index.js' import { useWalletSelector } from '@near-wallet-selector/react-hook' import { WalletAdapterProps } from '@solana/wallet-adapter-base' import { Connection, Transaction, VersionedTransaction } from '@solana/web3.js' import { getPublicClient } from 'wagmi/actions' import { WalletClient, formatUnits } from 'viem' import { useConfig } from 'wagmi' import { CROSS_CHAIN_FEE_RECEIVER, CROSS_CHAIN_FEE_RECEIVER_SOLANA, ZERO_ADDRESS } from '../constants/index.js' import { MAINNET_NETWORKS } from '../constants/index.js' import { SolanaToken } from '../constants/index.js' import { Quote } from '../registry.js' import { BaseSwapAdapter, Chain, NOT_SUPPORTED_CHAINS_PRICE_SERVICE, NormalizedQuote, NormalizedTxResponse, QuoteParams, SwapStatus, } from './BaseSwapAdapter.js' interface Step { action: string tx: { data: string to: string value: string } } export class OrbiterAdapter extends BaseSwapAdapter { constructor() { super() } getName(): string { return 'Orbiter' } getIcon(): string { return 'https://www.orbiter.finance/favicon.ico' } getSupportedChains(): Chain[] { return [...MAINNET_NETWORKS] } getSupportedTokens(_sourceChain: Chain, _destChain: Chain): Currency[] { return [] } async getQuote(params: QuoteParams): Promise<NormalizedQuote> { const fromToken = params.fromToken as any const toToken = params.toToken as any const body = { sourceChainId: params.fromChain === 1151111081099710 ? 'SOLANA_MAIN' : params.fromChain.toString(), destChainId: params.toChain === 1151111081099710 ? 'SOLANA_MAIN' : params.toChain.toString(), sourceToken: params.fromChain === 1151111081099710 ? (params.fromToken as SolanaToken).id : fromToken.isNative ? ZERO_ADDRESS : fromToken.address, destToken: params.toChain === 1151111081099710 ? (params.toToken as SolanaToken).id : toToken.isNative ? ZERO_ADDRESS : toToken.address, amount: params.amount.toString(), userAddress: params.sender, targetRecipient: params.recipient, slippage: params.slippage / 10_000, feeConfig: { feeRecipient: params.fromChain === 1151111081099710 ? CROSS_CHAIN_FEE_RECEIVER_SOLANA : CROSS_CHAIN_FEE_RECEIVER, feePercent: (params.feeBps / 10000).toString(), }, channel: 'kyberswap', } const res = await fetch(`https://api.orbiter.finance/quote`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(body), }) .then(res => res.json()) .then(res => res.result) const formattedOutputAmount = formatUnits(BigInt(res.details?.destTokenAmount || '0'), params.toToken.decimals) const formattedInputAmount = formatUnits(BigInt(params.amount), params.fromToken.decimals) const inputUsd = NOT_SUPPORTED_CHAINS_PRICE_SERVICE.includes(params.fromChain) ? Number(res.details?.sourceAmountUSD || 0) : params.tokenInUsd * +formattedInputAmount const outputUsd = NOT_SUPPORTED_CHAINS_PRICE_SERVICE.includes(params.toChain) ? Number(res.details?.destAmountUSD || 0) : params.tokenOutUsd * +formattedOutputAmount const haveApproval = res.steps.some((step: Step) => step.action === 'approve') const approvalContract = res.steps.find((step: Step) => step.action === 'swap' || step.action === 'bridge') return { quoteParams: params, outputAmount: BigInt(res.details?.destTokenAmount || '0'), formattedOutputAmount, inputUsd, outputUsd, priceImpact: !inputUsd || !outputUsd ? NaN : ((inputUsd - outputUsd) * 100) / inputUsd, //rate: Number(resp.details?.rate || 0), rate: +formattedOutputAmount / +formattedInputAmount, gasFeeUsd: 0, timeEstimate: 10, contractAddress: haveApproval ? approvalContract?.tx.to || ZERO_ADDRESS : ZERO_ADDRESS, rawQuote: res, protocolFee: 0, platformFeePercent: (params.feeBps * 100) / 10000, } } async executeSwap( { quote }: Quote, walletClient: WalletClient, _nearWallet?: ReturnType<typeof useWalletSelector>, _sendBtcFn?: (params: { recipient: string; amount: string | number }) => Promise<string>, sendSolanaFn?: WalletAdapterProps['sendTransaction'], solanaConnection?: Connection, ): Promise<NormalizedTxResponse> { if (quote.quoteParams.fromChain === 1151111081099710) { if (!solanaConnection || !sendSolanaFn) throw new Error('Connection is not defined for Solana swap') const encodedData = quote.rawQuote.steps?.[0]?.tx?.data const txBuffer = Buffer.from(encodedData, 'base64') // Try to deserialize as VersionedTransaction first let transaction try { transaction = VersionedTransaction.deserialize(txBuffer) console.log('Parsed as VersionedTransaction') } catch (versionedError) { console.log('Failed to parse as VersionedTransaction, trying legacy Transaction') try { transaction = Transaction.from(txBuffer) console.log('Parsed as legacy Transaction') } catch (legacyError) { throw new Error('Could not parse transaction as either VersionedTransaction or legacy Transaction') } } console.log('Transaction parsed successfully:', transaction) const waitForConfirmation = async (txId: string) => { try { const latestBlockhash = await solanaConnection.getLatestBlockhash() // Wait for confirmation with timeout const confirmation = await Promise.race([ solanaConnection.confirmTransaction( { signature: txId, blockhash: latestBlockhash.blockhash, lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, }, 'confirmed', ), new Promise((_, reject) => setTimeout(() => reject(new Error('Transaction confirmation timeout')), 60000)), ]) const confirmationResult = confirmation as { value: { err: any } } if (confirmationResult.value.err) { throw new Error(`Transaction failed: ${JSON.stringify(confirmationResult.value.err)}`) } console.log('Transaction confirmed successfully!') } catch (confirmError) { console.error('Transaction confirmation failed:', confirmError) // Check if transaction actually succeeded despite timeout const txStatus = await solanaConnection.getSignatureStatus(txId) if (txStatus?.value?.confirmationStatus !== 'confirmed') { throw new Error(`Transaction was not confirmed: ${confirmError.message}`) } } } // Send through wallet adapter const signature = await sendSolanaFn(transaction, solanaConnection) await waitForConfirmation(signature) return { sender: quote.quoteParams.sender, id: signature, sourceTxHash: signature, 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(), } } const steps = quote.rawQuote.steps.filter((st: Step) => st.action !== 'approve') // already approve before const account = walletClient.account?.address if (!account) throw new Error('WalletClient account is not defined') const txs = await Promise.all( steps.map(async (step: Step) => { const tx = await walletClient.sendTransaction({ chain: undefined, account, to: step.tx.to as `0x${string}`, value: BigInt(step.tx.value), data: step.tx.data as `0x${string}`, kzg: undefined, }) return tx }), ) if (!txs || txs.length === 0) throw new Error('No transactions found after executing swap steps') return { sender: quote.quoteParams.sender, sourceTxHash: txs[txs.length - 1], adapter: this.getName(), id: txs[txs.length - 1], 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(), } } async getTransactionStatus(p: NormalizedTxResponse): Promise<SwapStatus> { if (p.sourceChain !== 1151111081099710) { const publicClient = getPublicClient(useConfig(), { chainId: p.sourceChain as any, }) const receipt = await publicClient?.getTransactionReceipt({ hash: p.id as `0x${string}`, }) if (receipt.status === 'reverted') { return { txHash: '', status: 'Failed', } } } const res = await fetch(`https://api.orbiter.finance/transaction/${p.id}`).then(r => r.json()) return { txHash: res.result.targetId || '', // this is from orbiter source code, their docs dont have info for this // https://github.com/Orbiter-Finance/OrbiterFE-V2/blob/2b35399aad581e666c45a829e0151485f4007c93/src/views/statistics/LatestTransactions.vue#L115 status: res.result.opStatus === -1 ? 'Failed' : res.result.opStatus === 80 ? 'Refunded' : res.result.opStatus !== 98 && res.result.opStatus !== 99 ? 'Processing' : 'Success', } } }