UNPKG

@moonwell-fi/moonwell-sdk

Version:

TypeScript Interface for Moonwell

258 lines 12.3 kB
/** * Lunar Indexer Response Transformation Utilities for Morpho Markets * * This module provides transformation functions to convert Lunar Indexer API responses * into SDK MorphoMarket types. The indexer returns string values that need to be converted * to proper numeric types and Amount objects. * * @module morpho/markets/lunarIndexerTransform */ import { parseUnits } from "viem"; import { Amount } from "../../../common/amount.js"; import { getWithRetry } from "../../axiosWithRetry.js"; /** * Transform a Lunar Indexer isolated market snapshot to SDK MarketSnapshot format * * @param options.normalizeToCollateral - Set true for markets where totalSupplyAssets * is denominated in the loan token but the chart needs collateral-equivalent units * (e.g. the USDC/ETH market where loan = WETH and collateral = USDC: the indexer * returns supply in WETH units, but we need USDC units for the chart). */ export function transformIsolatedMarketSnapshotFromIndexer(snapshot, options) { const collateralTokenPrice = Number.parseFloat(snapshot.collateralTokenPrice); const loanTokenPrice = Number.parseFloat(snapshot.loanTokenPrice); const totalSupplyAssetsUsd = Number.parseFloat(snapshot.totalSupplyAssetsUsd); const totalLiquidityUsd = Number.parseFloat(snapshot.totalLiquidityUsd); const needsUsdNormalization = options?.normalizeToCollateral === true; const totalSupply = needsUsdNormalization && collateralTokenPrice > 0 ? totalSupplyAssetsUsd / collateralTokenPrice : Number.parseFloat(snapshot.totalSupplyAssets); const totalLiquidity = needsUsdNormalization && collateralTokenPrice > 0 ? totalLiquidityUsd / collateralTokenPrice : Number.parseFloat(snapshot.totalLiquidity); const totalReallocatableLiquidity = Number.parseFloat(snapshot.totalReallocatableLiquidity ?? "0"); const totalReallocatableLiquidityUsd = Number.parseFloat(snapshot.totalReallocatableLiquidityUsd ?? "0"); return { chainId: snapshot.chainId, marketId: snapshot.marketId.toLowerCase(), timestamp: snapshot.timestamp * 1000, totalSupply, totalSupplyUsd: totalSupplyAssetsUsd, totalBorrows: Number.parseFloat(snapshot.totalBorrowAssets), totalBorrowsUsd: Number.parseFloat(snapshot.totalBorrowAssetsUsd), totalLiquidity, totalLiquidityUsd, totalReallocatableLiquidity, totalReallocatableLiquidityUsd, baseSupplyApy: Number.parseFloat(snapshot.supplyApy), baseBorrowApy: Number.parseFloat(snapshot.borrowApy), loanTokenPrice, collateralTokenPrice, }; } /** * Fetch markets from Lunar Indexer */ export async function fetchMarketsFromIndexer(lunarIndexerUrl, chainId, options) { const params = new URLSearchParams(); if (options?.includeRewards) { params.set("includeRewards", "true"); } const queryString = params.toString(); const url = `${lunarIndexerUrl}/api/v1/isolated/markets/${chainId}${queryString ? `?${queryString}` : ""}`; const response = await getWithRetry(url); return response.data; } /** * Fetch a single market from Lunar Indexer */ export async function fetchMarketFromIndexer(lunarIndexerUrl, chainId, marketId) { const url = `${lunarIndexerUrl}/api/v1/isolated/market/${chainId}/${marketId.toLowerCase()}`; const response = await getWithRetry(url); return response.data; } /** * Fetch market snapshots from Lunar Indexer */ export async function fetchMarketSnapshotsFromIndexer(lunarIndexerUrl, chainId, marketId, options) { const params = new URLSearchParams(); if (options?.startTime !== undefined) { params.set("startTime", options.startTime.toString()); } if (options?.endTime !== undefined) { params.set("endTime", options.endTime.toString()); } if (options?.limit !== undefined) { params.set("limit", options.limit.toString()); } if (options?.cursor !== undefined) { params.set("cursor", options.cursor); } if (options?.granularity !== undefined) { params.set("granularity", options.granularity); } const queryString = params.toString(); const url = `${lunarIndexerUrl}/api/v1/isolated/market/${chainId}/${marketId.toLowerCase()}/snapshots${queryString ? `?${queryString}` : ""}`; const response = await getWithRetry(url); return response.data; } /** * Fetch account market portfolio from Lunar Indexer */ export async function fetchAccountMarketPortfolioFromIndexer(lunarIndexerUrl, accountAddress, options) { const params = new URLSearchParams(); if (options?.chainId !== undefined) { params.set("chainId", options.chainId.toString()); } if (options?.marketId !== undefined) { params.set("marketId", options.marketId.toLowerCase()); } if (options?.startTime !== undefined) { params.set("startTime", options.startTime.toString()); } if (options?.endTime !== undefined) { params.set("endTime", options.endTime.toString()); } if (options?.granularity !== undefined) { params.set("granularity", options.granularity); } const queryString = params.toString(); const url = `${lunarIndexerUrl}/api/v1/isolated/account/${accountAddress.toLowerCase()}/portfolio${queryString ? `?${queryString}` : ""}`; const response = await getWithRetry(url); return response.data; } /** * Transform a single market from Lunar Indexer format to SDK MorphoMarket type */ export function transformMarketFromIndexer(indexerMarket, environment, rewardsData, sharedLiquidityData, collateralPriceOverride) { // Find the market configuration in the environment const marketKey = Object.keys(environment.config.morphoMarkets).find((key) => environment.config.morphoMarkets[key].id.toLowerCase() === indexerMarket.marketId.toLowerCase()); if (!marketKey) { return null; } const marketConfig = environment.config.morphoMarkets[marketKey]; // Get token configs from environment const loanToken = environment.config.tokens[marketConfig.loanToken]; const collateralToken = environment.config.tokens[marketConfig.collateralToken]; // Parse prices from indexer const loanTokenPrice = Number.parseFloat(indexerMarket.loanTokenPrice); const collateralTokenPrice = collateralPriceOverride ?? Number.parseFloat(indexerMarket.collateralTokenPrice); // Calculate oracle price const lltv = Number.parseFloat(indexerMarket.lltv); const lltvBigInt = BigInt(Math.floor(lltv * 10 ** 16)); // Convert percentage to 18 decimal bigint // Use parseUnits (viem) to convert decimal strings to BigInt — avoids float64 // precision loss for large USDC amounts (e.g. "1026834782.838286" * 10^6). // parseUnits can throw on malformed strings (e.g. scientific notation), so we // wrap each call and fall back to 0n to avoid crashing the entire market list. const safeParseUnits = (value, decimals) => { try { return parseUnits(value, decimals); } catch { return 0n; } }; const totalSupplyInLoanToken = new Amount(safeParseUnits(indexerMarket.totalSupplyAssets, loanToken.decimals), loanToken.decimals); const totalBorrows = new Amount(safeParseUnits(indexerMarket.totalBorrowAssets, loanToken.decimals), loanToken.decimals); const availableLiquidity = new Amount(safeParseUnits(indexerMarket.totalLiquidity, loanToken.decimals), loanToken.decimals); // Calculate oracle price for collateral conversion // Oracle price = collateralTokenPrice / loanTokenPrice with proper decimal adjustment const oraclePrice = loanTokenPrice > 0 ? collateralTokenPrice / loanTokenPrice : 0; // Calculate total supply in collateral token terms. // When oraclePrice = 0 (missing collateral price), return 0 rather than falling // back to oraclePrice=1 which would produce a meaningless loan-token-sized number. const totalSupply = new Amount(oraclePrice > 0 ? BigInt(Math.floor((totalSupplyInLoanToken.value / oraclePrice) * 10 ** collateralToken.decimals)) : 0n, collateralToken.decimals); // Parse APYs const baseSupplyApy = Number.parseFloat(indexerMarket.supplyApy); const baseBorrowApy = Number.parseFloat(indexerMarket.borrowApy); // Calculate USD values const totalSupplyUsd = Number.parseFloat(indexerMarket.totalSupplyAssetsUsd); const totalBorrowsUsd = Number.parseFloat(indexerMarket.totalBorrowAssetsUsd); const availableLiquidityUsd = Number.parseFloat(indexerMarket.totalLiquidityUsd); // Parse market params const marketParams = { loanToken: indexerMarket.loanToken.address, collateralToken: indexerMarket.collateralToken.address, oracle: indexerMarket.oracle, irm: indexerMarket.irm, lltv: lltvBigInt, }; // Find shared liquidity for this market const publicAllocatorSharedLiquidity = sharedLiquidityData || []; // Build the MorphoMarket object const market = { chainId: environment.chainId, marketId: indexerMarket.marketId, marketKey, deprecated: marketConfig.deprecated === true, loanToValue: lltv / 100, // Convert from percentage to decimal (94.5 -> 0.945) performanceFee: Number.parseFloat(indexerMarket.fee), loanToken, loanTokenPrice, collateralToken, collateralTokenPrice, collateralAssets: rewardsData?.collateralAssets ?? null, collateralAssetsUsd: rewardsData?.collateralAssetsUsd ?? null, totalSupply, totalSupplyUsd, totalSupplyInLoanToken, totalBorrows, totalBorrowsUsd, availableLiquidity, availableLiquidityUsd, marketParams, baseSupplyApy, baseBorrowApy, rewardsSupplyApy: rewardsData?.rewardsSupplyApy ?? 0, rewardsBorrowApy: rewardsData?.rewardsBorrowApy ?? 0, totalSupplyApr: baseSupplyApy + (rewardsData?.rewardsSupplyApy ?? 0), totalBorrowApr: baseBorrowApy + (rewardsData?.rewardsBorrowApy ?? 0), rewards: rewardsData?.rewards ?? [], publicAllocatorSharedLiquidity, }; return market; } /** * Transform multiple markets from Lunar Indexer format to SDK MorphoMarket types */ // stkWELL is not priced correctly by the indexer: its oracle is the same as WELL's oracle // and the two tokens trade at the same price. Override with the WELL price to avoid // displaying zero or incorrect collateral prices for stkWELL markets. export const PRICE_ALIAS = { stkWELL: "WELL", }; export function transformMarketsFromIndexer(indexerMarkets, environment, rewardsDataMap, sharedLiquidityMap) { // Build symbol → price map from markets that have a known price. const tokenPriceBySymbol = new Map(); const setPriceWithCheck = (symbol, price) => { tokenPriceBySymbol.set(symbol, price); }; for (const m of indexerMarkets) { const loanPrice = Number.parseFloat(m.loanTokenPrice); const collateralPrice = Number.parseFloat(m.collateralTokenPrice); if (loanPrice > 0) setPriceWithCheck(m.loanToken.symbol, loanPrice); if (collateralPrice > 0) setPriceWithCheck(m.collateralToken.symbol, collateralPrice); } return indexerMarkets.flatMap((indexerMarket) => { const rewardKey = `${environment.chainId}-${indexerMarket.marketId.toLowerCase()}`; const rewardsData = rewardsDataMap?.get(rewardKey); const sharedLiquidityData = sharedLiquidityMap?.get(rewardKey); // Resolve price override for tokens that alias another token's price const collateralSymbol = indexerMarket.collateralToken.symbol; const collateralPrice = Number.parseFloat(indexerMarket.collateralTokenPrice); const aliasSymbol = PRICE_ALIAS[collateralSymbol]; const collateralPriceOverride = collateralPrice === 0 && aliasSymbol ? tokenPriceBySymbol.get(aliasSymbol) : undefined; const market = transformMarketFromIndexer(indexerMarket, environment, rewardsData, sharedLiquidityData, collateralPriceOverride); return market ? [market] : []; }); } //# sourceMappingURL=lunarIndexerTransform.js.map