UNPKG

@robertprp/intents-sdk

Version:

Shogun Network Intent-based cross-chain swaps SDK

332 lines (280 loc) 10.6 kB
import { ChainID, isEvmChain, type SupportedEvmChain } from '../../chains.js'; import { JupiterQuoteProvider } from './jupiter.js'; import { type QuoteResponse as JupiterQuoteResponse } from '@jup-ag/api'; import { ParaswapQuoteProvider, type SwapEstimateResult } from './paraswap.js'; import { CROSS_CHAIN_TOKENS } from './stablecoins-tokens.js'; import { AftermathQuoteProvider } from './aftermath.js'; import type { RouterCompleteTradeRoute } from 'aftermath-ts-sdk'; import { calculateAmounts } from '../defillama.js'; import { LiquidSwapQuoteProvider, type LiquidSwapQuoteResponse } from './liquidswap.js'; type SingleChainQuoteParams = { chainId: ChainID; amount: bigint; tokenIn: string; tokenOut: string; slippageBps?: number; }; export type RouteProvider = 'paraswap' | 'jupiter' | 'aftermath' | 'liquidswap'; export type Quote = { amountIn: bigint; amountOut: bigint; amountOutUsd: number; amountInUsd: number; slippage: number; priceImpact: number; provider: RouteProvider; rawQuote: any; }; export type QuoteParams = { sourceChainId: ChainID; destChainId: ChainID; amount: bigint; tokenIn: string; tokenOut: string; }; export type QuoteResponse = { amountInUsd: number; estimatedAmountInAsMinStablecoinAmount: bigint; estimatedAmountOut: bigint; estimatedAmountOutUsd: number; estimatedAmountOutReduced: bigint; estimatedAmountOutUsdReduced: number; }; export class QuoteProvider { private static readonly DEFAULT_BRIDGE_TOKEN = 'USDC'; private static readonly DEFAULT_BRIDGE_TOKEN_DECIMALS = 6; public static async getQuoteFromDefillama(params: QuoteParams): Promise<QuoteResponse> { const defillamaQuote = await calculateAmounts({ amountIn: params.amount, tokenIn: params.tokenIn, tokenOut: params.tokenOut, destChainId: params.destChainId, srcChainId: params.sourceChainId, }); return { amountInUsd: 0, estimatedAmountOutUsd: 0, estimatedAmountOutUsdReduced: 0, estimatedAmountInAsMinStablecoinAmount: defillamaQuote.minStablecoinsAmount, estimatedAmountOut: defillamaQuote.amountOut, estimatedAmountOutReduced: defillamaQuote.amountOut, }; } public static async getQuoteWithDefillamaFallback(params: QuoteParams): Promise<QuoteResponse> { try { const quote = await this.getQuoteFromRouters(params); return quote; } catch (e) { const quote = await this.getQuoteFromDefillama(params); return quote; } } public static async getQuote(params: QuoteParams): Promise<QuoteResponse> { return this.getQuoteWithDefillamaFallback(params); } private static async getQuoteFromRouters(params: QuoteParams): Promise<QuoteResponse> { const bridgeTokenSymbol = QuoteProvider.DEFAULT_BRIDGE_TOKEN; const sourceBridgeToken = CROSS_CHAIN_TOKENS[bridgeTokenSymbol]?.[params.sourceChainId]!; const destBridgeToken = CROSS_CHAIN_TOKENS[bridgeTokenSymbol]?.[params.destChainId]!; const sourceSingleChainQuoteParams: SingleChainQuoteParams = { chainId: params.sourceChainId, amount: params.amount, tokenIn: params.tokenIn, tokenOut: sourceBridgeToken, }; const sourceQuote = await this.getSingleChainQuote(sourceSingleChainQuoteParams); const destSingleChainQuoteParams: SingleChainQuoteParams = { chainId: params.destChainId, amount: sourceQuote.amountOut, tokenIn: destBridgeToken, tokenOut: params.tokenOut, }; const destQuote = await this.getSingleChainQuote(destSingleChainQuoteParams); // Use weighted reduction instead of fixed percentage const weightedReductionFactor = this.calculateWeightedReductionFactorForUsd(destQuote.amountOutUsd); const estimatedAmountOutReduced = BigInt(Math.floor(Number(destQuote.amountOut) * weightedReductionFactor)); const estimatedAmountOutUsdReduced = destQuote.amountOutUsd * weightedReductionFactor; const reducedAmountInUsd = this.calculateWeightedReductionFactorForUsd(sourceQuote.amountInUsd); const estimatedAmountInAsMinStablecoinAmount = BigInt( Math.floor(reducedAmountInUsd * 10 ** this.DEFAULT_BRIDGE_TOKEN_DECIMALS), ); return { estimatedAmountOut: destQuote.amountOut, estimatedAmountOutUsd: destQuote.amountOutUsd, amountInUsd: sourceQuote.amountInUsd, estimatedAmountInAsMinStablecoinAmount, estimatedAmountOutReduced, estimatedAmountOutUsdReduced, }; } public static async getSingleChainQuote(params: SingleChainQuoteParams): Promise<Quote> { if (params.tokenIn.toLowerCase() === params.tokenOut.toLowerCase()) { let amountUsd = 0; if (params.tokenIn.toLowerCase() === CROSS_CHAIN_TOKENS.USDC![params.chainId].toLowerCase()) { amountUsd = Number(params.amount) / 10 ** this.DEFAULT_BRIDGE_TOKEN_DECIMALS; } return { amountIn: params.amount, amountOut: params.amount, amountOutUsd: amountUsd, amountInUsd: amountUsd, slippage: 0, priceImpact: 0, provider: getProviderNameByChainId(params.chainId), rawQuote: null, }; } if (params.chainId === ChainID.Hyperliquid) { const liquidSwapQuote = await QuoteProvider.getLiquidSwapQuote(params); return QuoteProvider.transformLiquidSwapQuote(liquidSwapQuote); } if (params.chainId === ChainID.Solana) { const jupiterQuote = await QuoteProvider.getJupiterQuote(params); return QuoteProvider.transformJupiterQuote(jupiterQuote); } if (params.chainId === ChainID.Sui) { const quote = await QuoteProvider.getAftermathQuote(params); return QuoteProvider.transformAftermathQuote({ quote, request: params }); } if (isEvmChain(params.chainId)) { const paraswapQuote = await QuoteProvider.getParaswapQuote(params); return QuoteProvider.transformParaswapQuote(paraswapQuote); } throw new Error(`Unsupported chain for quote: ${params.chainId}`); } static transformLiquidSwapQuote(liquidSwapQuote: LiquidSwapQuoteResponse): Quote { return { amountIn: BigInt(liquidSwapQuote.amountIn), amountOut: BigInt(liquidSwapQuote.amountOut), amountOutUsd: 0, amountInUsd: 0, slippage: 0, priceImpact: Number(liquidSwapQuote.averagePriceImpact), provider: 'liquidswap', rawQuote: liquidSwapQuote, }; } /** * Calculates the weighted reduction factor for an amount in USD. * For amounts <= $5, the reduction factor is 0.65. */ public static calculateWeightedReductionFactorForUsd(amountUsd: number): number { if (amountUsd <= 5) { return 0.5; } else if (amountUsd <= 10) { return 0.65; } else if (amountUsd <= 20) { return 0.77; } else if (amountUsd <= 30) { return 0.9; } else { return 0.95; } } protected static async getLiquidSwapQuote(params: SingleChainQuoteParams) { const liquidSwapQuoter = new LiquidSwapQuoteProvider(); return liquidSwapQuoter.getQuote({ tokenIn: params.tokenIn, tokenOut: params.tokenOut, amountIn: params.amount.toString(), }); } protected static async getJupiterQuote(params: SingleChainQuoteParams) { const jupiterQuoter = new JupiterQuoteProvider(); return jupiterQuoter.getQuote({ amount: params.amount, tokenIn: params.tokenIn, tokenOut: params.tokenOut, swapMode: 'ExactIn', slippageBps: params.slippageBps, }); } protected static async getParaswapQuote(params: SingleChainQuoteParams) { const paraswapQuoter = new ParaswapQuoteProvider(params.chainId as SupportedEvmChain); return paraswapQuoter.getSwapEstimation({ amount: BigInt(params.amount), srcToken: params.tokenIn, destToken: params.tokenOut, side: 'SELL', }); } protected static async getAftermathQuote(params: SingleChainQuoteParams) { const aftermathQuoter = new AftermathQuoteProvider(); return aftermathQuoter.getQuote({ amountIn: params.amount, tokenIn: params.tokenIn, tokenOut: params.tokenOut, }); } private static transformAftermathQuote({ quote, request, }: { quote: RouterCompleteTradeRoute; request: SingleChainQuoteParams; }): Quote { let amountUsd = 0; if (request.tokenIn === CROSS_CHAIN_TOKENS.USDC![request.chainId]) { amountUsd = Number(quote.coinIn.amount) / 10 ** this.DEFAULT_BRIDGE_TOKEN_DECIMALS; } if (request.tokenOut === CROSS_CHAIN_TOKENS.USDC![request.chainId]) { amountUsd = Number(quote.coinOut.amount) / 10 ** this.DEFAULT_BRIDGE_TOKEN_DECIMALS; } return { amountIn: quote.coinIn.amount, amountOut: quote.coinOut.amount, amountOutUsd: amountUsd, amountInUsd: amountUsd, slippage: quote.slippage ?? 0, priceImpact: 0, provider: 'aftermath', rawQuote: quote, }; } private static transformParaswapQuote(paraswapQuote: SwapEstimateResult): Quote { return { amountIn: BigInt(paraswapQuote.amountIn), amountOut: BigInt(paraswapQuote.amountOut), amountOutUsd: paraswapQuote.amountOutUsd, amountInUsd: paraswapQuote.amountInUsd, slippage: 0, // Calculate from paraswap data if available priceImpact: paraswapQuote.quote.maxImpact ? Number(paraswapQuote.quote.maxImpact) * 100 : 0, provider: 'paraswap', rawQuote: paraswapQuote, }; } private static transformJupiterQuote({ quote }: { quote: JupiterQuoteResponse }): Quote { const amountIn = Number(quote.inAmount); const amountOut = Number(quote.outAmount); let amountUsd = 0; const priceImpact = quote.priceImpactPct ? Number(quote.priceImpactPct) * 100 : 0; if (quote.outputMint === CROSS_CHAIN_TOKENS.USDC![ChainID.Solana]) { amountUsd = amountOut / 10 ** this.DEFAULT_BRIDGE_TOKEN_DECIMALS; } if (quote.inputMint === CROSS_CHAIN_TOKENS.USDC![ChainID.Solana]) { amountUsd = amountIn / 10 ** this.DEFAULT_BRIDGE_TOKEN_DECIMALS; } return { amountIn: BigInt(amountIn), amountOut: BigInt(amountOut), amountOutUsd: amountUsd, amountInUsd: amountUsd, slippage: quote.slippageBps ? quote.slippageBps / 100 : 0, priceImpact, provider: 'jupiter', rawQuote: quote, }; } } function getProviderNameByChainId(chainId: ChainID): RouteProvider { switch (chainId) { case ChainID.Sui: return 'aftermath'; case ChainID.Solana: return 'jupiter'; case ChainID.Hyperliquid: return 'liquidswap'; default: return 'paraswap'; } }