UNPKG

@moonwell-fi/moonwell-sdk

Version:

TypeScript Interface for Moonwell

944 lines 45.2 kB
import dayjs from "dayjs"; import utc from "dayjs/plugin/utc.js"; import { getContract, parseAbi, zeroAddress } from "viem"; dayjs.extend(utc); import { Amount } from "../../../common/amount.js"; import { publicEnvironments, } from "../../../environments/index.js"; import { findMarketByAddress, findTokenByAddress, } from "../../../environments/utils/index.js"; import { getGraphQL, getVaultV2Apy } from "../utils/graphql.js"; import { SECONDS_PER_YEAR, WAD, mulDivDown, toAssetsDown, wMulDown, } from "../utils/math.js"; import { fetchTokenMap, fetchVaultsFromIndexer, getV1VaultKey, transformVaultsFromIndexer, } from "./lunarIndexerTransform.js"; /** * Scales a V1 vault's market position by V2's ownership percentage. * * V2 vaults wrap V1 vaults - this calculates the V2 vault's effective * position in a V1 vault's market by scaling based on ownership: * V2 ownership = realAssets / underlyingVaultTotalAssets * * @param v1VaultSupplied - The V1 vault's supplied amount in the market * @param v2RealAssets - The V2 vault's real assets in the V1 vault * @param v1VaultTotalAssets - Total assets in the underlying V1 vault * @returns Scaled vault supplied amount, or 0n if V1 vault has no assets */ function scaleV1MarketPositionByV2Ownership(v1VaultSupplied, v2RealAssets, v1VaultTotalAssets) { if (v1VaultTotalAssets === 0n) { return 0n; } return mulDivDown(v1VaultSupplied, v2RealAssets, v1VaultTotalAssets); } /** * Fetch Morpho vaults data from Lunar Indexer * This is the new implementation that replaces on-chain contract queries * * @param params - Parameters including environments and options * @returns Array of MorphoVault objects with data from Lunar Indexer */ async function getMorphoVaultsDataFromIndexer(params) { const { environments } = params; // Filter environments that have vaults and Lunar Indexer URL configured const environmentsWithVaults = environments.filter((environment) => Object.keys(environment.vaults).length > 0 && environment.lunarIndexerUrl); // Fetch vaults from Lunar Indexer for each environment const environmentsVaultsSettlements = await Promise.allSettled(environmentsWithVaults.map(async (environment) => { const lunarIndexerUrl = environment.lunarIndexerUrl; try { // Fetch tokens and vaults in parallel const [tokenMap, vaultsResponse] = await Promise.all([ fetchTokenMap(lunarIndexerUrl, environment.chainId), fetchVaultsFromIndexer(lunarIndexerUrl, environment.chainId, params.includeRewards ? { includeRewards: true } : undefined), ]); // Transform vaults from indexer format to SDK format let vaults = transformVaultsFromIndexer(vaultsResponse.results, environment, tokenMap); // For V2 vaults, substitute TVL from the paired V1 vault. // V2 routes deposits through V1 via a liquidity adapter, so V1 holds // the actual assets — the Morpho API returns only V2's idle assets (~$13). // APY and rewards are kept from V2's own indexer data. const vaultByKey = new Map(vaults.map((v) => [v.vaultKey, v])); vaults = vaults.map((vault) => { const v1VaultKey = getV1VaultKey(environment, vault.vaultKey); if (!v1VaultKey) return vault; const v1Vault = vaultByKey.get(v1VaultKey); if (!v1Vault) { return vault; } return { ...vault, totalSupply: v1Vault.totalSupply, totalSupplyUsd: v1Vault.totalSupplyUsd, vaultSupply: v1Vault.vaultSupply, totalLiquidity: v1Vault.totalLiquidity, totalLiquidityUsd: v1Vault.totalLiquidityUsd, underlyingPrice: v1Vault.underlyingPrice, }; }); // Filter by specific vault addresses if requested if (params.vaults) { const requestedVaults = params.vaults.map((id) => id.toLowerCase()); vaults = vaults.filter((vault) => requestedVaults.includes(vault.vaultToken.address.toLowerCase())); } // Sort vaults by the order defined in environment config const vaultKeyOrder = Object.keys(environment.config.vaults); vaults.sort((a, b) => { const indexA = vaultKeyOrder.indexOf(a.vaultKey); const indexB = vaultKeyOrder.indexOf(b.vaultKey); return ((indexA === -1 ? Number.POSITIVE_INFINITY : indexA) - (indexB === -1 ? Number.POSITIVE_INFINITY : indexB)); }); return vaults; } catch (error) { console.warn(`Failed to fetch vaults from Lunar Indexer for chain ${environment.chainId}, falling back to on-chain:`, error); environment.onError?.(error, { source: "vaults", chainId: environment.chainId, }); throw { environment, error }; } })); // Extract successful results const allVaults = environmentsVaultsSettlements.flatMap((settlement) => settlement.status === "fulfilled" ? settlement.value : []); // Collect environments that failed to fetch from indexer for fallback const failedEnvironments = environmentsVaultsSettlements .filter((s) => s.status === "rejected") .map((s) => s.reason?.environment) .filter((env) => env !== undefined); // Fall back to on-chain for environments where indexer failed let fallbackVaults = []; if (failedEnvironments.length > 0) { console.warn(`Falling back to on-chain for ${failedEnvironments.length} environment(s)`); fallbackVaults = await getMorphoVaultsDataFromOnChain({ ...params, environments: failedEnvironments, }); } // Add staking data if configured for (const vault of allVaults) { const environment = environments.find((env) => env.chainId === vault.chainId); if (!environment) continue; const vaultConfig = environment.config.vaults[vault.vaultKey]; if (!vaultConfig?.multiReward) continue; try { const [distributorTotalSupply, vaultTotalSupply, vaultTotalAssets] = await Promise.all([ getTotalSupplyData(environment, vaultConfig.multiReward), getTotalSupplyData(environment, vault.vaultToken.address), getTotalAssetsData(environment, vault.vaultToken.address), ]); const stakedAssets = toAssetsDown(distributorTotalSupply, vaultTotalAssets, vaultTotalSupply); vault.totalStaked = new Amount(stakedAssets, vault.underlyingToken.decimals); vault.totalStakedUsd = vault.totalStaked.value * vault.underlyingPrice; } catch (error) { // Staking data is optional, continue if it fails console.warn(`Failed to fetch staking data for vault ${vault.vaultKey}:`, error); } } // Add staking rewards if includeRewards is true if (params.includeRewards) { for (const vault of allVaults) { const environment = environments.find((env) => env.chainId === vault.chainId); if (!environment) continue; const vaultConfig = environment.config.vaults[vault.vaultKey]; if (!vaultConfig?.multiReward) continue; try { // Fetch market prices from the views contract to calculate rewards 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 data = await Promise.all([ viewsContract?.read.getAllMarketsInfo(), homeViewsContract?.read.getNativeTokenPrice(), homeViewsContract?.read.getGovernanceTokenPrice(), ]); const [allMarkets, nativeTokenPriceRaw, governanceTokenPriceRaw] = data; const governanceTokenPrice = new Amount(governanceTokenPriceRaw || 0n, 18); const nativeTokenPrice = new Amount(nativeTokenPriceRaw || 0n, 18); let tokenPrices = allMarkets ?.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) || []; // Add governance token to token prices if (environment.custom?.governance?.token) { tokenPrices = [ ...tokenPrices, { token: environment.config.tokens[environment.custom.governance.token], tokenPrice: governanceTokenPrice, }, ]; } // Add native token to token prices tokenPrices = [ ...tokenPrices, { token: findTokenByAddress(environment, zeroAddress), tokenPrice: nativeTokenPrice, }, ]; const rewards = await getRewardsData(environment, vaultConfig.multiReward); const distributorTotalSupply = await getTotalSupplyData(environment, vaultConfig.multiReward); rewards .filter((reward) => reward?.periodFinish && dayjs.utc().isBefore(dayjs.unix(Number(reward.periodFinish)))) .forEach((reward) => { const token = Object.values(environment.config.tokens).find((token) => token.address === reward?.token); if (!token || !reward?.rewardRate) return; const market = tokenPrices.find((m) => m?.token.address === reward.token); const rewardPriceUsd = market?.tokenPrice.value ?? 0; const rewardsPerYear = new Amount(reward.rewardRate, market?.token.decimals ?? 18) .value * SECONDS_PER_YEAR * rewardPriceUsd; vault.stakingRewards.push({ apr: (rewardsPerYear / (new Amount(distributorTotalSupply, vault.vaultToken.decimals) .value * vault.underlyingPrice)) * 100, token: token, }); }); vault.stakingRewardsApr = vault.stakingRewards.reduce((acc, curr) => acc + curr.apr, 0); vault.totalStakingApr = vault.stakingRewardsApr + vault.baseApy; } catch (error) { console.warn(`Failed to fetch staking rewards for vault ${vault.vaultKey}:`, error); } } } return [...allVaults, ...fallbackVaults]; } export async function getMorphoVaultsData(params) { const { environments } = params; // Check if any environment has Lunar Indexer URL configured const hasLunarIndexer = environments.some((env) => env.lunarIndexerUrl); // Use Lunar Indexer implementation if available if (hasLunarIndexer) { return getMorphoVaultsDataFromIndexer(params); } // Fall back to on-chain contract queries (legacy implementation) return getMorphoVaultsDataFromOnChain(params); } async function getMorphoVaultsDataFromOnChain(params) { const { environments } = params; const environmentsWithVaults = environments.filter((environment) => Object.keys(environment.vaults).length > 0 && environment.contracts.morphoViews); // Query vaults for each environment, combining v1 and v2 results per environment const environmentsVaultsInfoSettlements = await Promise.allSettled(environmentsWithVaults.map(async (environment) => { // Split vaults by version const v1VaultsAddresses = Object.entries(environment.config.vaults) .filter(([_, config]) => !config.version || config.version === 1) .map(([key, _]) => environment.vaults[key]?.address) .filter((address) => address !== undefined) .filter((address) => params.vaults ? params.vaults .map((id) => id.toLowerCase()) .includes(address.toLowerCase()) : true); const v2VaultsAddresses = Object.entries(environment.config.vaults) .filter(([_, config]) => config.version === 2) .map(([key, _]) => environment.vaults[key]?.address) .filter((address) => address !== undefined) .filter((address) => params.vaults ? params.vaults .map((id) => id.toLowerCase()) .includes(address.toLowerCase()) : true); // Run v1 and v2 queries in parallel within this environment const queryPromises = []; // Query v1 vaults with morphoViews if (v1VaultsAddresses.length > 0 && environment.contracts.morphoViews) { queryPromises.push((async () => { try { const vaultInfo = await environment.contracts.morphoViews?.read.getVaultsInfo([ v1VaultsAddresses, ]); return vaultInfo; } catch (error) { return undefined; } })()); } // Query v2 vaults with morphoViewsV2 // Note: V2 vaults (e.g., meUSDC) wrap V1 vaults (e.g., meUSDCv1). // The V2 vault allocates its assets to underlying V1 vaults via adapters. if (v2VaultsAddresses.length > 0 && environment.contracts.morphoViewsV2) { queryPromises.push((async () => { try { const vaultInfoV2 = await environment.contracts.morphoViewsV2?.read.getVaultsInfo([ v2VaultsAddresses, ]); // Extract unique underlying vault addresses from V2 adapters const underlyingVaultAddresses = vaultInfoV2 ?.flatMap((v2Vault) => v2Vault.adapters?.map((adapter) => adapter.underlyingVault)) .filter((addr) => addr && addr !== zeroAddress) || []; const uniqueUnderlyingAddresses = [ ...new Set(underlyingVaultAddresses), ]; // Query underlying V1 vaults to get their market positions const underlyingVaultsData = new Map(); if (uniqueUnderlyingAddresses.length > 0 && environment.contracts.morphoViews) { const underlyingInfo = await environment.contracts.morphoViews.read.getVaultsInfo([ uniqueUnderlyingAddresses, ]); // Map by vault address for quick lookup underlyingInfo?.forEach((vaultData, index) => { underlyingVaultsData.set(uniqueUnderlyingAddresses[index].toLowerCase(), vaultData); }); } // Transform v2 structure to v1 structure // V2 vaults wrap V1 vaults - we fetch the V1 data and scale allocations const transformedVaults = vaultInfoV2?.map((v2Vault) => { // Get the first adapter (assuming single adapter for now) const adapter = v2Vault.adapters?.[0]; if (!adapter) { return { vault: v2Vault.vault, totalSupply: v2Vault.totalSupply, totalAssets: v2Vault.totalAssets, underlyingPrice: v2Vault.underlyingPrice, fee: 0n, timelock: 0n, markets: [], }; } // Get underlying vault data for accurate market allocations const underlyingVaultData = underlyingVaultsData.get(adapter.underlyingVault.toLowerCase()); let markets = []; if (underlyingVaultData?.markets) { // Map V1 vault's markets with scaled allocations markets = underlyingVaultData.markets.map((v1Market) => { // Scale the V1 vault's position by V2's ownership // V2 ownership = realAssets / underlyingVaultTotalAssets const scaledVaultSupplied = scaleV1MarketPositionByV2Ownership(v1Market.vaultSupplied, adapter.realAssets, adapter.underlyingVaultTotalAssets); return { marketId: v1Market.marketId, marketCollateral: v1Market.marketCollateral, marketCollateralName: v1Market.marketCollateralName, marketCollateralSymbol: v1Market.marketCollateralSymbol, marketLltv: v1Market.marketLltv, marketApy: v1Market.marketApy, marketLiquidity: v1Market.marketLiquidity, vaultSupplied: scaledVaultSupplied, }; }); } else { // Fallback: if we can't get V1 data, use underlyingMarkets with 0 allocations markets = (adapter.underlyingMarkets || []).map((underlyingMarket) => ({ marketId: underlyingMarket.marketId, marketCollateral: underlyingMarket.collateralToken, marketCollateralName: underlyingMarket.collateralName, marketCollateralSymbol: underlyingMarket.collateralSymbol, marketLltv: underlyingMarket.marketLltv, marketApy: underlyingMarket.marketSupplyApy, marketLiquidity: underlyingMarket.marketLiquidity, vaultSupplied: 0n, })) || []; } return { vault: v2Vault.vault, totalSupply: v2Vault.totalSupply, totalAssets: v2Vault.totalAssets, underlyingPrice: v2Vault.underlyingPrice, fee: adapter.underlyingVaultFee, timelock: adapter.underlyingVaultTimelock, markets, }; }); return transformedVaults; } catch (error) { return undefined; } })()); } const queryResults = await Promise.all(queryPromises); const results = queryResults .filter((r) => r !== undefined) .flat(); // Sort results to match the order in environment.config.vaults const vaultKeys = Object.keys(environment.config.vaults); const sortedResults = results.sort((a, b) => { const aIndex = vaultKeys.findIndex((key) => environment.vaults[key]?.address.toLowerCase() === a.vault.toLowerCase()); const bIndex = vaultKeys.findIndex((key) => environment.vaults[key]?.address.toLowerCase() === b.vault.toLowerCase()); return aIndex - bIndex; }); return sortedResults; })); const environmentsVaultsInfo = environmentsVaultsInfoSettlements.map((s) => { if (s.status === "fulfilled") { return s.value; } return []; }); const result = await environmentsWithVaults.reduce(async (aggregatorPromise, environment, environmentIndex) => { const aggregator = await aggregatorPromise; const environmentVaultsInfo = environmentsVaultsInfo[environmentIndex]; if (!environmentVaultsInfo) { return aggregator; } // Batch fetch V2 APY data for all V2 vaults upfront const v2VaultsToFetch = environmentVaultsInfo.filter((vaultInfo) => { const vaultKey = Object.keys(environment.config.tokens).find((key) => environment.config.tokens[key].address.toLowerCase() === vaultInfo.vault.toLowerCase()); const vaultConfig = environment.config.vaults[vaultKey]; return vaultConfig?.version === 2; }); const v2ApyDataMap = new Map(); if (v2VaultsToFetch.length > 0) { const v2ApySettlements = await Promise.allSettled(v2VaultsToFetch.map((vaultInfo) => getVaultV2Apy(environment, vaultInfo.vault, environment.chainId))); v2ApySettlements.forEach((settlement, index) => { if (settlement.status === "fulfilled" && settlement.value) { v2ApyDataMap.set(v2VaultsToFetch[index].vault.toLowerCase(), settlement.value); } }); } const settled = await Promise.allSettled(environmentVaultsInfo.map(async (vaultInfo) => { const vaultKey = Object.keys(environment.config.tokens).find((key) => { return (environment.config.tokens[key].address.toLowerCase() === vaultInfo.vault.toLowerCase()); }); const vaultToken = environment.config.tokens[vaultKey]; const vaultConfig = environment.config.vaults[vaultKey]; const underlyingToken = environment.config.tokens[vaultConfig.underlyingToken]; const underlyingPrice = new Amount(vaultInfo.underlyingPrice, 18); const vaultSupply = new Amount(vaultInfo.totalSupply, vaultToken.decimals); const totalSupply = new Amount(vaultInfo.totalAssets, underlyingToken.decimals); const totalSupplyUsd = totalSupply.value * underlyingPrice.value; const performanceFee = new Amount(vaultInfo.fee, 18).value; const timelock = Number(vaultInfo.timelock) / (60 * 60); let ratio = 0n; let baseApy = 0; let v2ApyData; // For v2 vaults, use batched APY data if (vaultConfig.version === 2) { v2ApyData = v2ApyDataMap.get(vaultInfo.vault.toLowerCase()); if (v2ApyData) { // Use avgNetApy (net APY including rewards) baseApy = v2ApyData.avgNetApy * 100; } } const markets = vaultInfo.markets.map((vaultMarket) => { ratio += wMulDown(vaultMarket.marketApy, vaultMarket.vaultSupplied); const totalSupplied = new Amount(vaultMarket.vaultSupplied, underlyingToken.decimals); const totalSuppliedUsd = totalSupplied.value * underlyingPrice.value; const allocation = totalSupplied.value / totalSupply.value; const marketLoanToValue = new Amount(vaultMarket.marketLltv, 18).value * 100; const marketApy = new Amount(vaultMarket.marketApy, 18).value * 100; let marketLiquidity = new Amount(vaultMarket.marketLiquidity, underlyingToken.decimals); let marketLiquidityUsd = marketLiquidity.value * underlyingPrice.value; if (vaultMarket.marketCollateral === zeroAddress) { marketLiquidity = totalSupplied; marketLiquidityUsd = totalSuppliedUsd; } const mapping = { marketId: vaultMarket.marketId, allocation, marketApy, marketCollateral: { address: vaultMarket.marketCollateral, decimals: 0, name: vaultMarket.marketCollateralName, symbol: vaultMarket.marketCollateralSymbol, }, marketLiquidity, marketLiquidityUsd, marketLoanToValue, totalSupplied, totalSuppliedUsd, rewards: [], }; return mapping; }); // For V2 vaults, normalize allocations to reflect deployed asset distribution // V2 vaults may have idle capital not deployed to adapters, which causes // raw allocations to sum to less than 100% of total vault assets if (vaultConfig.version === 2) { const totalAllocation = markets.reduce((sum, m) => sum + m.allocation, 0); if (totalAllocation > 0) { for (const market of markets) { market.allocation = market.allocation / totalAllocation; } } } // Only calculate baseApy from markets for v1 vaults // v2 vaults already have baseApy from Morpho API if (vaultConfig.version !== 2) { const avgSupplyApy = mulDivDown(ratio, WAD - vaultInfo.fee, vaultInfo.totalAssets === 0n ? 1n : vaultInfo.totalAssets); baseApy = new Amount(avgSupplyApy, 18).value * 100; } let totalLiquidity; let totalLiquidityUsd; // V2 vaults: Use liquidity from Morpho API // V1 vaults: Sum up market liquidity if (vaultConfig.version === 2 && v2ApyData) { // Use liquidity data from Morpho API totalLiquidity = new Amount(BigInt(v2ApyData.liquidity || "0"), underlyingToken.decimals); totalLiquidityUsd = v2ApyData.liquidityUsd || 0; } else { // V1 vaults: sum market liquidity totalLiquidity = new Amount(markets.reduce((acc, curr) => acc + curr.marketLiquidity.exponential, 0n), underlyingToken.decimals); totalLiquidityUsd = markets.reduce((acc, curr) => acc + curr.marketLiquidityUsd, 0); // Cap liquidity at totalSupply for V1 vaults if (totalLiquidity.value > totalSupply.value) { totalLiquidity = totalSupply; totalLiquidityUsd = totalSupplyUsd; } } let totalStaked = new Amount(0n, underlyingToken.decimals); let totalStakedUsd = 0; if (vaultConfig.multiReward) { const [distributorTotalSupply, vaultTotalSupply, vaultTotalAssets,] = await Promise.all([ getTotalSupplyData(environment, vaultConfig.multiReward), getTotalSupplyData(environment, vaultToken.address), getTotalAssetsData(environment, vaultToken.address), ]); const stakedAssets = toAssetsDown(distributorTotalSupply, vaultTotalAssets, vaultTotalSupply); totalStaked = new Amount(stakedAssets, underlyingToken.decimals); totalStakedUsd = totalStaked.value * underlyingPrice.value; } const mapping = { chainId: environment.chainId, vaultKey: vaultKey, version: vaultConfig.version || 1, deprecated: vaultConfig.deprecated === true, vaultToken, underlyingToken, underlyingPrice: underlyingPrice.value, baseApy, totalApy: baseApy, rewardsApy: 0, stakingRewardsApr: 0, totalStakingApr: baseApy, curators: [], performanceFee, timelock, totalLiquidity, totalLiquidityUsd, totalSupplyUsd, totalSupply, vaultSupply, totalStaked, totalStakedUsd, markets: markets, rewards: [], stakingRewards: [], }; return mapping; })); let vaults = settled.flatMap((s) => s.status === "fulfilled" ? s.value : []); // For V2 vaults, substitute TVL from the paired V1 vault (same as indexer path). // V2 routes deposits through V1 via a liquidity adapter, so V1 holds the actual // assets — on-chain data returns only V2's idle assets. const onChainVaultByKey = new Map(vaults.map((v) => [v.vaultKey, v])); vaults = vaults.map((vault) => { const v1VaultKey = getV1VaultKey(environment, vault.vaultKey); if (!v1VaultKey) return vault; const v1Vault = onChainVaultByKey.get(v1VaultKey); if (!v1Vault) { return vault; } return { ...vault, totalSupply: v1Vault.totalSupply, totalSupplyUsd: v1Vault.totalSupplyUsd, vaultSupply: v1Vault.vaultSupply, totalLiquidity: v1Vault.totalLiquidity, totalLiquidityUsd: v1Vault.totalLiquidityUsd, underlyingPrice: v1Vault.underlyingPrice, }; }); return { ...(await aggregator), [environment.chainId]: vaults, }; }, Promise.resolve({})); // Add rewards to vaults if (params.includeRewards === true) { // add stake rewards const flatList = Object.values(result).flat(); for (const vault of flatList) { const environment = params.environments.find((environment) => environment.chainId === vault.chainId); if (!environment) { continue; } // Fetch market prices from the views contract to calculate rewards 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 data = await Promise.all([ viewsContract?.read.getAllMarketsInfo(), homeViewsContract?.read.getNativeTokenPrice(), homeViewsContract?.read.getGovernanceTokenPrice(), ]); const [allMarkets, nativeTokenPriceRaw, governanceTokenPriceRaw] = data; const governanceTokenPrice = new Amount(governanceTokenPriceRaw || 0n, 18); const nativeTokenPrice = new Amount(nativeTokenPriceRaw || 0n, 18); let tokenPrices = allMarkets ?.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) || []; // Add governance token to token prices if (environment.custom?.governance?.token) { tokenPrices = [ ...tokenPrices, { token: environment.config.tokens[environment.custom.governance.token], tokenPrice: governanceTokenPrice, }, ]; } // Add native token to token prices tokenPrices = [ ...tokenPrices, { token: findTokenByAddress(environment, zeroAddress), tokenPrice: nativeTokenPrice, }, ]; const vaultConfig = environment?.config.vaults[vault.vaultKey]; if (!environment || !vaultConfig || !vaultConfig.multiReward) { continue; } const rewards = await getRewardsData(environment, vaultConfig.multiReward); const distributorTotalSupply = await getTotalSupplyData(environment, vaultConfig.multiReward); rewards .filter((reward) => reward?.periodFinish && dayjs.utc().isBefore(dayjs.unix(Number(reward.periodFinish)))) .forEach((reward) => { const token = Object.values(environment.config.tokens).find((token) => token.address === reward?.token); if (!token || !reward?.rewardRate) return; const market = tokenPrices.find((m) => m?.token.address === reward.token); const rewardPriceUsd = market?.tokenPrice.value ?? 0; const rewardsPerYear = new Amount(reward.rewardRate, market?.token.decimals ?? 18).value * SECONDS_PER_YEAR * rewardPriceUsd; vault.stakingRewards.push({ apr: (rewardsPerYear / (new Amount(distributorTotalSupply, vault.vaultToken.decimals) .value * vault.underlyingPrice)) * 100, token: token, }); }); vault.stakingRewardsApr = vault.stakingRewards.reduce((acc, curr) => acc + curr.apr, 0); vault.totalStakingApr = vault.stakingRewardsApr + vault.baseApy; } const vaults = Object.values(result) .flat() .filter((vault) => { const environment = params.environments.find((environment) => environment.chainId === vault.chainId); return environment?.custom.morpho?.minimalDeployment === false; }); const rewards = await getMorphoVaultsRewards(params.environments[0], vaults, params.currentChainRewardsOnly); vaults.forEach((vault) => { const vaultRewards = rewards.find((reward) => reward.vaultToken.address === vault.vaultToken.address && reward.chainId === vault.chainId); vault.rewards = vaultRewards?.rewards .filter((reward) => reward.marketId === undefined) .map((reward) => ({ asset: reward.asset, supplyApr: reward.supplyApr, supplyAmount: reward.supplyAmount, borrowApr: reward.borrowApr, borrowAmount: reward.borrowAmount, })) || []; vault.markets.forEach((market) => { const marketRewards = vaultRewards?.rewards .filter((reward) => reward.marketId === market.marketId) .map((reward) => ({ asset: reward.asset, supplyApr: reward.supplyApr, supplyAmount: reward.supplyAmount, borrowApr: reward.borrowApr, borrowAmount: reward.borrowAmount, })) || []; market.rewards = marketRewards; market.rewards.forEach((reward) => { const supplyApr = reward.supplyApr * market.allocation; const supplyAmount = reward.supplyAmount * market.allocation; const vaultReward = vault.rewards.find((r) => r.asset.address === reward.asset.address); if (vaultReward) { vaultReward.supplyApr += supplyApr; vaultReward.supplyAmount += supplyAmount; } else { vault.rewards.push({ asset: reward.asset, supplyApr, supplyAmount, borrowApr: 0, borrowAmount: 0, }); } }); }); vault.rewardsApy = vault.rewards.reduce((acc, curr) => acc + curr.supplyApr, 0); vault.totalApy = vault.rewardsApy + vault.baseApy; }); } return environments.flatMap((environment) => { return result[environment.chainId] || []; }); } const getRewardsData = async (environment, multiRewardsAddress) => { if (!environment.custom.multiRewarder) { return []; } const multiRewardAbi = parseAbi([ "function rewardData(address token) view returns (address, uint256, uint256, uint256, uint256, uint256)", ]); const multiRewardContract = getContract({ address: multiRewardsAddress, abi: multiRewardAbi, client: environment.publicClient, }); const rewards = await Promise.all(environment.custom.multiRewarder.map(async (multiRewarder) => { const tokenAddress = environment.tokens[multiRewarder.rewardToken].address; if (!tokenAddress) { return; } try { const rewardData = await multiRewardContract.read.rewardData([ tokenAddress, ]); return { rewardRate: BigInt(rewardData[3]), token: tokenAddress, periodFinish: BigInt(rewardData[2]), }; } catch { return { rewardRate: 0n, token: tokenAddress }; } })); return rewards.filter(Boolean); }; const getTotalSupplyData = async (environment, multiRewardsAddress) => { if (!environment.custom.multiRewarder) { return 0n; } const multiRewardAbi = parseAbi([ "function totalSupply() view returns (uint256)", ]); const multiRewardContract = getContract({ address: multiRewardsAddress, abi: multiRewardAbi, client: environment.publicClient, }); try { const totalSupply = await multiRewardContract.read.totalSupply(); return BigInt(totalSupply); } catch { return 0n; } }; const getTotalAssetsData = async (environment, multiRewardsAddress) => { if (!environment.custom.multiRewarder) { return 0n; } const multiRewardAbi = parseAbi([ "function totalAssets() view returns (uint256)", ]); const multiRewardContract = getContract({ address: multiRewardsAddress, abi: multiRewardAbi, client: environment.publicClient, }); try { const totalAssets = await multiRewardContract.read.totalAssets(); return BigInt(totalAssets); } catch { return 0n; } }; export async function getMorphoVaultsRewards(environment, vaults, currentChainRewardsOnly) { const query = ` { vaults( where: { address_in: [${vaults.map((vault) => `"${vault.vaultToken.address}"`).join(",")}], chainId_in: [${vaults.map((vault) => vault.chainId).join(",")}] } ) { items { chain { id } id address asset { priceUsd } state { rewards { asset { address symbol decimals name chain { id } } supplyApr amountPerSuppliedToken } } } } marketPositions( where: { userAddress_in: [${vaults.map((vault) => `"${vault.vaultToken.address}"`).join(",")}], chainId_in: [${vaults.map((vault) => vault.chainId).join(",")}] } ) { items { user { address } market { morphoBlue { chain { id } } uniqueKey loanAsset { priceUsd } state { rewards { asset { address symbol decimals name chain { id } } supplyApr amountPerSuppliedToken } } } } } } `; const result = await getGraphQL(environment, query); if (result) { try { const marketsRewards = result.marketPositions.items.flatMap((item) => { const rewards = (item.market.state?.rewards || []).map((reward) => { const tokenAmountPer1000 = (Number.parseFloat(reward.amountPerSuppliedToken) / item.market.loanAsset.priceUsd) * 1000; const tokenDecimals = 10 ** reward.asset.decimals; const amount = Number(tokenAmountPer1000) / tokenDecimals; return { chainId: reward.asset.chain.id, vaultId: item.user.address, marketId: item.market.uniqueKey, asset: reward.asset, supplyApr: (reward.supplyApr || 0) * 100, supplyAmount: amount, borrowApr: 0, borrowAmount: 0, }; }); return rewards; }); const vaultsRewards = result.vaults.items.flatMap((item) => { return (item.state?.rewards || []).map((reward) => { const tokenAmountPer1000 = (Number.parseFloat(reward.amountPerSuppliedToken) / item.asset.priceUsd) * 1000; const tokenDecimals = 10 ** reward.asset.decimals; const amount = Number(tokenAmountPer1000) / tokenDecimals; return { chainId: reward.asset.chain.id, vaultId: item.address, marketId: undefined, asset: reward.asset, supplyApr: (reward.supplyApr || 0) * 100, supplyAmount: amount, borrowApr: 0, borrowAmount: 0, }; }); }); const rewards = [...marketsRewards, ...vaultsRewards]; return vaults.map((vault) => { return { chainId: vault.chainId, vaultToken: vault.vaultToken, rewards: rewards .filter((reward) => reward.vaultId.toLowerCase() === vault.vaultToken.address.toLowerCase() && (reward.chainId === vault.chainId || !currentChainRewardsOnly)) .map((reward) => { return { marketId: reward.marketId, asset: reward.asset, supplyApr: reward.supplyApr, supplyAmount: reward.supplyAmount, borrowApr: reward.borrowApr, borrowAmount: reward.borrowAmount, }; }), }; }); } catch (ex) { return vaults.map((vault) => { return { chainId: vault.chainId, vaultToken: vault.vaultToken, rewards: [], }; }); } } else { return vaults.map((vault) => { return { chainId: vault.chainId, vaultToken: vault.vaultToken, rewards: [], }; }); } } //# sourceMappingURL=common.js.map