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