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