UNPKG

@kamino-finance/kliquidity-sdk

Version:

Typescript SDK for interacting with the Kamino Liquidity (kliquidity) protocol

157 lines (135 loc) 5.08 kB
import Decimal from 'decimal.js'; import { Address } from '@solana/kit'; import { Logger } from '../utils/Logger'; interface KSwapTokenPriceData { isScaledUiToken: boolean; value: number; updateUnixTime: number; updateHumanTime: string; priceInNative: number; priceChange24h: number; } interface KSwapBatchPriceResponse { success: boolean; data: { [key: string]: KSwapTokenPriceData | null }; } const BATCH_TOKEN_PRICES_LIMIT = 100; const BATCH_TOKEN_PRICES_MAX_URL_LENGTH = 2000; const BATCH_TOKEN_PRICES_PATH = 'batch-token-prices'; const BATCH_TOKEN_PRICES_QUERY_KEY = 'tokens'; const getBatchTokenPricesBaseUrl = (apiBaseUrl: string): string => `${apiBaseUrl.replace(/\/+$/, '')}/${BATCH_TOKEN_PRICES_PATH}`; const getBatchTokenPricesUrl = (apiBaseUrl: string, tokens: Address[]): string => { const tokensParams = new URLSearchParams(); for (const token of tokens) { tokensParams.append(BATCH_TOKEN_PRICES_QUERY_KEY, token); } return `${getBatchTokenPricesBaseUrl(apiBaseUrl)}?${tokensParams.toString()}`; }; const getTokenQueryParamLength = (token: Address): number => { const tokenParam = new URLSearchParams(); tokenParam.append(BATCH_TOKEN_PRICES_QUERY_KEY, token); return tokenParam.toString().length; }; const getTokensPriceBatches = (apiBaseUrl: string, tokens: Address[]): Address[][] => { const baseUrlLength = `${getBatchTokenPricesBaseUrl(apiBaseUrl)}?`.length; const batches: Address[][] = []; let batch: Address[] = []; let batchUrlLength = baseUrlLength; for (const token of tokens) { const tokenParamLength = getTokenQueryParamLength(token); const separatorLength = batch.length === 0 ? 0 : 1; const nextBatchUrlLength = batchUrlLength + separatorLength + tokenParamLength; if (batch.length < BATCH_TOKEN_PRICES_LIMIT && nextBatchUrlLength <= BATCH_TOKEN_PRICES_MAX_URL_LENGTH) { batch.push(token); batchUrlLength = nextBatchUrlLength; continue; } if (batch.length > 0) { batches.push(batch); } const singleTokenUrl = getBatchTokenPricesUrl(apiBaseUrl, [token]); if (singleTokenUrl.length > BATCH_TOKEN_PRICES_MAX_URL_LENGTH) { throw new Error( `KSwap token price URL length ${singleTokenUrl.length} exceeds limit ${BATCH_TOKEN_PRICES_MAX_URL_LENGTH} for a single token: ${singleTokenUrl}` ); } batch = [token]; batchUrlLength = baseUrlLength + tokenParamLength; } if (batch.length > 0) { batches.push(batch); } return batches; }; const getResponseBodyForLog = async (response: Response): Promise<string> => { try { return await response.text(); } catch (error) { return `Failed to read response body: ${error instanceof Error ? error.message : String(error)}`; } }; export const getTokensPrices = async ( apiBaseUrl: string, tokens: Address[], logger: Logger = console ): Promise<Map<Address, Decimal>> => { if (tokens.length === 0) { return new Map<Address, Decimal>(); } const batches = getTokensPriceBatches(apiBaseUrl, tokens); const results = await Promise.all( batches.map(async (batch) => { const url = getBatchTokenPricesUrl(apiBaseUrl, batch); let response: Response; try { response = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); } catch (error) { logger.error(`Failed to fetch tokens batch price. URL: ${url}.`, error); return { batch, data: null }; } const responseBody = await getResponseBodyForLog(response); if (!response.ok) { logger.error( `Failed to fetch tokens batch price: ${response.status} ${response.statusText}. URL: ${url}. Response body: ${responseBody}` ); return { batch, data: null }; } try { const data = JSON.parse(responseBody) as KSwapBatchPriceResponse; if (data.success === false || !data.data) { logger.error(`Unexpected tokens batch price response. URL: ${url}. Response body: ${responseBody}`); return { batch, data: null }; } return { batch, data }; } catch (error) { logger.error(`Failed to parse tokens batch price response. URL: ${url}. Response body: ${responseBody}`, error); return { batch, data: null }; } }) ); const prices = new Map<Address, Decimal>(); for (const { batch, data } of results) { for (const token of batch) { const tokenData = data?.data[token]; if (tokenData && tokenData.value !== null && tokenData.value !== undefined) { try { const price = new Decimal(tokenData.value); prices.set(token, price); } catch (error) { logger.error(`Failed to parse price for token, setting to 0: ${token}:`, error); prices.set(token, new Decimal(0)); } } else { logger.warn(`No price data available for token ${token}, setting to 0`); prices.set(token, new Decimal(0)); } } } return prices; };