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