@robertprp/intents-sdk
Version:
Shogun Network Intent-based cross-chain swaps SDK
332 lines (280 loc) • 10.6 kB
text/typescript
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';
}
}