UNPKG

@moonwell-fi/moonwell-sdk

Version:

TypeScript Interface for Moonwell

587 lines (524 loc) 16.7 kB
/** * Lunar Indexer API Client * * Client for interacting with the Lunar Indexer REST API endpoints. * Provides functions for fetching comptroller, market, token, and portfolio data. */ import axios, { type AxiosInstance, type AxiosError } from "axios"; import { attachRetryInterceptor } from "./retry.js"; // ============================================================================ // Type Definitions // ============================================================================ export interface LunarIndexerConfig { baseUrl: string; timeout?: number; } export interface LunarPaginatedResponse<T> { results: T[]; nextCursor: string | null; } export interface LunarSnapshotOptions { limit?: number; cursor?: string; granularity?: "15m" | "1h" | "6h" | "1d"; startTime?: number; endTime?: number; } export interface LunarStakingSnapshotOptions { limit?: number; cursor?: string; granularity?: "1h" | "6h" | "1d"; startTime?: number; endTime?: number; } export interface LunarVaultStakingSnapshotOptions extends LunarStakingSnapshotOptions { vaultAddress?: string; } export interface LunarStakingSnapshot { id: string; chainId: number; stakingTokenAddress: string; timestamp: number; blockNumber: string; totalStaked: string; totalStakedUSD: string; wellPrice: string; timeInterval: number; } export interface LunarVaultStakingSnapshot { id: string; chainId: number; vaultAddress: string; timestamp: number; blockNumber: string; totalStaked: string; totalStakedUSD: string; underlyingPrice: string; timeInterval: number; } export interface LunarPortfolioOptions { startTime: number; endTime: number; granularity?: "1h" | "6h" | "1d"; chainId?: number; market?: string; } export interface LunarVaultPortfolioOptions { startTime: number; endTime: number; granularity?: "1h" | "6h" | "1d"; chainId?: number; vault?: string; } export interface LunarVaultPosition { chainId: number; vaultAddress: string; shareBalance: string; shareBalanceUsd: number; assetsValue: number; } export interface LunarVaultPortfolio { account: string; positions: Array<{ timestamp: number; vaults: LunarVaultPosition[]; }>; } export interface LunarComptroller { id: string; chainId: number; address: string; priceOracleAddress: string; } export interface LunarMarket { id: string; chainId: number; address: string; underlyingTokenAddress: string; collateralFactor: number; interestRateModelAddress: string; priceFeedAddress: string; reserveFactor: string; blockNumber: string; timestamp: number; } /** * Full market data with all real-time fields from Lunar Indexer * Based on actual API responses from /markets/:chainId and /market/:marketId */ export interface LunarMarketFull { id: string; chainId: number; address: string; underlyingTokenAddress: string; comptrollerAddress: string; totalBorrows: number; totalBorrowsUsd: number; totalSupply: number; totalSupplyUsd: number; totalReserves: number; totalReservesUsd: number; cash: number; cashUsd: number; badDebt: number; badDebtUsd: number; exchangeRate: number; priceUsd: number; baseSupplyApy: number; baseBorrowApy: number; mintPaused: boolean; borrowPaused: boolean; seizePaused: boolean; transferPaused: boolean; borrowCap: number; supplyCap: number; collateralFactor: number; reserveFactor: string; incentives: Array<{ token: string; supplyIncentivesPerSec: string | number; borrowIncentivesPerSec: string | number; priceUsd: number | null; supplyApr: number | null; borrowApr: number | null; }>; underlyingToken: { id: string; chainId: number; address: string; name: string; symbol: string; decimals: number; }; blockNumber: string; timestamp: number; } export interface LunarMarketWithToken extends LunarMarket { underlyingToken: LunarToken; } export interface LunarToken { id: string; chainId: number; address: string; name: string; symbol: string; decimals: number; } export interface LunarMarketSnapshot { id: string; chainId: number; marketAddress: string; timestamp: number; blockNumber: string; totalBorrows: number; totalBorrowsUSD: number; totalSupplies: number; totalSuppliesUSD: number; totalLiquidity: number; totalLiquidityUSD: number; totalReserves: number; totalReservesUSD: number; baseSupplyApy: number; baseBorrowApy: number; timeInterval: number; } export interface LunarPortfolio { account: string; positions: Array<{ timestamp: number; markets: Array<{ chainId: number; marketAddress: string; supplyBalance: string; supplyBalanceUsd: string; borrowBalance: string; borrowBalanceUsd: string; }>; }>; } // ============================================================================ // Error Handling // ============================================================================ export class LunarIndexerError extends Error { constructor( message: string, public readonly statusCode?: number, public readonly endpoint?: string, public readonly originalError?: Error, ) { super(message); this.name = "LunarIndexerError"; } } /** * Determine if an error should trigger fallback to Ponder/on-chain */ export function shouldFallback(error: unknown): boolean { if (axios.isAxiosError(error)) { const axiosError = error as AxiosError; const isNetworkError = !axiosError.response; const is5xxError = !!axiosError.response && axiosError.response.status >= 500; const is404Error = !!axiosError.response && axiosError.response.status === 404; if (isNetworkError || is5xxError || is404Error) { return true; } // 4xx errors (except 404) should NOT fallback - fail fast return false; } // Unknown errors should fallback return true; } export const DEFAULT_LUNAR_TIMEOUT_MS = 10_000; // ============================================================================ // Lunar Indexer Client Class // ============================================================================ export class LunarIndexerClient { private client: AxiosInstance; private stakingClient: AxiosInstance; private vaultsClient: AxiosInstance; constructor(config: LunarIndexerConfig) { this.client = axios.create({ baseURL: `${config.baseUrl}/api/v1/core`, timeout: config.timeout || DEFAULT_LUNAR_TIMEOUT_MS, headers: { "Content-Type": "application/json", }, }); this.stakingClient = axios.create({ baseURL: `${config.baseUrl}/api/v1/staking`, timeout: config.timeout || DEFAULT_LUNAR_TIMEOUT_MS, headers: { "Content-Type": "application/json", }, }); this.vaultsClient = axios.create({ baseURL: `${config.baseUrl}/api/v1/vaults`, timeout: config.timeout || DEFAULT_LUNAR_TIMEOUT_MS, headers: { "Content-Type": "application/json", }, }); // Retry transient failures (5xx, network errors, timeouts) silently before // surfacing to callers. 4xx (incl. 404) bypasses retries — see ./retry.ts. attachRetryInterceptor(this.client); attachRetryInterceptor(this.stakingClient); attachRetryInterceptor(this.vaultsClient); } /** * Get comptroller data for a specific chain */ async getComptroller(chainId: number): Promise<LunarComptroller> { try { const response = await this.client.get<LunarComptroller>( `/comptroller/${chainId}`, ); return response.data; } catch (error) { throw new LunarIndexerError( `Failed to fetch comptroller for chain ${chainId}`, axios.isAxiosError(error) ? error.response?.status : undefined, `/comptroller/${chainId}`, error as Error, ); } } /** * List all markets for a specific chain with pagination * Returns full market data with real-time values, APYs, and incentives */ async listMarkets( chainId: number, options?: { limit?: number; cursor?: string }, ): Promise<LunarPaginatedResponse<LunarMarketFull>> { try { const params: Record<string, string> = {}; if (options?.limit) params.limit = options.limit.toString(); if (options?.cursor) params.cursor = options.cursor; const response = await this.client.get< LunarPaginatedResponse<LunarMarketFull> >(`/markets/${chainId}`, { params }); return response.data; } catch (error) { throw new LunarIndexerError( `Failed to list markets for chain ${chainId}`, axios.isAxiosError(error) ? error.response?.status : undefined, `/markets/${chainId}`, error as Error, ); } } /** * Get a single market by marketId (format: chainId-marketAddress) * Returns full market data with real-time values, APYs, and incentives */ async getMarket(marketId: string): Promise<LunarMarketFull> { try { const response = await this.client.get<LunarMarketFull>( `/market/${marketId}`, ); return response.data; } catch (error) { throw new LunarIndexerError( `Failed to fetch market ${marketId}`, axios.isAxiosError(error) ? error.response?.status : undefined, `/market/${marketId}`, error as Error, ); } } /** * Get market snapshots with optional time range and granularity */ async getMarketSnapshots( marketId: string, options?: LunarSnapshotOptions, ): Promise<LunarPaginatedResponse<LunarMarketSnapshot>> { try { const params: Record<string, string> = {}; if (options?.limit) params.limit = options.limit.toString(); if (options?.cursor) params.cursor = options.cursor; if (options?.granularity) params.granularity = options.granularity; if (options?.startTime) params.startTime = options.startTime.toString(); if (options?.endTime) params.endTime = options.endTime.toString(); const response = await this.client.get< LunarPaginatedResponse<LunarMarketSnapshot> >(`/market/${marketId}/snapshots`, { params }); return response.data; } catch (error) { throw new LunarIndexerError( `Failed to fetch market snapshots for ${marketId}`, axios.isAxiosError(error) ? error.response?.status : undefined, `/market/${marketId}/snapshots`, error as Error, ); } } /** * List all tokens for a specific chain with pagination */ async listTokens( chainId: number, options?: { limit?: number; cursor?: string }, ): Promise<LunarPaginatedResponse<LunarToken>> { try { const params: Record<string, string> = {}; if (options?.limit) params.limit = options.limit.toString(); if (options?.cursor) params.cursor = options.cursor; const response = await this.client.get< LunarPaginatedResponse<LunarToken> >(`/tokens/${chainId}`, { params }); return response.data; } catch (error) { throw new LunarIndexerError( `Failed to list tokens for chain ${chainId}`, axios.isAxiosError(error) ? error.response?.status : undefined, `/tokens/${chainId}`, error as Error, ); } } /** * Get a single token by tokenId (format: chainId-tokenAddress) */ async getToken(tokenId: string): Promise<LunarToken> { try { const response = await this.client.get<LunarToken>(`/token/${tokenId}`); return response.data; } catch (error) { throw new LunarIndexerError( `Failed to fetch token ${tokenId}`, axios.isAxiosError(error) ? error.response?.status : undefined, `/token/${tokenId}`, error as Error, ); } } /** * Get account portfolio with historical positions * NOTE: USD fields (supplyBalanceUsd, borrowBalanceUsd) are being added by Lunar team */ async getAccountPortfolio( accountAddress: string, options: LunarPortfolioOptions, ): Promise<LunarPortfolio> { try { const params: Record<string, string> = { startTime: options.startTime.toString(), endTime: options.endTime.toString(), }; if (options.granularity) params.granularity = options.granularity; if (options.chainId) params.chainId = options.chainId.toString(); if (options.market) params.market = options.market; const response = await this.client.get<LunarPortfolio>( `/account/${accountAddress.toLowerCase()}/portfolio`, { params }, ); return response.data; } catch (error) { throw new LunarIndexerError( `Failed to fetch portfolio for account ${accountAddress}`, axios.isAxiosError(error) ? error.response?.status : undefined, `/account/${accountAddress}/portfolio`, error as Error, ); } } /** * Get staking snapshots for a specific chain (stkWELL staking) */ async getStakingSnapshots( chainId: number, options?: LunarStakingSnapshotOptions, ): Promise<LunarPaginatedResponse<LunarStakingSnapshot>> { try { const params: Record<string, string> = {}; if (options?.limit) params.limit = options.limit.toString(); if (options?.cursor) params.cursor = options.cursor; if (options?.granularity) params.granularity = options.granularity; if (options?.startTime) params.startTime = options.startTime.toString(); if (options?.endTime) params.endTime = options.endTime.toString(); const response = await this.stakingClient.get< LunarPaginatedResponse<LunarStakingSnapshot> >(`/snapshots/${chainId}`, { params }); return response.data; } catch (error) { throw new LunarIndexerError( `Failed to fetch staking snapshots for chain ${chainId}`, axios.isAxiosError(error) ? error.response?.status : undefined, `/snapshots/${chainId}`, error as Error, ); } } /** * Get vault account portfolio with historical positions */ async getVaultAccountPortfolio( accountAddress: string, options: LunarVaultPortfolioOptions, ): Promise<LunarVaultPortfolio> { try { const params: Record<string, string> = { startTime: options.startTime.toString(), endTime: options.endTime.toString(), }; if (options.granularity) params.granularity = options.granularity; if (options.chainId) params.chainId = options.chainId.toString(); if (options.vault) params.vault = options.vault; const response = await this.vaultsClient.get<LunarVaultPortfolio>( `/account/${accountAddress.toLowerCase()}/portfolio`, { params }, ); return response.data; } catch (error) { throw new LunarIndexerError( `Failed to fetch vault portfolio for account ${accountAddress}`, axios.isAxiosError(error) ? error.response?.status : undefined, `/account/${accountAddress}/portfolio`, error as Error, ); } } /** * Get vault staking snapshots for a specific chain (MetaMorpho vault staking) */ async getVaultStakingSnapshots( chainId: number, options?: LunarVaultStakingSnapshotOptions, ): Promise<LunarPaginatedResponse<LunarVaultStakingSnapshot>> { try { const params: Record<string, string> = {}; if (options?.limit) params.limit = options.limit.toString(); if (options?.cursor) params.cursor = options.cursor; if (options?.granularity) params.granularity = options.granularity; if (options?.startTime) params.startTime = options.startTime.toString(); if (options?.endTime) params.endTime = options.endTime.toString(); if (options?.vaultAddress) params.vaultAddress = options.vaultAddress; const response = await this.stakingClient.get< LunarPaginatedResponse<LunarVaultStakingSnapshot> >(`/vault-snapshots/${chainId}`, { params }); return response.data; } catch (error) { throw new LunarIndexerError( `Failed to fetch vault staking snapshots for chain ${chainId}`, axios.isAxiosError(error) ? error.response?.status : undefined, `/vault-snapshots/${chainId}`, error as Error, ); } } } // ============================================================================ // Factory Function // ============================================================================ /** * Create a new Lunar Indexer client instance */ export function createLunarIndexerClient( config: LunarIndexerConfig, ): LunarIndexerClient { return new LunarIndexerClient(config); }