@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.
410 lines • 16.8 kB
JavaScript
import { ChainId, createConfig, getRoutes, getStatus, getStepTransaction, } from '@lifi/sdk';
import { Transaction, VersionedTransaction } from '@solana/web3.js';
import { formatUnits } from 'viem';
import * as bitcoin from 'bitcoinjs-lib';
import { CROSS_CHAIN_FEE_RECEIVER, ZERO_ADDRESS, MAINNET_NETWORKS, } from '../constants/index.js';
import { BaseSwapAdapter, NOT_SUPPORTED_CHAINS_PRICE_SERVICE, NonEvmChain, } from './BaseSwapAdapter.js';
const LIFI_INTEGRATOR = 'openocean';
export class LifiAdapter extends BaseSwapAdapter {
constructor() {
super();
createConfig({
integrator: LIFI_INTEGRATOR,
});
}
getName() {
return 'LIFI';
}
getIcon() {
return 'https://storage.googleapis.com/ks-setting-1d682dca/aed3a971-48be-4c3c-9597-5ab78073fbf11745552578218.png';
}
getSupportedChains() {
return [NonEvmChain.Solana, NonEvmChain.Bitcoin, ...MAINNET_NETWORKS];
}
getSupportedTokens(_sourceChain, _destChain) {
return [];
}
async getQuote(params) {
const routesRequest = this.buildRoutesRequest(params);
const routesResponse = await getRoutes(routesRequest).catch((error) => {
const message = error?.cause?.responseBody?.message ||
error?.message ||
'Failed to fetch LiFi routes';
throw new Error(message);
});
if (!routesResponse.routes?.length) {
const unavailableMessage = this.getUnavailableRoutesMessage(routesResponse.unavailableRoutes);
throw new Error(unavailableMessage || 'No available routes for the requested transfer');
}
const selectedRoute = this.selectBestRoute(routesResponse.routes);
const firstStepWithTx = await this.resolveStepTransaction(selectedRoute.steps[0]);
const formattedOutputAmount = formatUnits(BigInt(selectedRoute.toAmount), params.toToken.decimals);
const formattedInputAmount = formatUnits(BigInt(params.amount), params.fromToken.decimals);
const inputUsd = NOT_SUPPORTED_CHAINS_PRICE_SERVICE.includes(params.fromChain)
? Number(selectedRoute.fromAmountUSD)
: params.tokenInUsd * +formattedInputAmount;
const outputUsd = NOT_SUPPORTED_CHAINS_PRICE_SERVICE.includes(params.toChain)
? Number(selectedRoute.toAmountUSD)
: params.tokenOutUsd * +formattedOutputAmount;
const { protocolFee, gasFeeUsd } = this.aggregateRouteFees(selectedRoute);
const timeEstimate = selectedRoute.steps.reduce((acc, step) => acc + (step.estimate?.executionDuration || 0), 0);
const rawQuote = {
route: selectedRoute,
transactionRequest: firstStepWithTx.transactionRequest,
};
return {
quoteParams: params,
outputAmount: BigInt(selectedRoute.toAmount),
formattedOutputAmount,
inputUsd,
outputUsd,
priceImpact: !inputUsd || !outputUsd
? NaN
: ((inputUsd - outputUsd) * 100) / inputUsd,
rate: +formattedOutputAmount / +formattedInputAmount,
gasFeeUsd,
timeEstimate,
contractAddress: firstStepWithTx.transactionRequest?.to ||
firstStepWithTx.estimate?.approvalAddress ||
'',
rawQuote,
protocolFee,
platformFeePercent: (params.feeBps * 100) / 10000,
};
}
async executeSwap({ quote }, walletClient, _nearWalletClient, _sendBtcFn, sendTransaction, connection) {
const rawQuote = quote.rawQuote;
const route = rawQuote?.route;
if (!route?.steps?.length) {
throw new Error('LiFi route is missing or has no steps');
}
if (quote.quoteParams.fromChain === NonEvmChain.Bitcoin) {
const stepWithTx = await this.resolveStepTransaction(route.steps[0]);
return this.executeBitcoinSwap(quote, stepWithTx, walletClient);
}
if (quote.quoteParams.fromChain === NonEvmChain.Solana) {
const stepWithTx = await this.resolveStepTransaction(route.steps[0]);
return this.executeSolanaSwap(quote, stepWithTx, walletClient);
}
return this.executeEvmRoute(quote, route, walletClient);
}
async getTransactionStatus(p) {
const res = await getStatus({
fromChain: this.toLifiChainId(p.sourceChain),
toChain: this.toLifiChainId(p.targetChain),
txHash: p.sourceTxHash,
});
return {
txHash: res?.receiving?.txHash || '',
status: res.status === 'DONE'
? 'Success'
: res.status === 'FAILED'
? 'Failed'
: 'Processing',
};
}
buildRoutesRequest(params) {
const fromChainId = this.toLifiChainId(params.fromChain);
const toChainId = this.toLifiChainId(params.toChain);
const fromTokenAddress = this.toLifiTokenAddress(params.fromChain, params.fromToken);
const toTokenAddress = this.toLifiTokenAddress(params.toChain, params.toToken);
const fromAddress = params.sender === ZERO_ADDRESS ? CROSS_CHAIN_FEE_RECEIVER : params.sender;
return {
fromChainId,
fromTokenAddress,
fromAmount: params.amount,
toChainId,
toTokenAddress,
fromAddress,
toAddress: params.recipient,
options: {
integrator: LIFI_INTEGRATOR,
slippage: params.slippage / 10000,
fee: params.feeBps / 10000,
order: 'CHEAPEST',
allowSwitchChain: true,
},
};
}
selectBestRoute(routes) {
return routes.reduce((best, route) => BigInt(route.toAmount) > BigInt(best.toAmount) ? route : best);
}
aggregateRouteFees(route) {
let protocolFee = 0;
let gasFeeUsd = Number(route.gasCostUSD || 0);
for (const step of route.steps) {
protocolFee +=
step.estimate?.feeCosts?.reduce((acc, curr) => acc + Number(curr.amountUSD), 0) || 0;
if (!gasFeeUsd) {
gasFeeUsd +=
step.estimate?.gasCosts?.reduce((acc, curr) => acc + Number(curr.amountUSD), 0) || 0;
}
}
return { protocolFee, gasFeeUsd };
}
getUnavailableRoutesMessage(unavailableRoutes) {
if (!unavailableRoutes)
return undefined;
const failed = unavailableRoutes.failed;
if (Array.isArray(failed) && failed.length > 0) {
const first = failed[0];
if (typeof first === 'object' && first !== null) {
const subpathErrors = Object.values(first.subpaths || {});
const toolError = subpathErrors.find((e) => e?.message || e?.code);
if (toolError?.message)
return toolError.message;
if (toolError?.code)
return toolError.code;
if (first.reason)
return first.reason;
}
}
return undefined;
}
async resolveStepTransaction(step) {
if (step.transactionRequest) {
return step;
}
return getStepTransaction(step);
}
async executeEvmRoute(quote, route, walletClient) {
const account = walletClient.account?.address;
if (!account)
throw new Error('WalletClient account is not defined');
let lastTxHash = '';
for (const step of route.steps) {
const stepWithTx = await this.resolveStepTransaction(step);
const { transactionRequest } = stepWithTx;
if (!transactionRequest?.to) {
continue;
}
lastTxHash = await walletClient.sendTransaction({
chain: undefined,
account,
to: transactionRequest.to,
value: BigInt(transactionRequest.value || '0'),
data: transactionRequest.data || '0x',
kzg: undefined,
});
}
if (!lastTxHash) {
throw new Error('No LiFi EVM transaction was generated');
}
return {
sender: quote.quoteParams.sender,
id: lastTxHash,
sourceTxHash: lastTxHash,
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: Date.now(),
};
}
async executeSolanaSwap(quote, step, walletClient) {
if (!walletClient.sendTransaction) {
throw new Error('Connection is not defined for Solana swap');
}
if (!step.transactionRequest?.data) {
throw new Error('LiFi Solana transaction data is missing');
}
const txBuffer = Buffer.from(step.transactionRequest.data, 'base64');
let transaction;
try {
transaction = VersionedTransaction.deserialize(txBuffer);
}
catch {
transaction = Transaction.from(txBuffer);
}
const tx = await walletClient.sendTransaction(transaction);
const signature = tx.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: Date.now(),
};
}
async executeBitcoinSwap(quote, step, walletClient) {
const account = walletClient.account?.address || quote.quoteParams.sender;
if (!account)
throw new Error('WalletClient account is not defined');
const transactionRequest = step.transactionRequest;
if (!transactionRequest?.data) {
throw new Error('TransactionRequest data is missing');
}
let psbt;
try {
psbt = bitcoin.Psbt.fromBase64(transactionRequest.data, {
network: bitcoin.networks.bitcoin,
});
}
catch {
psbt = bitcoin.Psbt.fromHex(transactionRequest.data, {
network: bitcoin.networks.bitcoin,
});
}
const anyWindow = typeof window !== 'undefined' ? window : undefined;
let connectorName;
if (anyWindow?.okxwallet?.bitcoin)
connectorName = 'OKX Wallet';
else if (anyWindow?.unisat)
connectorName = 'Unisat';
else if (anyWindow?.BitcoinProvider)
connectorName = 'Xverse';
else if (anyWindow?.phantom?.bitcoin)
connectorName = 'Phantom';
else
throw new Error('No Bitcoin wallet found');
const inputsToSign = [];
for (let index = 0; index < psbt.data.inputs.length; index++) {
const input = psbt.data.inputs[index];
let inputAddress;
if (input.witnessUtxo) {
inputAddress = bitcoin.address.fromOutputScript(input.witnessUtxo.script, bitcoin.networks.bitcoin);
}
else if (input.nonWitnessUtxo) {
inputAddress = account.toString();
}
else {
inputAddress = account.toString();
}
if (inputAddress === account.toString()) {
inputsToSign.push({ index, address: inputAddress });
}
}
if (inputsToSign.length === 0) {
throw new Error('No inputs found to sign');
}
const psbtBase64 = psbt.toBase64();
const psbtHex = psbt.toHex();
let signedPsbtBase64;
switch (connectorName) {
case 'OKX Wallet': {
const response = await anyWindow.okxwallet.bitcoin.signPsbt(psbtHex, {
autoFinalized: false,
toSignInputs: inputsToSign.map((item) => ({
index: item.index,
address: item.address,
sighashTypes: [1],
})),
});
signedPsbtBase64 = this.convertHexToBase64(this.extractSignedPsbt(response) || '');
break;
}
case 'Unisat': {
const response = await anyWindow.unisat.signPsbt(psbtHex, {
autoFinalized: false,
toSignInputs: inputsToSign.map((item) => ({
index: item.index,
address: item.address,
sighashTypes: [1],
})),
});
signedPsbtBase64 = this.convertHexToBase64(this.extractSignedPsbt(response) || '');
break;
}
case 'Xverse': {
const response = await anyWindow.BitcoinProvider.request('signPsbt', {
psbt: psbtHex,
finalize: false,
toSignInputs: inputsToSign.map((item) => ({
index: item.index,
address: item.address,
})),
});
signedPsbtBase64 = this.extractSignedPsbt(response) || '';
break;
}
case 'Phantom': {
const phantom = anyWindow.phantom.bitcoin;
if (!phantom?.signPSBT) {
throw new Error('Phantom wallet does not support signPSBT');
}
const response = await phantom.signPSBT(psbtBase64, {
autoFinalize: false,
inputsToSign: inputsToSign.map((item) => ({
index: item.index,
address: item.address,
sighashTypes: [1],
})),
});
signedPsbtBase64 = this.extractSignedPsbt(response) || '';
break;
}
default:
throw new Error(`Unsupported wallet: ${connectorName}`);
}
if (!signedPsbtBase64) {
throw new Error('Failed to sign PSBT');
}
const signedPsbt = bitcoin.Psbt.fromBase64(signedPsbtBase64, {
network: bitcoin.networks.bitcoin,
});
signedPsbt.finalizeAllInputs();
const rawTx = signedPsbt.extractTransaction().toHex();
const txHash = await fetch('https://mempool.space/api/tx', {
method: 'POST',
headers: { 'Content-Type': 'text/plain' },
body: rawTx,
}).then((r) => r.text());
if (!txHash || txHash.startsWith('<')) {
throw new Error(`Failed to broadcast transaction: ${txHash}`);
}
return {
sender: quote.quoteParams.sender,
id: txHash,
sourceTxHash: txHash,
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: Date.now(),
};
}
toLifiChainId(chain) {
if (chain === NonEvmChain.Solana)
return ChainId.SOL;
if (chain === NonEvmChain.Bitcoin)
return ChainId.BTC;
return Number(chain);
}
toLifiTokenAddress(chain, token) {
if (chain === NonEvmChain.Solana || chain === NonEvmChain.Bitcoin) {
return token.address;
}
return token.isNative ? ZERO_ADDRESS : token.address;
}
convertHexToBase64(hexString) {
try {
return Buffer.from(hexString, 'hex').toString('base64');
}
catch {
return hexString;
}
}
extractSignedPsbt(response) {
if (!response)
return null;
if (typeof response === 'string')
return response;
return (response.signedPsbtHex ||
response.signedPsbtBase64 ||
response.signedPsbt ||
null);
}
}
//# sourceMappingURL=LifiAdapter.js.map