UNPKG

@moonwell-fi/moonwell-sdk

Version:

TypeScript Interface for Moonwell

298 lines 13 kB
/** * Lunar Indexer Response Transformation Utilities * * This module provides transformation functions to convert Lunar Indexer API responses * into SDK MorphoVault types. The indexer returns string values that need to be converted * to proper numeric types and Amount objects. * * @module morpho/vaults/lunarIndexerTransform */ import { Amount } from "../../../common/amount.js"; /** * Build a TokenConfig from Lunar Indexer token data */ function buildTokenConfig(token) { return { address: token.address, symbol: token.symbol, name: token.name, decimals: token.decimals, }; } /** * Build a TokenConfig from vault data (for the vault token itself) */ function buildVaultTokenConfig(vault) { return { address: vault.address, symbol: vault.symbol, name: vault.name, decimals: vault.decimals, }; } /** * Transform a single market from Lunar Indexer format to SDK format */ function transformMarket(market, underlyingDecimals, totalAssets, tokenMap) { // Get collateral token info from map const collateralToken = tokenMap.get(market.marketCollateral.toLowerCase()); const vaultSupplied = Number.parseFloat(market.vaultSupplied); const allocation = totalAssets > 0 ? vaultSupplied / totalAssets : 0; return { marketId: market.marketId, allocation, marketApy: Number.parseFloat(market.marketApy), marketLoanToValue: Number.parseFloat(market.marketLltv), marketCollateral: collateralToken ? buildTokenConfig(collateralToken) : { // Fallback if token not in map address: market.marketCollateral, symbol: market.marketCollateralSymbol, name: market.marketCollateralName, decimals: 18, // Default fallback - should be populated by indexer }, marketLiquidity: new Amount(Number.parseFloat(market.marketLiquidity), underlyingDecimals), marketLiquidityUsd: Number.parseFloat(market.marketLiquidityUsd), totalSupplied: new Amount(vaultSupplied, underlyingDecimals), totalSuppliedUsd: Number.parseFloat(market.vaultSuppliedUsd), rewards: [], // Rewards are at vault level, not market level in indexer response }; } /** * Transform rewards from Lunar Indexer format to SDK format */ function transformRewards(rewards, tokenMap) { return rewards.map((reward) => { const token = tokenMap.get(reward.token.toLowerCase()); return { asset: token ? buildTokenConfig(token) : { address: reward.token, symbol: reward.tokenSymbol, name: reward.tokenSymbol, decimals: 18, // Default fallback }, supplyApr: Number.parseFloat(reward.apr), supplyAmount: 0, // Not provided by indexer borrowApr: 0, borrowAmount: 0, }; }); } /** * Find vault configuration from environment by address */ function findVaultConfigByAddress(environment, address) { const vaultKey = Object.keys(environment.config.vaults).find((key) => environment.config.tokens[key]?.address.toLowerCase() === address.toLowerCase()); return { key: vaultKey, config: vaultKey ? environment.config.vaults[vaultKey] : undefined, }; } /** * Transform a single vault from Lunar Indexer format to SDK MorphoVault format * * @param indexerVault - Vault data from Lunar Indexer * @param environment - SDK environment for this chain * @param tokenMap - Map of token addresses to token data for lookups * @returns Transformed MorphoVault object */ export function transformVaultFromIndexer(indexerVault, environment, tokenMap) { // Get underlying token info const underlyingToken = indexerVault.underlyingToken || tokenMap.get(indexerVault.underlyingTokenAddress.toLowerCase()); if (!underlyingToken) { throw new Error(`Underlying token not found: ${indexerVault.underlyingTokenAddress}`); } const underlyingDecimals = underlyingToken.decimals; // Find vault config from environment const { key: vaultKey, config: vaultConfig } = findVaultConfigByAddress(environment, indexerVault.address); // Resolve SDK token configs (preferred over API data for known vaults) const sdkVaultTokenConfig = vaultConfig ? environment.config.tokens[vaultConfig.vaultToken] : undefined; const sdkUnderlyingTokenConfig = vaultConfig ? environment.config.tokens[vaultConfig.underlyingToken] : undefined; // Determine version from config; unknown vaults default to V1 const version = vaultConfig?.version ?? 1; // Parse numeric values const totalAssetsValue = Number.parseFloat(indexerVault.totalAssets); const totalLiquidityValue = Number.parseFloat(indexerVault.totalLiquidity); // Calculate vault supply (total assets deployed, not in liquidity). // Guard against negative values in case of transient data inconsistency // where totalLiquidity briefly exceeds totalAssets. const vaultSupplyValue = Math.max(0, totalAssetsValue - totalLiquidityValue); // Transform markets const markets = indexerVault.markets.map((market) => transformMarket(market, underlyingDecimals, totalAssetsValue, tokenMap)); // Transform rewards const rewards = transformRewards(indexerVault.rewards, tokenMap); // Build vault object const vault = { chainId: indexerVault.chainId, vaultKey: vaultKey || `unknown_${indexerVault.address}`, version, deprecated: vaultConfig?.deprecated ?? false, vaultToken: sdkVaultTokenConfig ?? buildVaultTokenConfig(indexerVault), underlyingToken: sdkUnderlyingTokenConfig ?? buildTokenConfig(underlyingToken), totalSupply: new Amount(totalAssetsValue, underlyingDecimals), totalLiquidity: new Amount(totalLiquidityValue, underlyingDecimals), vaultSupply: new Amount(vaultSupplyValue, underlyingDecimals), totalSupplyUsd: Number.parseFloat(indexerVault.totalAssetsUsd), totalLiquidityUsd: Number.parseFloat(indexerVault.totalLiquidityUsd), underlyingPrice: Number.parseFloat(indexerVault.underlyingPrice), baseApy: Number.parseFloat(indexerVault.baseApy), rewardsApy: Number.parseFloat(indexerVault.rewardsApy), totalApy: Number.parseFloat(indexerVault.totalApy), // Indexer returns performanceFee as a percentage string (e.g. "15" = 15%). // SDK consumers expect a 0-1 fraction, so divide by 100. // If the indexer ever changes to return 0-1 directly, remove the division. performanceFee: Number.parseFloat(indexerVault.performanceFee) / 100, timelock: Number.parseInt(indexerVault.timelock) / (60 * 60), // Convert seconds to hours curators: indexerVault.curators || [], // Will be populated by indexer or empty markets, rewards, // Staking fields (will be populated separately if multiReward configured) totalStaked: new Amount(0n, underlyingDecimals), totalStakedUsd: 0, stakingRewardsApr: 0, totalStakingApr: Number.parseFloat(indexerVault.baseApy), // Initially same as baseApy stakingRewards: [], }; return vault; } /** * Transform multiple vaults from Lunar Indexer format * * @param indexerVaults - Array of vault data from Lunar Indexer * @param environment - SDK environment for this chain * @param tokenMap - Map of token addresses to token data for lookups * @returns Array of transformed MorphoVault objects */ export function transformVaultsFromIndexer(indexerVaults, environment, tokenMap) { return indexerVaults.map((vault) => transformVaultFromIndexer(vault, environment, tokenMap)); } /** * Fetch tokens from Lunar Indexer and create a lookup map * * @param lunarIndexerUrl - Base URL for Lunar Indexer API * @param chainId - Chain ID to fetch tokens for * @returns Map of token address (lowercase) to token data */ export async function fetchTokenMap(lunarIndexerUrl, chainId) { const url = `${lunarIndexerUrl}/api/v1/vaults/tokens/${chainId}`; const response = await fetch(url, { signal: AbortSignal.timeout(10_000) }); if (!response.ok) { throw new Error(`Failed to fetch tokens from Lunar Indexer: ${response.status} ${response.statusText}`); } const data = await response.json(); const tokenMap = new Map(); for (const token of data.results) { tokenMap.set(token.address.toLowerCase(), token); } return tokenMap; } /** * Fetch vaults from Lunar Indexer * * @param lunarIndexerUrl - Base URL for Lunar Indexer API * @param chainId - Chain ID to fetch vaults for * @param options - Optional query parameters * @returns Lunar Indexer vaults response */ export async function fetchVaultsFromIndexer(lunarIndexerUrl, chainId, options) { const params = new URLSearchParams(); if (options?.limit) params.set("limit", options.limit.toString()); if (options?.cursor) params.set("cursor", options.cursor); if (options?.includeRewards) params.set("includeRewards", "true"); const url = `${lunarIndexerUrl}/api/v1/vaults/vaults/${chainId}${params.toString() ? `?${params.toString()}` : ""}`; const response = await fetch(url, { signal: AbortSignal.timeout(10_000) }); if (!response.ok) { throw new Error(`Failed to fetch vaults from Lunar Indexer: ${response.status} ${response.statusText}`); } return response.json(); } /** * Fetch single vault from Lunar Indexer * * @param lunarIndexerUrl - Base URL for Lunar Indexer API * @param vaultId - Vault ID in format "chainId-address" * @returns Single vault data with underlyingToken populated */ export async function fetchVaultFromIndexer(lunarIndexerUrl, vaultId) { const url = `${lunarIndexerUrl}/api/v1/vaults/vault/${vaultId}`; const response = await fetch(url, { signal: AbortSignal.timeout(10_000) }); if (!response.ok) { throw new Error(`Failed to fetch vault from Lunar Indexer: ${response.status} ${response.statusText}`); } return response.json(); } /** * Fetch vault snapshots from Lunar Indexer * * @param lunarIndexerUrl - Base URL for Lunar Indexer API * @param vaultId - Vault ID in format "chainId-address" * @param options - Optional query parameters * @returns Lunar Indexer vault snapshots response */ export async function fetchVaultSnapshotsFromIndexer(lunarIndexerUrl, vaultId, options) { const params = new URLSearchParams(); if (options?.cursor) params.set("cursor", options.cursor); if (options?.limit) params.set("limit", options.limit.toString()); if (options?.granularity) params.set("granularity", options.granularity); if (options?.startTime) params.set("startTime", options.startTime.toString()); if (options?.endTime) params.set("endTime", options.endTime.toString()); const queryString = params.toString(); const url = `${lunarIndexerUrl}/api/v1/vaults/vault/${vaultId}/snapshots${queryString ? `?${queryString}` : ""}`; const response = await fetch(url, { signal: AbortSignal.timeout(10_000) }); if (!response.ok) { throw new Error(`Failed to fetch vault snapshots from Lunar Indexer: ${response.status} ${response.statusText}`); } return response.json(); } /** * Transform vault snapshots from Lunar Indexer format to SDK MorphoVaultSnapshot format * * @param snapshots - Array of snapshot data from Lunar Indexer * @param chainId - Chain ID for the snapshots * @returns Array of transformed MorphoVaultSnapshot objects */ /** * Returns the V1 vault key paired with a given vault, or undefined if the vault * has no V1 pair (i.e. it is already a V1 vault or has no v1VaultKey configured). */ export function getV1VaultKey(environment, vaultKey) { const rawKey = environment.config.vaults[vaultKey]?.v1VaultKey; return typeof rawKey === "string" ? rawKey : undefined; } export function transformVaultSnapshotsFromIndexer(snapshots, chainId) { return snapshots.map((snapshot) => { const totalAssets = Number.parseFloat(snapshot.totalAssets); const totalAssetsUsd = Number.parseFloat(snapshot.totalAssetsUsd); const totalLiquidity = Number.parseFloat(snapshot.totalLiquidity); const totalLiquidityUsd = Number.parseFloat(snapshot.totalLiquidityUsd); return { chainId, vaultAddress: snapshot.vaultAddress.toLowerCase(), totalSupply: totalAssets, totalSupplyUsd: totalAssetsUsd, totalBorrows: totalAssets - totalLiquidity, totalBorrowsUsd: totalAssetsUsd - totalLiquidityUsd, totalLiquidity, totalLiquidityUsd, timestamp: snapshot.timestamp * 1000, }; }); } //# sourceMappingURL=lunarIndexerTransform.js.map