@robertprp/intents-sdk
Version:
Shogun Network Intent-based cross-chain swaps SDK
165 lines (140 loc) • 5.18 kB
text/typescript
import { ChainID, type SupportedChain } from '../chains.js';
import { NATIVE_EVM_ETH_ADDRESSES, NATIVE_SOLANA_TOKEN_ADDRESS, WRAPPED_SOL_MINT_ADDRESS } from '../constants.js';
import { Parsers } from './parsers.js';
import { QuoteProvider } from './quote/aggregator.js';
const TOKEN_PRICE_BASE_URL: string = 'https://coins.llama.fi/prices/current/';
const STABLECOIN_DECIMALS = 6; // USDC/USDT typically use 6 decimals
/**
* Response structure from DefiLlama API for token prices
*/
export interface DefiLlamaTokensResponse {
coins: Record<string, DefiLlamaCoinData>;
}
/**
* Individual token data from DefiLlama API
*/
export interface DefiLlamaCoinData {
/** Number of decimal places for the token */
decimals: number;
/** Token symbol (e.g., "ETH", "USDC") */
symbol: string;
/** Current price in USD */
price: number;
/** Unix timestamp of the price data */
timestamp: number;
/** Confidence level of the price data (0-1) */
confidence: number;
}
/**
* Maps chain IDs to their corresponding chain types
* Used to determine which SDK implementation to use for a given chain
*/
export const chainIdToDefiLlamaChainMap = {
[ChainID.Arbitrum]: 'arbitrum',
[ChainID.Base]: 'base',
[ChainID.Optimism]: 'optimism',
[ChainID.Hyperliquid]: 'hyperliquid',
[ChainID.Solana]: 'solana',
[ChainID.Sui]: 'sui',
} as const;
/**
* Creates DefiLlama coin key from chain ID and token address
* Format: "chainName:tokenAddress"
*/
export const createDefiLlamaCoinKey = (chainId: ChainID, tokenAddress: string): string => {
const chainName = chainIdToDefiLlamaChainMap[chainId];
return `${chainName}:${toDefiLlamaToken(tokenAddress)}`;
};
/**
* Retrieves token data from DefiLlama response by chain and address
*/
export const getCoinFromResponse = (
response: DefiLlamaTokensResponse,
chainId: ChainID,
tokenAddress: string,
): DefiLlamaCoinData => {
const key = createDefiLlamaCoinKey(chainId, tokenAddress);
const coin = response.coins[key];
if (!coin) {
throw new Error(`DefiLlama coin not found for ${key}`);
}
return coin;
};
/**
* Converts Solana native token address to wrapped SOL mint address
* DefiLlama uses wrapped SOL mint address for native SOL
*
* @param tokenAddress The token address to convert
* @returns The DefiLlama-compatible token address
*/
function toDefiLlamaToken(tokenAddress: string): string {
if (isNativeTokenEvmAddress(tokenAddress)) {
return '0x0000000000000000000000000000000000000000';
}
if (tokenAddress === NATIVE_SOLANA_TOKEN_ADDRESS) {
return WRAPPED_SOL_MINT_ADDRESS;
}
return tokenAddress;
}
function isNativeTokenEvmAddress(tokenAddress: string): boolean {
const normalizedAddress = tokenAddress.toLowerCase();
return NATIVE_EVM_ETH_ADDRESSES.some((addr) => addr.toLowerCase() === normalizedAddress);
}
/**
* Builds a comma-separated query string for DefiLlama API token requests
*
* Converts an array of chain/token pairs into the format expected by DefiLlama's
* bulk price endpoint: "chain1:token1,chain2:token2,..."
*
* @param tokens - Array of [ChainID, token address] tuples to query
* @returns Comma-separated string of DefiLlama coin keys
*/
function buildTokensQueryString(tokens: Array<[ChainID, string]>): string {
return tokens.map(([chainId, tokenAddress]) => createDefiLlamaCoinKey(chainId, tokenAddress)).join(',');
}
/**
* Fetch tokens data for array of coins
*
* @param tokens Array of [ChainID, Token Address] tuples
* @returns Promise resolving to DefiLlama tokens response
*/
export async function getTokensData(tokens: Array<[ChainID, string]>): Promise<DefiLlamaTokensResponse> {
const tokensStr = buildTokensQueryString(tokens);
const url = `${TOKEN_PRICE_BASE_URL}${tokensStr}`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
return response.json();
}
export async function calculateAmounts({
srcChainId,
tokenIn,
amountIn,
destChainId,
tokenOut,
}: {
srcChainId: SupportedChain;
tokenIn: string;
amountIn: bigint;
destChainId: SupportedChain;
tokenOut: string;
}): Promise<{ amountOut: bigint; minStablecoinsAmount: bigint }> {
// Fetch token data from DefiLlama
const tokensResponse = await getTokensData([
[srcChainId, tokenIn],
[destChainId, tokenOut],
]);
const tokenInData = getCoinFromResponse(tokensResponse, srcChainId, tokenIn);
const tokenOutData = getCoinFromResponse(tokensResponse, destChainId, tokenOut);
// Calculate 80% of the amount_in value as amount_out_min and min_stablecoins_amount
const tokenInUsdAmount = Parsers.bigintToFloat(amountIn, tokenInData.decimals) * tokenInData.price;
const reductionFactor = QuoteProvider.calculateWeightedReductionFactorForUsd(tokenInUsdAmount);
const amountInUsdReduced = tokenInUsdAmount * reductionFactor;
const amountOutReduced = amountInUsdReduced / tokenOutData.price;
const amountOut = Parsers.floatToBigint(amountOutReduced, tokenOutData.decimals);
const minStablecoinsAmount = Parsers.floatToBigint(amountInUsdReduced, STABLECOIN_DECIMALS);
return { amountOut, minStablecoinsAmount };
}