@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.
214 lines • 10.2 kB
JavaScript
import { Transaction, VersionedTransaction } from '@solana/web3.js';
import { getPublicClient } from 'wagmi/actions';
import { 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 { BaseSwapAdapter, NOT_SUPPORTED_CHAINS_PRICE_SERVICE, } from './BaseSwapAdapter.js';
export class OrbiterAdapter extends BaseSwapAdapter {
constructor() {
super();
}
getName() {
return 'Orbiter';
}
getIcon() {
return 'https://www.orbiter.finance/favicon.ico';
}
getSupportedChains() {
return [...MAINNET_NETWORKS];
}
getSupportedTokens(_sourceChain, _destChain) {
return [];
}
async getQuote(params) {
const fromToken = params.fromToken;
const toToken = params.toToken;
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.id
: fromToken.isNative
? ZERO_ADDRESS
: fromToken.address,
destToken: params.toChain === 1151111081099710
? params.toToken.id
: toToken.isNative
? ZERO_ADDRESS
: toToken.address,
amount: params.amount.toString(),
userAddress: params.sender,
targetRecipient: params.recipient,
slippage: params.slippage / 10000,
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.action === 'approve');
const approvalContract = res.steps.find((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 }, walletClient, _nearWallet, _sendBtcFn, sendSolanaFn, solanaConnection) {
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) => {
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;
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) => 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) => {
const tx = await walletClient.sendTransaction({
chain: undefined,
account,
to: step.tx.to,
value: BigInt(step.tx.value),
data: step.tx.data,
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) {
if (p.sourceChain !== 1151111081099710) {
const publicClient = getPublicClient(useConfig(), {
chainId: p.sourceChain,
});
const receipt = await publicClient?.getTransactionReceipt({
hash: p.id,
});
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',
};
}
}
//# sourceMappingURL=OrbiterAdapter.js.map