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