UNPKG

@moonwell-fi/moonwell-sdk

Version:

TypeScript Interface for Moonwell

490 lines 24.6 kB
import { zeroAddress } from "viem"; import { Amount, DAYS_PER_YEAR, calculateApy, perDay, } from "../../../common/index.js"; import { MOONWELL_FETCH_JSON_HEADERS } from "../../../common/fetch-headers.js"; import { publicEnvironments, } from "../../../environments/index.js"; import { findMarketByAddress, findTokenByAddress, } from "../../../environments/utils/index.js"; export const getMarketsData = async (environment) => { // Moonriver (chainId 1285) should always use on-chain data const isMoonriver = environment.chainId === 1285; if (environment.lunarIndexerUrl && !isMoonriver) { try { return await fetchMarketsFromLunar(environment); } catch (error) { console.warn(`[getMarketsData] Lunar Indexer failed for chain ${environment.chainId}, falling back to on-chain:`, error); environment.onError?.(error, { source: "markets", chainId: environment.chainId, }); } } const homeEnvironment = Object.values(publicEnvironments).find((e) => e.custom?.governance?.chainIds?.includes(environment.chainId)) || environment; const viewsContract = environment.contracts.views; const homeViewsContract = homeEnvironment.contracts.views; const [protocolInfoResult, allMarketsInfoResult, nativePriceResult, govPriceResult,] = await Promise.allSettled([ viewsContract?.read.getProtocolInfo(), viewsContract?.read.getAllMarketsInfo(), homeViewsContract?.read.getNativeTokenPrice(), homeViewsContract?.read.getGovernanceTokenPrice(), ]); // If getAllMarketsInfo failed (e.g. broken on-chain oracle), fall back to // per-mToken RPC calls. This handles deprecated chains like Moonriver where // the price oracle is non-functional but individual mToken data is readable. if (allMarketsInfoResult.status === "rejected") { console.debug("[mToken fallback] getAllMarketsInfo failed, using per-mToken fallback:", allMarketsInfoResult.reason); const seizePaused = protocolInfoResult.status === "fulfilled" && protocolInfoResult.value !== undefined ? protocolInfoResult.value.seizePaused : false; const transferPaused = protocolInfoResult.status === "fulfilled" && protocolInfoResult.value !== undefined ? protocolInfoResult.value.transferPaused : false; return await getMarketsFromMTokenFallback(environment, seizePaused, transferPaused); } const { seizePaused, transferPaused } = protocolInfoResult.status === "fulfilled" && protocolInfoResult.value !== undefined ? protocolInfoResult.value : { seizePaused: false, transferPaused: false }; const allMarketsInfo = allMarketsInfoResult.value; if (!allMarketsInfo) { environment.onError?.(new Error("getAllMarketsInfo returned undefined"), { source: "markets-onchain-missing-views", chainId: environment.chainId, }); return []; } const nativeTokenPriceRaw = nativePriceResult.status === "fulfilled" && nativePriceResult.value !== undefined ? nativePriceResult.value : 0n; const governanceTokenPriceRaw = govPriceResult.status === "fulfilled" && govPriceResult.value !== undefined ? govPriceResult.value : 0n; const governanceTokenPrice = new Amount(governanceTokenPriceRaw, 18); const nativeTokenPrice = new Amount(nativeTokenPriceRaw, 18); const markets = []; const tokenPrices = allMarketsInfo .map((marketInfo) => { const marketFound = findMarketByAddress(environment, marketInfo.market); if (marketFound) { return { token: marketFound.underlyingToken, tokenPrice: new Amount(marketInfo.underlyingPrice, 36 - marketFound.underlyingToken.decimals), }; } else { return; } }) .filter((token) => !!token); for (const marketInfo of allMarketsInfo) { const marketFound = findMarketByAddress(environment, marketInfo.market); if (marketFound) { const { marketConfig, marketToken, underlyingToken, marketKey } = marketFound; let badDebt = new Amount(0n, underlyingToken.decimals); if (marketConfig.badDebt === true) { try { const badDebtResult = await environment.markets[marketKey]?.read.badDebt(); badDebt = new Amount(badDebtResult, underlyingToken.decimals); } catch (error) { // ignore } } const supplyCaps = new Amount(marketInfo.supplyCap, underlyingToken.decimals); const borrowCaps = new Amount(marketInfo.borrowCap, underlyingToken.decimals); const collateralFactor = new Amount(marketInfo.collateralFactor, 18) .value; const underlyingPrice = new Amount(marketInfo.underlyingPrice, 36 - underlyingToken.decimals).value; const marketTotalSupply = new Amount(marketInfo.totalSupply, marketToken.decimals); const totalBorrows = new Amount(marketInfo.totalBorrows, underlyingToken.decimals); const totalReserves = new Amount(marketInfo.totalReserves, underlyingToken.decimals); const cash = new Amount(marketInfo.cash, underlyingToken.decimals); const exchangeRate = new Amount(marketInfo.exchangeRate, 10 + underlyingToken.decimals).value; const reserveFactor = new Amount(marketInfo.reserveFactor, 18).value; const borrowRate = new Amount(marketInfo.borrowRate, 18); const supplyRate = new Amount(marketInfo.supplyRate, 18); const totalSupply = new Amount(marketTotalSupply.value * exchangeRate, underlyingToken.decimals); const badDebtUsd = badDebt.value * underlyingPrice; const totalSupplyUsd = totalSupply.value * underlyingPrice; const totalBorrowsUsd = totalBorrows.value * underlyingPrice; const totalReservesUsd = totalReserves.value * underlyingPrice; const supplyCapsUsd = supplyCaps.value * underlyingPrice; const borrowCapsUsd = borrowCaps.value * underlyingPrice; const baseSupplyApy = calculateApy(supplyRate.value); const baseBorrowApy = calculateApy(borrowRate.value); const market = { marketKey, chainId: environment.chainId, seizePaused, transferPaused, mintPaused: marketInfo.mintPaused, borrowPaused: marketInfo.borrowPaused, deprecated: marketConfig.deprecated === true, borrowCaps, borrowCapsUsd, cash, collateralFactor, exchangeRate, marketToken, reserveFactor, supplyCaps, supplyCapsUsd, badDebt, badDebtUsd, totalBorrows, totalBorrowsUsd, totalReserves, totalReservesUsd, totalSupply, totalSupplyUsd, underlyingPrice, underlyingToken, baseBorrowApy, baseSupplyApy, totalBorrowApr: 0, totalSupplyApr: 0, rewards: [], }; for (const incentive of marketInfo.incentives) { let { borrowIncentivesPerSec, supplyIncentivesPerSec, token: tokenAddress, } = incentive; const token = findTokenByAddress(environment, tokenAddress); if (token) { const isGovernanceToken = token.symbol === environment.custom?.governance?.token; const isNativeToken = token.address === zeroAddress; const tokenPrice = tokenPrices.find((r) => r?.token.address === incentive.token)?.tokenPrice.value; const price = isNativeToken ? nativeTokenPrice.value : isGovernanceToken ? governanceTokenPrice.value : tokenPrice; if (price) { // On-chain contracts use borrowIncentivesPerSec=1 (1 wei) as a // placeholder when there are no active borrow incentives, because // setting it to 0 triggers a known smart contract bug. Treat as zero. if (borrowIncentivesPerSec === 1n) { borrowIncentivesPerSec = 0n; } const supplyRewardsPerDayUsd = perDay(new Amount(supplyIncentivesPerSec, token.decimals).value) * price; const borrowRewardsPerDayUsd = perDay(new Amount(borrowIncentivesPerSec, token.decimals).value) * price; const supplyApr = totalSupplyUsd === 0 ? 0 : (supplyRewardsPerDayUsd / totalSupplyUsd) * DAYS_PER_YEAR * 100; // Negative: borrow reward APR reduces the effective borrowing cost const borrowApr = totalBorrowsUsd === 0 ? 0 : (borrowRewardsPerDayUsd / totalBorrowsUsd) * DAYS_PER_YEAR * 100 * -1; market.rewards.push({ liquidStakingApr: 0, borrowApr, supplyApr, token, }); } } } market.totalSupplyApr = market.rewards.reduce((prev, curr) => prev + curr.supplyApr, market.baseSupplyApy); market.totalBorrowApr = market.rewards.reduce((prev, curr) => prev + curr.borrowApr, market.baseBorrowApy); markets.push(market); } } return markets; }; /** * Fallback for chains whose on-chain price oracle is non-functional (e.g. * deprecated Moonriver). Reads raw mToken contract data individually via * Promise.allSettled so a single failed call does not abort the entire chain. * All USD/price values are set to 0 since oracle prices are unavailable. */ async function getMarketsFromMTokenFallback(environment, seizePaused, transferPaused) { const markets = []; for (const marketKey of Object.keys(environment.config.markets)) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const envAny = environment; const marketConfig = envAny.config.markets[marketKey]; if (!marketConfig) continue; const underlyingToken = envAny.config.tokens[marketConfig.underlyingToken]; const marketToken = envAny.config.tokens[marketConfig.marketToken]; if (!underlyingToken || !marketToken) continue; const mTokenContract = envAny.markets[marketKey]; if (!mTokenContract) continue; const [totalSupplyResult, totalBorrowsResult, totalReservesResult, cashResult, exchangeRateResult, supplyRateResult, borrowRateResult, reserveFactorResult,] = await Promise.allSettled([ mTokenContract.read.totalSupply(), mTokenContract.read.totalBorrows(), mTokenContract.read.totalReserves(), mTokenContract.read.getCash(), mTokenContract.read.exchangeRateStored(), mTokenContract.read.supplyRatePerTimestamp(), mTokenContract.read.borrowRatePerTimestamp(), mTokenContract.read.reserveFactorMantissa(), ]); const totalSupplyRaw = totalSupplyResult.status === "fulfilled" ? totalSupplyResult.value : 0n; const totalBorrowsRaw = totalBorrowsResult.status === "fulfilled" ? totalBorrowsResult.value : 0n; const totalReservesRaw = totalReservesResult.status === "fulfilled" ? totalReservesResult.value : 0n; const cashRaw = cashResult.status === "fulfilled" ? cashResult.value : 0n; // Default exchange rate of 1.0: 10^(10 + underlyingDecimals) in raw form const exchangeRateRaw = exchangeRateResult.status === "fulfilled" ? exchangeRateResult.value : 10n ** BigInt(10 + underlyingToken.decimals); const supplyRateRaw = supplyRateResult.status === "fulfilled" ? supplyRateResult.value : 0n; const borrowRateRaw = borrowRateResult.status === "fulfilled" ? borrowRateResult.value : 0n; const reserveFactorRaw = reserveFactorResult.status === "fulfilled" ? reserveFactorResult.value : 0n; const exchangeRate = new Amount(exchangeRateRaw, 10 + underlyingToken.decimals).value; const marketTotalSupply = new Amount(totalSupplyRaw, marketToken.decimals); const totalSupply = new Amount(marketTotalSupply.value * exchangeRate, underlyingToken.decimals); const totalBorrows = new Amount(totalBorrowsRaw, underlyingToken.decimals); const totalReserves = new Amount(totalReservesRaw, underlyingToken.decimals); const cash = new Amount(cashRaw, underlyingToken.decimals); const supplyRate = new Amount(supplyRateRaw, 18); const borrowRate = new Amount(borrowRateRaw, 18); const reserveFactor = new Amount(reserveFactorRaw, 18).value; const baseSupplyApy = calculateApy(supplyRate.value); const baseBorrowApy = calculateApy(borrowRate.value); const market = { marketKey, chainId: environment.chainId, seizePaused, transferPaused, // Oracle is non-functional so supply/borrow would fail on-chain; mark paused mintPaused: true, borrowPaused: true, deprecated: marketConfig.deprecated === true, borrowCaps: new Amount(0n, underlyingToken.decimals), borrowCapsUsd: 0, cash, collateralFactor: 0, exchangeRate, marketToken, reserveFactor, supplyCaps: new Amount(0n, underlyingToken.decimals), supplyCapsUsd: 0, badDebt: new Amount(0n, underlyingToken.decimals), badDebtUsd: 0, totalBorrows, totalBorrowsUsd: 0, totalReserves, totalReservesUsd: 0, totalSupply, totalSupplyUsd: 0, underlyingPrice: 0, underlyingToken, baseBorrowApy, baseSupplyApy, totalBorrowApr: baseBorrowApy, totalSupplyApr: baseSupplyApy, rewards: [], }; markets.push(market); } return markets; } /** * Fetch markets data from Lunar Indexer (hybrid approach) * * Uses Lunar for core market data and conditionally: * - If Lunar provides priceUsd/supplyApr/borrowApr in incentives: use those (NO RPC calls) * - If Lunar fields are null: fetch governance/native token prices for reward APR calculations (RPC calls) * - Always fetch liquid staking APRs from external APIs */ async function fetchMarketsFromLunar(environment) { if (!environment.lunarIndexerUrl) { throw new Error("Lunar Indexer URL not configured"); } // Import client dynamically to avoid circular dependencies const { createLunarIndexerClient, DEFAULT_LUNAR_TIMEOUT_MS } = await import("../../lunar-indexer-client.js"); const client = createLunarIndexerClient({ baseUrl: environment.lunarIndexerUrl, timeout: DEFAULT_LUNAR_TIMEOUT_MS, }); const lunarMarketsResponse = await client.listMarkets(environment.chainId); const lunarMarkets = lunarMarketsResponse.results; const needsRpcPrices = lunarMarkets.some((market) => market.incentives.some((incentive) => incentive.priceUsd === null || incentive.supplyApr === null || incentive.borrowApr === null)); let governanceTokenPrice; let nativeTokenPrice; if (needsRpcPrices) { const homeEnvironment = Object.values(publicEnvironments).find((e) => e.custom?.governance?.chainIds?.includes(environment.chainId)) || environment; const [nativeTokenPriceRaw, governanceTokenPriceRaw] = await Promise.all([ homeEnvironment.contracts.views?.read.getNativeTokenPrice(), homeEnvironment.contracts.views?.read.getGovernanceTokenPrice(), ]); if (!nativeTokenPriceRaw || !governanceTokenPriceRaw) { throw new Error("Failed to fetch native or governance token prices from home chain"); } governanceTokenPrice = new Amount(governanceTokenPriceRaw, 18); nativeTokenPrice = new Amount(nativeTokenPriceRaw, 18); } const markets = []; for (const lunarMarket of lunarMarkets) { const marketFound = findMarketByAddress(environment, lunarMarket.address); if (!marketFound) { continue; } const { marketConfig, marketToken, underlyingToken, marketKey } = marketFound; // Transform Lunar decimal numbers to SDK Amount types // Note: Number() wrapping is defensive — the Lunar API may return numeric // fields as strings, which would break BigInt conversion via Math.floor. const totalSupply = new Amount(BigInt(Math.floor(Number(lunarMarket.totalSupply) * 10 ** underlyingToken.decimals)), underlyingToken.decimals); const totalBorrows = new Amount(BigInt(Math.floor(Number(lunarMarket.totalBorrows) * 10 ** underlyingToken.decimals)), underlyingToken.decimals); const totalReserves = new Amount(BigInt(Math.floor(Number(lunarMarket.totalReserves) * 10 ** underlyingToken.decimals)), underlyingToken.decimals); const cash = new Amount(BigInt(Math.floor(Number(lunarMarket.cash) * 10 ** underlyingToken.decimals)), underlyingToken.decimals); const badDebt = new Amount(BigInt(Math.floor(Number(lunarMarket.badDebt) * 10 ** underlyingToken.decimals)), underlyingToken.decimals); const supplyCaps = new Amount(BigInt(Math.floor(Number(lunarMarket.supplyCap) * 10 ** underlyingToken.decimals)), underlyingToken.decimals); const borrowCaps = new Amount(BigInt(Math.floor(Number(lunarMarket.borrowCap) * 10 ** underlyingToken.decimals)), underlyingToken.decimals); // Lunar provides reserveFactor as wei string, convert to decimal const reserveFactor = new Amount(BigInt(lunarMarket.reserveFactor), 18) .value; const market = { marketKey, chainId: environment.chainId, seizePaused: lunarMarket.seizePaused, transferPaused: lunarMarket.transferPaused, mintPaused: lunarMarket.mintPaused, borrowPaused: lunarMarket.borrowPaused, deprecated: marketConfig.deprecated === true, borrowCaps, borrowCapsUsd: Number(lunarMarket.borrowCap) * Number(lunarMarket.priceUsd), cash, collateralFactor: Number(lunarMarket.collateralFactor), exchangeRate: Number(lunarMarket.exchangeRate), marketToken, reserveFactor, supplyCaps, supplyCapsUsd: Number(lunarMarket.supplyCap) * Number(lunarMarket.priceUsd), badDebt, badDebtUsd: Number(lunarMarket.badDebtUsd), totalBorrows, totalBorrowsUsd: Number(lunarMarket.totalBorrowsUsd), totalReserves, totalReservesUsd: Number(lunarMarket.totalReservesUsd), totalSupply, totalSupplyUsd: Number(lunarMarket.totalSupplyUsd), underlyingPrice: Number(lunarMarket.priceUsd), underlyingToken, baseBorrowApy: Number(lunarMarket.baseBorrowApy), baseSupplyApy: Number(lunarMarket.baseSupplyApy), totalBorrowApr: 0, totalSupplyApr: 0, rewards: [], }; for (const incentive of lunarMarket.incentives) { const token = findTokenByAddress(environment, incentive.token); if (!token) { continue; } let supplyApr; let borrowApr; // On-chain contracts use borrowIncentivesPerSec=1 (1 wei) as a // placeholder when there are no active borrow incentives, because // setting it to 0 triggers a known smart contract bug. Treat as zero. const isBorrowPlaceholder = BigInt(incentive.borrowIncentivesPerSec) === 1n; if (incentive.priceUsd !== null && incentive.supplyApr !== null && incentive.borrowApr !== null) { supplyApr = Number(incentive.supplyApr); borrowApr = isBorrowPlaceholder ? 0 : -Number(incentive.borrowApr); } else { const isGovernanceToken = token.symbol === environment.custom?.governance?.token; const isNativeToken = token.address === zeroAddress; const price = isNativeToken ? nativeTokenPrice?.value : isGovernanceToken ? governanceTokenPrice?.value : undefined; if (!price) { continue; } const borrowIncentivesPerSec = isBorrowPlaceholder ? 0n : BigInt(incentive.borrowIncentivesPerSec); const supplyIncentivesPerSec = BigInt(incentive.supplyIncentivesPerSec); const supplyRewardsPerDayUsd = perDay(new Amount(supplyIncentivesPerSec, token.decimals).value) * price; const borrowRewardsPerDayUsd = perDay(new Amount(borrowIncentivesPerSec, token.decimals).value) * price; supplyApr = Number(lunarMarket.totalSupplyUsd) === 0 ? 0 : (supplyRewardsPerDayUsd / Number(lunarMarket.totalSupplyUsd)) * DAYS_PER_YEAR * 100; // Negative: borrow reward APR reduces the effective borrowing cost borrowApr = Number(lunarMarket.totalBorrowsUsd) === 0 ? 0 : (borrowRewardsPerDayUsd / Number(lunarMarket.totalBorrowsUsd)) * DAYS_PER_YEAR * 100 * -1; } market.rewards.push({ liquidStakingApr: 0, borrowApr, supplyApr, token, }); } market.totalSupplyApr = market.rewards.reduce((prev, curr) => prev + curr.supplyApr, market.baseSupplyApy); market.totalBorrowApr = market.rewards.reduce((prev, curr) => prev + curr.borrowApr, market.baseBorrowApy); markets.push(market); } return markets; } const fetchFromGenericCacheApi = async (uri) => { const response = await fetch("https://generic-api-cache.moonwell.workers.dev/", { method: "POST", body: `{"uri":"${uri}","cacheDuration":"300"}`, headers: { ...MOONWELL_FETCH_JSON_HEADERS, "Content-Type": "text/plain", }, }); return response.json(); }; export const fetchLiquidStakingRewards = async () => { const result = { cbETH: 0, rETH: 0, wstETH: 0, }; try { const cbETH = await fetchFromGenericCacheApi("https://api.exchange.coinbase.com/wrapped-assets/CBETH"); result.cbETH = Number(cbETH.apy) * 100; } catch (error) { result.cbETH = 0; } try { const rETH = await fetchFromGenericCacheApi("https://rocketpool.net/api/mainnet/payload"); result.rETH = Number(rETH.rethAPR); } catch (error) { result.rETH = 0; } try { const stETH = await fetchFromGenericCacheApi("https://eth-api.lido.fi/v1/protocol/steth/apr/last"); result.wstETH = stETH.data.apr; } catch (error) { result.wstETH = 0; } return result; }; //# sourceMappingURL=common.js.map