UNPKG

@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.

361 lines 13.6 kB
import { ChainId } from '@openocean.finance/widget-sdk'; import { formatUnits, parseUnits } from 'viem'; import { BaseSwapAdapter, } from './BaseSwapAdapter.js'; // const DEFAULT_PEGASUS_API_BASE = 'https://stagenet-api.pegasusfi.xyz' const DEFAULT_PEGASUS_API_BASE = 'https://pegasusfi.openocean.finance'; const PEGASUS_API_KEY = ''; //'pegasus-b13023448e2d40c691001548' const CHAIN_ID_TO_PEGASUS = { [ChainId.ETH]: 'ETH', [ChainId.BSC]: 'BSC', [ChainId.POL]: 'POL', [ChainId.ARB]: 'ARB', [ChainId.OPT]: 'OPT', [ChainId.AVA]: 'AVA', [ChainId.BAS]: 'BASE', [ChainId.FTM]: 'FTM', [ChainId.MNT]: 'MNT', [ChainId.SCL]: 'SCROLL', [ChainId.BLS]: 'BLAST', [ChainId.SON]: 'SONIC', [ChainId.UNI]: 'UNI', [ChainId.MAM]: 'METIS', }; const NATIVE_TOKEN_SYMBOL = { [ChainId.ETH]: 'ETH', [ChainId.BSC]: 'BNB', [ChainId.POL]: 'POL', [ChainId.ARB]: 'ETH', [ChainId.OPT]: 'ETH', [ChainId.AVA]: 'AVAX', [ChainId.BAS]: 'ETH', [ChainId.FTM]: 'FTM', [ChainId.MNT]: 'MNT', }; const erc20ApproveAbi = [ { inputs: [ { type: 'address', name: 'spender' }, { type: 'uint256', name: 'amount' }, ], name: 'approve', outputs: [{ type: 'bool', name: '' }], stateMutability: 'nonpayable', type: 'function', }, ]; const CHAINS_CACHE_TTL_MS = 5 * 60 * 1000; const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; const NATIVE_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; function parsePegasusAsset(asset) { const dashIndex = asset.indexOf('-'); if (dashIndex === -1) { return { symbol: asset }; } return { symbol: asset.slice(0, dashIndex), address: asset.slice(dashIndex + 1), }; } function readEnvVar(name) { try { if (typeof process !== 'undefined' && process.env?.[name]) { return process.env[name]; } } catch { // ignore } try { const metaEnv = import.meta.env; if (metaEnv?.[name]) { return metaEnv[name]; } if (metaEnv?.[`VITE_${name}`]) { return metaEnv[`VITE_${name}`]; } } catch { // ignore } return undefined; } function getPegasusApiBase() { return (readEnvVar('PEGASUS_API_BASE')?.replace(/\/$/, '') || DEFAULT_PEGASUS_API_BASE); } function getPegasusApiKey() { return PEGASUS_API_KEY; } export class PegasusAdapter extends BaseSwapAdapter { constructor() { super(...arguments); this.chainsCache = null; } getName() { return 'Pegasus'; } getIcon() { return 'https://pegasusfi.openocean.finance/favicon.ico'; } getSupportedChains() { return Object.keys(CHAIN_ID_TO_PEGASUS).map(Number); } getSupportedTokens(_sourceChain, _destChain) { return []; } async pegasusRequest(path, init) { const response = await fetch(`${getPegasusApiBase()}${path}`, { ...init, headers: { // 'X-API-Key': getPegasusApiKey(), ...(init?.body ? { 'Content-Type': 'application/json' } : {}), ...(init?.headers || {}), }, }); const data = (await response.json()); if (!response.ok || data.error) { throw new Error(data.error?.userMessage || data.error?.message || `Pegasus API request failed (${response.status})`); } return data; } async getChains() { if (this.chainsCache && Date.now() - this.chainsCache.fetchedAt < CHAINS_CACHE_TTL_MS) { return this.chainsCache.data; } const data = await this.pegasusRequest('/chains', { method: 'GET', }); this.chainsCache = { fetchedAt: Date.now(), data, }; return data; } async getChainAssets(pegasusChain) { const { chains } = await this.getChains(); const chainData = chains.find((chain) => chain.chain === pegasusChain); if (!chainData?.assets?.length) { throw new Error(`No Pegasus assets found for chain ${pegasusChain}`); } return chainData.assets; } toPegasusChain(chain) { const pegasusChain = CHAIN_ID_TO_PEGASUS[chain]; if (!pegasusChain) { throw new Error(`Pegasus does not support chain ${chain}`); } return pegasusChain; } async resolvePegasusToken(chain, token) { const pegasusChain = this.toPegasusChain(chain); const assets = await this.getChainAssets(pegasusChain); const address = token.address?.toLowerCase?.() || ''; const isNative = token.isNative || address === ZERO_ADDRESS || address === NATIVE_ADDRESS; if (isNative) { const nativeCandidates = [ NATIVE_TOKEN_SYMBOL[chain], token.symbol, ] .filter(Boolean) .map((symbol) => symbol.toUpperCase()); for (const symbol of nativeCandidates) { const nativeAsset = assets.find((asset) => !asset.includes('-') && asset.toUpperCase() === symbol); if (nativeAsset) { return nativeAsset; } } throw new Error(`Native token not supported on Pegasus chain ${pegasusChain}`); } const erc20Asset = assets.find((asset) => { const parsed = parsePegasusAsset(asset); return parsed.address?.toLowerCase() === address; }); if (erc20Asset) { return erc20Asset; } const symbolAsset = assets.find((asset) => { const parsed = parsePegasusAsset(asset); return (parsed.address && parsed.symbol.toUpperCase() === token.symbol?.toUpperCase()); }); if (symbolAsset) { return symbolAsset; } throw new Error(`Token ${token.symbol || address} is not supported on Pegasus chain ${pegasusChain}`); } selectBestRoute(routes, warnings) { if (!routes.length) { throw new Error('No Pegasus routes found'); } const blockedProviders = new Set((warnings || []) .filter((warning) => ['UNSUPPORTED_PAIR', 'CHAIN_HALTED'].includes(warning.code)) .map((warning) => warning.provider.toLowerCase())); const eligibleRoutes = routes.filter((route) => !blockedProviders.has(route.provider.toLowerCase())); const targetRoutes = eligibleRoutes.length > 0 ? eligibleRoutes : routes; return targetRoutes.reduce((best, current) => Number.parseFloat(current.expectedOutput) > Number.parseFloat(best.expectedOutput) ? current : best); } async getQuote(params) { try { const fromChain = this.toPegasusChain(params.fromChain); const toChain = this.toPegasusChain(params.toChain); const fromToken = await this.resolvePegasusToken(params.fromChain, params.fromToken); const toToken = await this.resolvePegasusToken(params.toChain, params.toToken); const amount = formatUnits(BigInt(params.amount), params.fromToken.decimals); const searchParams = new URLSearchParams({ fromChain, fromToken, toChain, toToken, amount, }); if (params.sender) { searchParams.set('fromAddress', params.sender); } if (params.recipient) { searchParams.set('toAddress', params.recipient); } if (params.slippage > 0) { searchParams.set('slippageBps', params.slippage.toString()); } const quoteResponse = await this.pegasusRequest(`/quote?${searchParams.toString()}`, { method: 'GET' }); const selectedRoute = this.selectBestRoute(quoteResponse.routes, quoteResponse.warnings); const formattedInputAmount = amount; const formattedOutputAmount = selectedRoute.expectedOutput; const outputAmount = parseUnits(formattedOutputAmount, params.toToken.decimals); const inputUsd = params.tokenInUsd * Number.parseFloat(formattedInputAmount); const outputUsd = params.tokenOutUsd * Number.parseFloat(formattedOutputAmount); const priceImpact = !inputUsd || !outputUsd ? Number.NaN : ((inputUsd - outputUsd) * 100) / inputUsd; const rate = Number.parseFloat(formattedInputAmount) > 0 ? Number.parseFloat(formattedOutputAmount) / Number.parseFloat(formattedInputAmount) : 0; const rawQuote = { ...quoteResponse, selectedRoute, }; return { quoteParams: params, outputAmount, formattedOutputAmount, inputUsd, outputUsd, rate, timeEstimate: selectedRoute.estimatedTimeSeconds || 0, priceImpact, gasFeeUsd: 0, contractAddress: selectedRoute.router || selectedRoute.inboundAddress || '', rawQuote, protocolFee: Number.parseFloat(selectedRoute.fees?.total || '0'), platformFeePercent: (params.feeBps * 100) / 10000, }; } catch (error) { return this.handleError(error); } } async executeSwap({ quote }, walletClient) { const rawQuote = quote.rawQuote; if (!rawQuote?.quoteId || !rawQuote?.selectedRoute?.provider) { throw new Error('Pegasus quote is missing quoteId or provider'); } const account = walletClient.account?.address; if (!account) { throw new Error('Wallet client account is not defined'); } const swapResponse = await this.pegasusRequest('/swap', { method: 'POST', body: JSON.stringify({ quoteId: rawQuote.quoteId, provider: rawQuote.selectedRoute.provider, fromAddress: quote.quoteParams.sender || account, toAddress: quote.quoteParams.recipient || account, slippageBps: quote.quoteParams.slippage, }), }); if (swapResponse.approvalTx) { await walletClient.writeContract({ chain: undefined, account, address: swapResponse.approvalTx.tokenAddress, abi: erc20ApproveAbi, functionName: 'approve', args: [ swapResponse.approvalTx.spender, BigInt(swapResponse.approvalTx.amount), ], }); } const txParams = swapResponse.txParams; let sourceTxHash = swapResponse.transactionId; if (txParams?.to) { sourceTxHash = await walletClient.sendTransaction({ chain: undefined, account, to: txParams.to, data: txParams.data || '0x', value: BigInt(txParams.value || '0'), gas: txParams.gasLimit ? BigInt(txParams.gasLimit) : undefined, kzg: undefined, }); } else if (txParams?.instaswapSwapLite?.depositAddress) { throw new Error(`Pegasus route requires deposit to ${txParams.instaswapSwapLite.depositAddress}. Manual deposit flow is not implemented yet.`); } else if (rawQuote.selectedRoute.inboundAddress) { throw new Error(`Pegasus route requires deposit to ${rawQuote.selectedRoute.inboundAddress}. Manual deposit flow is not implemented yet.`); } return { sender: quote.quoteParams.sender, id: swapResponse.transactionId, sourceTxHash, 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 getTransactionStatus(p) { try { const data = await this.pegasusRequest(`/swap/${encodeURIComponent(p.id)}`, { method: 'GET' }); const status = data.status?.toLowerCase(); let finalStatus = 'Processing'; if (status === 'completed' || status === 'success' || status === 'done') { finalStatus = 'Success'; } else if (status === 'failed' || status === 'reverted') { finalStatus = 'Failed'; } else if (status === 'refunded') { finalStatus = 'Refunded'; } return { txHash: data.destinationTxHash || data.txHash || '', status: finalStatus, }; } catch (error) { console.error('Failed to get Pegasus transaction status:', error); return { txHash: '', status: 'Processing', }; } } } //# sourceMappingURL=PegasusAdapter.js.map