@moonwell-fi/moonwell-sdk
Version:
TypeScript Interface for Moonwell
258 lines • 12.3 kB
JavaScript
/**
* 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