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.

376 lines (333 loc) 11.5 kB
import type { useWalletSelector } from '@near-wallet-selector/react-hook' import { ChainId } from '@openocean.finance/widget-sdk' import type { WalletAdapterProps } from '@solana/wallet-adapter-base' import { type Connection, Transaction, VersionedTransaction, } from '@solana/web3.js' import { type WalletClient, formatUnits } from 'viem' import type { Currency } from '../constants/index.js' import { TOKEN_API_URL } from '../constants/index.js' import { CROSS_CHAIN_FEE_RECEIVER, CROSS_CHAIN_FEE_RECEIVER_SOLANA, ZERO_ADDRESS, } from '../constants/index.js' import { NativeCurrencies } from '../constants/index.js' import type { SolanaToken } from '../constants/index.js' import type { Quote } from '../registry.js' import { BaseSwapAdapter, type Chain, NOT_SUPPORTED_CHAINS_PRICE_SERVICE, NonEvmChain, type NormalizedQuote, type NormalizedTxResponse, type QuoteParams, type SwapStatus, } from './BaseSwapAdapter.js' const DEBRIDGE_API = 'https://dln.debridge.finance/v1.0/dln/order' const mappingChainId: Record<string, number> = { [ChainId.DAI]: 100000002, [ChainId.MAM]: 100000004, // Metis [ChainId.SON]: 100000014, // Sonic [ChainId.ABS]: 100000017, // Abstract [ChainId.BER]: 100000020, // Berachain [ChainId.BOB]: 100000021, // BOB [ChainId.MNT]: 100000023, // Mantle [NonEvmChain.Solana]: 7565164, } export class DeBridgeAdapter extends BaseSwapAdapter { constructor() { super() } getName(): string { return 'deBridge' } getIcon(): string { return 'https://app.debridge.finance/assets/images/meta-deswap/favicon-32x32.png' } getSupportedChains(): Chain[] { return [ ChainId.ETH, ChainId.BSC, ChainId.POL, ChainId.AVA, ChainId.ARB, ChainId.OPT, ChainId.ONE, ChainId.FSN, ChainId.MOR, ChainId.CEL, ChainId.FUS, ChainId.TLO, ChainId.CRO, ChainId.BOB, ChainId.RSK, ChainId.VEL, ChainId.MOO, ChainId.MAM, ChainId.AUR, ChainId.EVM, ChainId.ARN, ChainId.ERA, ChainId.PZE, ChainId.LNA, ChainId.BAS, ChainId.SCL, ChainId.MOD, ChainId.MNT, ChainId.BLS, ChainId.SEI, ChainId.FRA, ChainId.TAI, ChainId.GRA, ChainId.IMX, ChainId.KAI, ChainId.XLY, // NonEvmChain.Solana, ] } 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 let p: Record<string, string | boolean | number> = { srcChainId: mappingChainId[params.fromChain] || params.fromChain, srcChainTokenIn: params.fromChain === 1151111081099710 ? (params.fromToken as SolanaToken).id : fromToken.isNative ? ZERO_ADDRESS : fromToken.address, srcChainTokenInAmount: params.amount, dstChainId: mappingChainId[params.toChain] || params.toChain, dstChainTokenOut: params.toChain === 1151111081099710 ? (params.toToken as SolanaToken).id : toToken.isNative ? ZERO_ADDRESS : toToken.address, dstChainTokenOutAmount: 'auto', enableEstimate: false, prependOperatingExpenses: false, referralCode: 31982, affiliateFeePercent: (params.feeBps * 100) / 10_000, affiliateFeeRecipient: params.fromChain === 1151111081099710 ? CROSS_CHAIN_FEE_RECEIVER_SOLANA : CROSS_CHAIN_FEE_RECEIVER, } let path = 'quote' if (params.recipient && params.sender && params.sender !== ZERO_ADDRESS) { path = 'create-tx' p = { ...p, srcChainOrderAuthorityAddress: params.sender, dstChainOrderAuthorityAddress: params.recipient, dstChainTokenOutRecipient: params.recipient, } } // Convert the parameters object to URL query string const queryParams = new URLSearchParams() for (const [key, value] of Object.entries(p)) { queryParams.append(key, String(value)) } const r = await fetch( `${DEBRIDGE_API}/${path}?${queryParams.toString()}` ).then((res) => res.json()) if (!r.estimation) { throw new Error(r.errorMessage) } //const inputUsd = r.estimation.srcChainTokenIn.approximateUsdValue //const outputUsd = r.estimation.dstChainTokenOut.recommendedApproximateUsdValue const formattedInputAmount = formatUnits( BigInt(params.amount), params.fromToken.decimals ) const formattedOutputAmount = formatUnits( BigInt(r.estimation.dstChainTokenOut.recommendedAmount), params.toToken.decimals ) const inputUsd = NOT_SUPPORTED_CHAINS_PRICE_SERVICE.includes( params.fromChain ) ? r.estimation.srcChainTokenIn.approximateUsdValue : params.tokenInUsd * +formattedInputAmount const outputUsd = NOT_SUPPORTED_CHAINS_PRICE_SERVICE.includes( params.toChain ) ? r.estimation.dstChainTokenOut.recommendedApproximateUsdValue : params.tokenOutUsd * +formattedOutputAmount const fixFee = r.fixFee const wrappedAddress = NativeCurrencies[params.fromChain as ChainId].wrapped.address const nativePrice = await fetch( `${TOKEN_API_URL}/v1/public/tokens/prices`, { method: 'POST', body: JSON.stringify({ [params.fromChain]: [wrappedAddress], }), } ) .then((res) => res.json()) .then((res) => { return res?.data?.[params.fromChain]?.[wrappedAddress]?.PriceBuy || 0 }) const nativeDecimals = params.fromChain === 1151111081099710 ? 9 : NativeCurrencies[params.fromChain as ChainId].decimals const protocolFee = Number(nativePrice) * (Number(fixFee) / 10 ** nativeDecimals) const protocolFeeString = `${Number(fixFee) / 10 ** nativeDecimals} ${params.fromChain === 1151111081099710 ? 'SOL' : NativeCurrencies[params.fromChain as ChainId].symbol }` return { quoteParams: params, outputAmount: BigInt(r.estimation.dstChainTokenOut.recommendedAmount), formattedOutputAmount, inputUsd, outputUsd, priceImpact: !inputUsd || !outputUsd ? Number.NaN : ((inputUsd - outputUsd) * 100) / inputUsd, rate: +formattedOutputAmount / +formattedInputAmount, gasFeeUsd: 0, timeEstimate: r.order.approximateFulfillmentDelay, contractAddress: r.tx.allowanceTarget || r.tx.to, rawQuote: r, protocolFee, protocolFeeString, platformFeePercent: (params.feeBps * 100) / 10_000, } } 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 txBuffer = Buffer.from(quote.rawQuote.tx.data.slice(2), 'hex') // 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) // Send through wallet adapter const signature = await sendSolanaFn(transaction, solanaConnection) 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}` ) } } } await waitForConfirmation(signature) return { sender: quote.quoteParams.sender, id: quote.rawQuote.orderId, // specific id for debridge 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 account = walletClient.account?.address if (!account) throw new Error('WalletClient account is not defined') const tx = await walletClient.sendTransaction({ chain: undefined, account: account as `0x${string}`, to: quote.rawQuote.tx.to, value: BigInt(quote.rawQuote.tx.value), data: quote.rawQuote.tx.data, kzg: undefined, }) return { sender: quote.quoteParams.sender, id: quote.rawQuote.orderId, // specific id for each provider sourceTxHash: tx, 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(), } } async getTransactionStatus(p: NormalizedTxResponse): Promise<SwapStatus> { const r = await fetch(`${DEBRIDGE_API}/${p.id}/status`).then((res) => res.json() ) return { status: r.status === 'Fulfilled' ? 'Success' : 'Processing', txHash: p.id, } } }