@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.
622 lines • 29.7 kB
JavaScript
import { encodeAbiParameters, parseAbiParameters } from 'viem';
import { useServerErrorStore } from '../stores/useServerErrorStore.js';
// Define mapping between chain IDs and Debridge internal chain IDs
const DEBRIDGE_CHAIN_IDS = {
1151111081099710: '7565164', // Solana
100: '100000002', // Gnosis Chain
42161: '42161', // Arbitrum
43114: '43114', // Avalanche
56: '56', // BNB Chain
1: '1', // Ethereum
137: '137', // Polygon
250: '250', // Fantom
59144: '59144', // Linea
10: '10', // Optimism
8453: '8453', // Base
245022934: '100000001', // Neon
1890: '100000003', // Lightlink (suspended)
1088: '100000004', // Metis
7171: '100000005', // Bitrock
4158: '100000006', // CrossFi
388: '100000010', // Cronos zkEVM
1514: '100000013', // Story
146: '100000014', // Sonic
48900: '100000015', // Zircuit
2741: '100000017', // Abstract
80094: '100000020', // Berachain
60808: '100000021', // BOB
999: '100000022', // HyperEVM
5000: '100000023', // Mantle
747: '100000009', // Flow
32769: '100000008', // Zilliqa
// Other EVM chain IDs use numeric strings directly
};
// Debridge internal uses native token addresses
const DEBRIDGE_NATIVE_ADDRESS = {
evm: '0x0000000000000000000000000000000000000000',
solana: '11111111111111111111111111111111', // Debridge specific representation for SOL
};
// Actual native token addresses (for isNativeToken check)
const NATIVE_TOKEN_ADDRESSES = [
'0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', // Common EVM Native Placeholder
'0x0000000000000000000000000000000000001010', // Polygon Native Placeholder
'0x0000000000000000000000000000000000000000', // EVM Zero Address (often used for native)
'So11111111111111111111111111111111111111112', // Solana Native Mint Address
'', // Empty string might be used in some cases
].map((addr) => addr.toLowerCase());
// Native token information
const NATIVE_TOKENS = {
1: {
chainId: 1,
symbol: 'ETH',
name: 'Ethereum',
decimals: 18,
address: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
icon: '//s3.openocean.finance/images/1637894743832_8242841824007741.png',
},
56: {
chainId: 56,
address: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
name: 'Binance Chain Native Token',
symbol: 'BNB',
decimals: 18,
icon: 'https://s3.openocean.finance/token_logos/logos/bsc/0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.png',
},
137: {
chainId: 137,
address: '0x0000000000000000000000000000000000001010',
icon: 'https://s3.openocean.finance/images/1637561049975_1903381661429342.png',
name: 'Matic',
symbol: 'MATIC',
decimals: 18,
},
43114: {
chainId: 43114,
address: '0x0000000000000000000000000000000000000000',
name: 'Avalanche',
symbol: 'AVAX',
icon: 'https://ethapi.openocean.finance/logos/avax/0x0000000000000000000000000000000000000000.png',
decimals: 18,
},
250: {
chainId: 250,
address: '0x0000000000000000000000000000000000000000',
name: 'Fantom',
symbol: 'FTM',
decimals: 18,
icon: 'https://ethapi.openocean.finance/logos/fantom/0x0000000000000000000000000000000000000000.png',
},
42161: {
name: 'ETH',
address: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
decimals: 18,
symbol: 'ETH',
icon: 'https://s3.openocean.finance/images/1660286550539_3465620840567112.png',
chainId: 42161,
},
10: {
name: 'Etherum',
address: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
decimals: 18,
symbol: 'ETH',
icon: 'https://s3.openocean.finance/images/1661137422943_3757149396730206.png',
chainId: 10,
},
324: {
chainId: 324,
name: 'Ethereum',
address: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
decimals: 18,
symbol: 'ETH',
icon: 'https://s3.openocean.finance/token_logos/logos/1678448299500_8777733284012612.png',
},
8453: {
chainId: 8453,
name: 'Ethereum',
address: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
decimals: 18,
symbol: 'ETH',
icon: 'https://s3.openocean.finance/token_logos/logos/1704872214606_042688653920736286.png',
},
7565164: {
chainId: 7565164,
name: 'SOL',
address: 'So11111111111111111111111111111111111111112',
decimals: 9,
symbol: 'SOL',
icon: 'https://s3.openocean.finance/token_logos/logos/solana/So11111111111111111111111111111111111111112.png',
},
};
export class DebridgeService {
/**
* Map Debridge status code to internal unified status code
* @param status Debridge return status string or number
* @returns Internal status code ('2': failure, '3': pending, '5': success) or '2' (unknown/failure)
*/
static mapStatus(status) {
const mapped = DebridgeService.STATUS_MAP[status] ||
DebridgeService.STATUS_MAP[String(status).toLowerCase()];
console.log(`Mapping status: ${status} -> ${mapped || '2 (default)'}`);
return mapped || '2'; // Default to failure if unknown
}
/**
* Check if address is native token address
* @param tokenAddress Token address
* @returns Whether it's a native token
*/
static isNativeToken(tokenAddress) {
return NATIVE_TOKEN_ADDRESSES.includes(tokenAddress.toLowerCase());
}
/**
* Get native token information for specified chain
* @param chainId Chain ID
* @returns Native token Asset object or undefined
*/
static getNativeTokenInfo(chainId) {
return NATIVE_TOKENS[chainId];
}
/**
* Get Debridge internal chain ID
* @param chainId Original chain ID
* @returns Debridge chain ID string
*/
static getDebridgeChainId(chainId) {
return DEBRIDGE_CHAIN_IDS[chainId] || chainId.toString();
}
/**
* Get Debridge token address for specified asset
* @param asset Token Asset object
* @returns Debridge token address string
*/
static getDebridgeTokenAddress(asset) {
if (asset.chain === 'solana') {
return asset.address === NATIVE_TOKENS[7565164]?.address // 'So11111111111111111111111111111111111111112'
? DEBRIDGE_NATIVE_ADDRESS.solana // '11111111111111111111111111111111'
: asset.address;
}
else {
// EVM
return DebridgeService.isNativeToken(asset.address)
? DEBRIDGE_NATIVE_ADDRESS.evm // '0x0000000000000000000000000000000000000000'
: asset.address;
}
}
// Debridge directly get cross amount API not found, return original input value
static async getCrossAmount(params) {
console.log('Debridge getCrossAmount called with:', params);
// Debridge quote or order/create-tx API returns estimated output, but here needs independent API
// Temporary unable to directly get, return input value as placeholder
return { crossOutAmount: params.amt };
}
// Debridge get minSend direct API not found, return 0
static async minSend(params) {
console.log('Debridge minSend called with:', params);
// Debridge documentation seems not have separate minSend API
// Minimum value usually implicit in /quote or /order/create-tx response or error
return 0; // Return 0 as placeholder
}
/**
* Query Debridge cross-chain transaction status
* @param params Contains requestId
* @returns Transaction status and destination chain hash
*/
static async getCrossStatus(params) {
const { requestId } = params;
const url = new URL(`${DebridgeService.DEBRIDGE_API_URL}/intents/status/v2`);
url.searchParams.append('requestId', requestId);
try {
const response = await fetch(url.toString());
const data = await response.json(); // Try parsing JSON even for errors
console.log('Debridge getCrossStatus response:', data);
if (!response.ok) {
const error = new Error(`HTTP error! status: ${response.status}`);
error.response = { status: response.status, data: data };
throw error;
}
const { status, txHashes, error: apiError } = data || {};
let internalStatus = '2'; // Default to failure
if (apiError) {
console.error('Debridge status API returned error:', apiError);
internalStatus = '2';
}
else if (status) {
internalStatus = DebridgeService.mapStatus(status);
}
const destHash = txHashes && txHashes.length > 0 ? txHashes[0] : undefined;
return {
code: 200, // response.status should be 200 here
data: { status: internalStatus, destHash: destHash },
};
}
catch (error) {
// Handles fetch network errors or errors thrown from !response.ok
console.error(`Error fetching Debridge status for ${requestId}:`, error.response?.data || error.message);
const errorStatus = error.response?.data?.status;
const internalStatus = errorStatus
? DebridgeService.mapStatus(errorStatus)
: '2';
const code = error.response?.status || 500;
return {
code: code === 404 ? 404 : 500,
data: { status: internalStatus },
};
}
}
/**
* Get Debridge cross-chain quote (Swap U Then Cross)
* @param params Contains source/target information, amount, slippage, etc.
* @returns Contains object with fields needed to build Route or null (failure)
*/
static async swapUThenCross(params) {
// Return type needs more specific definition
const { fromMsg, toMsg, inAmount, slippage_tolerance, account, receiver } = params;
const debridgeSrcChainId = DebridgeService.getDebridgeChainId(fromMsg.chainId);
const debridgeDstChainId = DebridgeService.getDebridgeChainId(toMsg.chainId);
const debridgeSrcToken = DebridgeService.getDebridgeTokenAddress(fromMsg);
const debridgeDstToken = DebridgeService.getDebridgeTokenAddress(toMsg);
const queryParams = account && receiver
? {
srcChainId: debridgeSrcChainId,
srcChainTokenIn: debridgeSrcToken,
srcChainTokenInAmount: inAmount,
dstChainId: debridgeDstChainId,
dstChainTokenOut: debridgeDstToken,
dstChainTokenOutRecipient: receiver || account,
senderAddress: account,
referralCode: DebridgeService.REFERRAL_CODE.toString(), // Ensure referralCode is string
srcChainRefundAddress: account,
srcChainOrderAuthorityAddress: account,
dstChainOrderAuthorityAddress: receiver || account,
enableEstimate: false,
prependOperatingExpenses: true,
additionalTakerRewardBps: 0,
allowedTaker: debridgeDstChainId === '7565164'
? '2snHHreXbpJ7UwZxPe37gnUNf7Wx7wv6UKDSR2JckKuS'
: '0x555CE236C0220695b68341bc48C68d52210cC35b',
deBridgeApp: 'DESWAP',
ptp: false,
tab: new Date().getTime(),
}
: {
srcChainId: debridgeSrcChainId,
srcChainTokenIn: debridgeSrcToken,
srcChainTokenInAmount: inAmount,
dstChainTokenOutAmount: 'auto',
dstChainId: debridgeDstChainId,
dstChainTokenOut: debridgeDstToken,
referralCode: DebridgeService.REFERRAL_CODE.toString(), // Ensure referralCode is string
prependOperatingExpenses: true,
additionalTakerRewardBps: 0,
tab: new Date().getTime(),
};
const url = new URL(`${DebridgeService.DEBRIDGE_QUOTE_URL}/dln/order/create-tx`);
Object.entries(queryParams).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.append(key, value.toString());
}
});
try {
console.log('Calling Debridge create-tx with url:', url.toString());
const response = await fetch(url.toString());
const quoteData = await response.json();
console.log('Debridge create-tx response:', quoteData);
if (!response.ok) {
const error = new Error(`HTTP error! status: ${response.status}`);
error.response = { status: response.status, data: quoteData };
throw error;
}
if (!quoteData || !quoteData.estimation || !quoteData.tx) {
console.error('Invalid response from Debridge create-tx API', quoteData);
return null;
}
const { estimation, tx, orderId, fixFee, prependedOperatingExpenseCost } = quoteData;
const { srcChainTokenIn, dstChainTokenOut, approximateFulfillmentDelay } = estimation;
// Calculate minOutAmount based on slippage
const outAmount = dstChainTokenOut?.amount;
const slippagePercent = Number.parseFloat(slippage_tolerance.toString()) / 100;
const minOutAmount = outAmount
? ((BigInt(outAmount) *
BigInt(Math.floor((1 - slippagePercent) * 10000))) /
BigInt(10000)).toString()
: '0';
// Determine fee token (native token of source chain)
const feeTokenInfo = DebridgeService.getNativeTokenInfo(fromMsg.chainId);
// Use fixFee if available, otherwise default to '0'
const feeAmount = fixFee || '0';
let finalFeeToken = feeTokenInfo;
if (finalFeeToken) {
// Apply address transformation rules
let address = finalFeeToken.address.toLowerCase();
const chainId = finalFeeToken.chainId;
if (chainId === 1151111081099710 &&
address === 'so11111111111111111111111111111111111111112') {
address = '11111111111111111111111111111111'; // Debridge Solana native representation
}
else if (address === '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee') {
address = '0x0000000000000000000000000000000000000000'; // Zero address representation
}
// Create a new object with the potentially modified address
finalFeeToken = { ...finalFeeToken, address: address };
}
// Specific address for Solana, Debridge contract address for other chains
const to = fromMsg.chainId === 1151111081099710
? 'src5qyZHqTqecJV4aY6Cb6zDZLMDzrDKKezs22MPHr4'
: tx.to;
const data = {
prependedOperatingExpenseCost,
isDebridgeRoute: true, // Flag to identify the route source
orderId: orderId,
fromTokenUSD: srcChainTokenIn?.approximateUsdValue?.toString() || '0',
toTokenUSD: dstChainTokenOut?.approximateUsdValue?.toString() || '0',
outAmount: outAmount || '0',
minOutAmount: minOutAmount,
// Transaction details directly from tx object
transaction: tx.data,
to,
value: tx.value || feeAmount || '0', // Value (often native token fee for Debridge)
// Chain info
chainId: fromMsg.chainId, // Source chain for the transaction
from: account, // User's account initiates the transaction
// Estimate details
approveContract: to, // Approval needed for the Debridge contract
// executionDuration: approximateFulfillmentDelay ? approximateFulfillmentDelay * 1000 : 300000, // Convert seconds to ms? Needs verification. Defaulting to 5min.
executionDuration: 0, // Hardcoding 5min for now, Debridge delay unit unclear.
// Fee details
feeCosts: finalFeeToken
? [
{
name: 'Debridge Fee',
description: 'Protocol fee charged by Debridge',
token: finalFeeToken,
amount: feeAmount,
amountUSD: '0', // USD value of fee not directly available
percentage: '0', // Percentage calculation complex
included: false, // Assume fee is paid separately or via tx.value
},
]
: [],
// Include potential operating expense as another fee?
// const operatingExpense = estimation?.srcChainTokenIn?.approximateOperatingExpense;
// if (feeToken && operatingExpense && operatingExpense !== '0') {
// feeCosts.push({ ... })
// }
estimatedGas: feeAmount, // Using Debridge fixFee as a placeholder for estimated gas display
// Other potential fields needed by useRoutes that might be missing:
// gasPrice: tx.gasPrice, // Not directly available in Debridge response
// dexId: // Not applicable for Debridge
};
return { data };
}
catch (error) {
useServerErrorStore.getState().setError(error.response?.data?.errorMessage || error.message);
console.error('Error getting Debridge quote:', error.response?.data || error.message);
return null;
}
}
/**
* Build Debridge EVM cross-chain transaction data (Completed in swapUThenCross)
* @param params Contains account, route, receiver, etc.
* @returns Returns transactionRequest data from swapUThenCross
*/
static async buildBridgeData(params) {
// Return type needs more specific definition
const { route, account, receiver } = params; // toMiddlewareRoute, swapResult in Debridge dLN mode seems not needed
console.log('Debridge buildBridgeData called with:', params);
// Debridge dLN transaction data in swapUThenCross already obtained via /dln/order/create-tx
if (route && route.bridgeRoute && route.bridgeRoute.transactionRequest) {
const { transactionRequest, bridgeId, fees } = route.bridgeRoute;
const { fixFee } = fees?.middlewareFee || {}; // Get previous calculated fee
const bridgeRouteFromAddress = DebridgeService.getDebridgeTokenAddress(route.bridgeRoute.fromAsset);
// Debridge dLN tx object returned by `create-tx` contains `to`, `data`, `value`
// We need to encode it for passing to OpenOcean aggregation contract (if applicable)
// Format: [bridgeId, feeAmount, bridgeTokenAddress, encodedBridgeData]
// encodedBridgeData: abi.encode(['address', 'bytes'], [tx.to, tx.data])
// Check if transactionRequest is valid
if (!transactionRequest ||
!transactionRequest.to ||
!transactionRequest.data) {
console.error('Invalid transactionRequest in route for buildBridgeData');
return null;
}
try {
// Use viem to encode Debridge target address and data ABI
const abiParams = parseAbiParameters('address to, bytes data');
const sendData = encodeAbiParameters(abiParams, [
transactionRequest.to,
transactionRequest.data,
]);
// Return array format expected by aggregation contract
// Note: Here feeAmount should be Debridge fee (fixFee), need to pay with native token
// If input token is native token, this fee will be included in msg.value;
// If input token is ERC20, this fee needs additional handling (possibly needs aggregation contract support?)
// OpenOcean aggregation contract might need explicit fee parameter. Here assuming fixFee.
const feeAmount = fixFee || '0'; // Use fee from quote
console.log('Encoded sendData for Debridge:', sendData);
console.log('Params for Aggregator:', [
bridgeId,
feeAmount,
bridgeRouteFromAddress,
sendData,
]);
// Return final built data, this data will be sent to OpenOcean aggregator contract
return [bridgeId, feeAmount, bridgeRouteFromAddress, sendData];
}
catch (encodeError) {
console.error('Error encoding Debridge transaction data:', encodeError);
return null;
}
}
else {
console.error('Missing route or transactionRequest in buildBridgeData for Debridge');
// If no transactionRequest, try to refetch create-tx API to get
// This needs to extract necessary information from route
if (route?.bridgeRoute) {
const { fromAsset, toAsset, inputAmount } = route.bridgeRoute;
if (fromAsset && toAsset && inputAmount && account && receiver) {
console.log('Attempting to refetch Debridge transaction data...');
const quoteResult = await DebridgeService.swapUThenCross({
fromMsg: fromAsset,
toMsg: toAsset,
inAmount: inputAmount,
slippage_tolerance: '1', // Default slippage or get from route?
account: account,
receiver: receiver,
});
if (quoteResult?.bridgeRoute?.transactionRequest) {
const { transactionRequest, bridgeId } = quoteResult.bridgeRoute;
const fixFee = quoteResult.fees?.middlewareFee?.amount || '0';
const bridgeRouteFromAddress = DebridgeService.getDebridgeTokenAddress(fromAsset);
try {
const abiParams = parseAbiParameters('address to, bytes data');
const sendData = encodeAbiParameters(abiParams, [
transactionRequest.to,
transactionRequest.data,
]);
console.log('Re-encoded sendData for Debridge:', sendData);
console.log('Params for Aggregator (refetched):', [
bridgeId,
fixFee,
bridgeRouteFromAddress,
sendData,
]);
return [bridgeId, fixFee, bridgeRouteFromAddress, sendData];
}
catch (encodeError) {
console.error('Error encoding refetched Debridge transaction data:', encodeError);
return null;
}
}
else {
console.error('Failed to refetch Debridge transaction data.');
return null;
}
}
}
return null; // Return null indicating build failure
}
}
/**
* Build Debridge transaction data from Solana to EVM
* @param params Contains account, route, receiver
* @returns Solana transaction object { code, data: { from, to, data, value } } or null
*/
static async buildSolanaBridgeData(params) {
const { account, route, receiver } = params;
const { bridgeRoute } = route || {};
const { fromAsset, toAsset, inputAmount, toChainId } = bridgeRoute || {};
if (!fromAsset ||
!toAsset ||
!inputAmount ||
!toChainId ||
fromAsset.chainId !== 7565164) {
console.error('Invalid params for buildSolanaBridgeData', params);
return null;
}
const debridgeSrcChainId = DebridgeService.getDebridgeChainId(fromAsset.chainId); // Should be '7565164'
const debridgeDstChainId = DebridgeService.getDebridgeChainId(toChainId);
const debridgeSrcToken = DebridgeService.getDebridgeTokenAddress(fromAsset); // Handles native SOL mapping
const debridgeDstToken = DebridgeService.getDebridgeTokenAddress(toAsset); // Handles native EVM mapping
const queryParams = {
srcChainId: debridgeSrcChainId,
srcChainTokenIn: debridgeSrcToken,
srcChainTokenInAmount: inputAmount,
dstChainId: debridgeDstChainId,
dstChainTokenOut: debridgeDstToken,
senderAddress: account,
srcChainOrderAuthorityAddress: account,
dstChainTokenOutRecipient: receiver,
srcChainRefundAddress: account,
dstChainOrderAuthorityAddress: receiver,
referralCode: DebridgeService.REFERRAL_CODE.toString(), // Ensure referralCode is string
};
const url = new URL(`${DebridgeService.DEBRIDGE_QUOTE_URL}/dln/order/create-tx`);
Object.entries(queryParams).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.append(key, value.toString());
}
});
try {
console.log('Calling Debridge create-tx for Solana with url:', url.toString());
const response = await fetch(url.toString());
const data = await response.json();
console.log('Debridge create-tx (Solana) response:', data);
if (!response.ok) {
const error = new Error(`HTTP error! status: ${response.status}`);
error.response = { status: response.status, data: data };
throw error;
}
if (!data?.tx) {
console.error('Invalid response from Debridge create-tx API for Solana', data);
return null;
}
const { tx, fixFee } = data;
let valueToSend = tx.value || '0';
if (DebridgeService.isNativeToken(fromAsset.address) &&
(!tx.value || tx.value === '0') &&
fixFee) {
try {
// Use BigInt for large number addition
const totalValue = (BigInt(inputAmount) + BigInt(fixFee)).toString();
valueToSend = totalValue;
console.warn(`tx.value was missing for native SOL, calculating value as inputAmount + fixFee: ${inputAmount} + ${fixFee} = ${valueToSend}`);
}
catch (mathError) {
console.error('Error calculating total value for Solana native send:', mathError);
valueToSend = tx.value || '0';
}
}
else if (!DebridgeService.isNativeToken(fromAsset.address) &&
fixFee &&
fixFee !== '0') {
console.warn(`Sending SPL token but fixFee (${fixFee}) exists. This fee might need separate handling in SOL.`);
}
return {
code: 200, // response.status should be 200 here
data: {
from: account,
to: tx.to,
data: tx.data,
value: valueToSend,
},
};
}
catch (error) {
console.error('Error fetching Debridge Solana transaction:', error.response?.data || error.message);
useServerErrorStore.getState().setError(error.response?.data?.errorMessage || error.message);
return null;
}
}
}
DebridgeService.DEBRIDGE_API_URL = 'https://api.debridge.finance'; // Correct API URL
DebridgeService.DEBRIDGE_QUOTE_URL = 'https://deswap.debridge.finance/v1.0';
DebridgeService.REFERRAL_CODE = 31824;
// Map STATUS to internal unified status code
DebridgeService.STATUS_MAP = {
10: '5', // success (Debridge internal)
14: '2', // failure (Debridge internal)
8: '3', // comfir (Debridge internal)
DELIVERED: '5',
FAILED: '2',
INFLIGHT: '3', // Pending/In progress
EXECUTED: '5', // Likely success
CANCELLED: '2', // Failure
PENDING: '3', // Pending
ERROR: '2', // Failure
// dLN/deswap statuses
Created: '3', // Pending
Fulfilled: '5', // Success
SentUnlock: '3', // In progress
ClaimedUnlock: '5', // Success (final step for receiver)
// Common statuses from original code (might need adjustments)
Success: '5',
Pending: '3',
Stucked: '3', // Consider mapping to '3' (Pending) or '2' (Failure)
Reverted: '2',
'Not found': '2',
destination_executed: '5',
error: '2',
source_gateway_called: '3', // In progress
2: '5', // ccip success? Needs verification
success: '5',
};
//# sourceMappingURL=DebridgeService.js.map