UNPKG

@moonwell-fi/moonwell-sdk

Version:

TypeScript Interface for Moonwell

1,029 lines (929 loc) 33.4 kB
import type { Address } from "viem"; import { Amount } from "../../../common/amount.js"; import type { MultichainReturnType } from "../../../common/types.js"; import type { Environment } from "../../../environments/index.js"; import type { MorphoMarket, PublicAllocatorSharedLiquidityType, } from "../../../types/morphoMarket.js"; import type { MorphoReward } from "../../../types/morphoReward.js"; import { getWithRetry } from "../../axiosWithRetry.js"; import { getGraphQL } from "../utils/graphql.js"; import { type GetMorphoMarketsRewardsReturnType as LunarIndexerRewardsType, fetchMarketsFromIndexer, transformMarketsFromIndexer, } from "./lunarIndexerTransform.js"; export interface LunarVaultMarket { marketId: string; flowCapIn: string; flowCapOut: string; supplyCap: string; supplyCapEnabled: boolean; vaultSupplyShares: string; vaultSupplyAssets: string; } export interface LunarVault { address: string; name: string; fee: string; markets: LunarVaultMarket[]; } export interface LunarMarketLiveData { totalSupplyAssets?: string; totalBorrowAssets?: string; totalLiquidity?: string; loanToken?: { address: string; decimals: number }; collateralToken?: { address: string; decimals: number }; } export interface LunarSharedLiquidityResponse { vaults: LunarVault[]; markets: Record<string, LunarMarketLiveData>; } async function fetchSharedLiquidityFromLunar( lunarIndexerUrl: string, chainId: number, ): Promise<LunarSharedLiquidityResponse> { const response = await getWithRetry<LunarSharedLiquidityResponse>( `${lunarIndexerUrl}/api/v1/isolated/shared-liquidity/${chainId}`, ); return response.data; } export function computeSharedLiquidityFromLunar( data: LunarSharedLiquidityResponse, targetMarkets: string[], marketParamsMap: Map< string, { oracle: string; irm: string; lltv: string; loanToken: { address: string; decimals: number }; collateralToken: { address: string; decimals: number }; } >, chainId: number, ): GetMorphoMarketsPublicAllocatorSharedLiquidityReturnType[] { return targetMarkets.map((targetMarket) => { const targetId = targetMarket.toLowerCase(); const r: PublicAllocatorSharedLiquidityType[] = []; let reallocatableLiquidityAssets = 0; const marketRemainingLiquidity: Record<string, number> = {}; const targetLiveData = data.markets[targetId]; const targetParams = marketParamsMap.get(targetId); // Vault values (flowCapIn, flowCapOut, vaultSupplyAssets, supplyCap) are raw // (wei-like). Markets data (totalSupplyAssets, totalBorrowAssets, totalLiquidity) // is already in token units (divided by decimals). We convert vault raw values // to token units using the loan token decimals before comparing. const targetLoanDecimals = targetLiveData?.loanToken?.decimals ?? targetParams?.loanToken.decimals ?? 18; const targetScale = 10 ** targetLoanDecimals; for (const vault of data.vaults) { const thisMarketInVault = vault.markets.find( (m) => m.marketId.toLowerCase() === targetId, ); if (!thisMarketInVault) continue; const vaultSupplyInTarget = Number(thisMarketInVault.vaultSupplyAssets) / targetScale; if (vaultSupplyInTarget <= 0) continue; let maxIn = Number(thisMarketInVault.flowCapIn) / targetScale; if (thisMarketInVault.supplyCapEnabled) { const remainingCap = Number(thisMarketInVault.supplyCap) / targetScale - vaultSupplyInTarget; maxIn = Math.min(maxIn, remainingCap); } if (maxIn <= 0) continue; const flowCaps = vault.markets.map((m) => ({ maxIn: Number(m.flowCapIn), maxOut: Number(m.flowCapOut), market: { uniqueKey: m.marketId }, })); const vaultConfig = { address: vault.address, name: vault.name, publicAllocatorConfig: { fee: Number(vault.fee), flowCaps, }, }; const otherMarketsLiquidity: { marketId: string; amount: number; liquidity: number; allocationMarket: PublicAllocatorSharedLiquidityType["allocationMarket"]; }[] = []; for (const sourceMarket of vault.markets) { if (sourceMarket.marketId.toLowerCase() === targetId) continue; const sourceLiveData = data.markets[sourceMarket.marketId.toLowerCase()]; if (!sourceLiveData) continue; const sourceLoanDecimals = sourceLiveData.loanToken?.decimals ?? marketParamsMap.get(sourceMarket.marketId.toLowerCase())?.loanToken .decimals ?? 18; const sourceScale = 10 ** sourceLoanDecimals; const vaultSupplyInSource = Number(sourceMarket.vaultSupplyAssets) / sourceScale; const maxOut = Number(sourceMarket.flowCapOut) / sourceScale; // Use pre-computed totalLiquidity if available, else derive from supply/borrow const liquidity = sourceLiveData.totalLiquidity ? Number(sourceLiveData.totalLiquidity) : Number(sourceLiveData.totalSupplyAssets ?? 0) - Number(sourceLiveData.totalBorrowAssets ?? 0); if (vaultSupplyInSource > 0 && maxOut > 0 && liquidity > 0) { const sourceParams = marketParamsMap.get( sourceMarket.marketId.toLowerCase(), ); const allocationMarket: PublicAllocatorSharedLiquidityType["allocationMarket"] = sourceParams ? { uniqueKey: sourceMarket.marketId, loanAsset: { address: sourceParams.loanToken.address }, collateralAsset: { address: sourceParams.collateralToken.address, }, oracleAddress: sourceParams.oracle, irmAddress: sourceParams.irm, lltv: sourceParams.lltv, } : undefined; otherMarketsLiquidity.push({ marketId: sourceMarket.marketId.toLowerCase(), amount: Math.min(liquidity, vaultSupplyInSource, maxOut), liquidity, allocationMarket, }); } } for (const source of otherMarketsLiquidity .filter((s) => s.amount > 0) .sort((a, b) => b.amount - a.amount)) { const marketLiquidity = marketRemainingLiquidity[source.marketId] ?? source.liquidity; if (maxIn > 0 && marketLiquidity > 0) { const assets = Math.min(marketLiquidity, source.amount, maxIn); maxIn -= assets; marketRemainingLiquidity[source.marketId] = marketLiquidity - assets; reallocatableLiquidityAssets += assets; r.push({ assets, vault: vaultConfig, ...(source.allocationMarket ? { allocationMarket: source.allocationMarket } : {}), }); } } } // reallocatableLiquidityAssets is in token units; Amount expects raw units return { chainId, marketId: targetId, reallocatableLiquidityAssets: new Amount( BigInt(Math.round(reallocatableLiquidityAssets * targetScale)), targetLoanDecimals, ), publicAllocatorSharedLiquidity: r, }; }); } export async function getMorphoMarketsData(params: { environments: Environment[]; markets?: string[] | undefined; includeRewards?: boolean | undefined; }): Promise<MorphoMarket[]> { const { environments } = params; const hasLunarIndexer = environments.some((env) => env.lunarIndexerUrl); // Use Lunar Indexer implementation if available (with automatic fallback to on-chain on failure) if (hasLunarIndexer) { return getMorphoMarketsDataFromIndexer(params); } // Fall back to on-chain contract queries (legacy implementation) return getMorphoMarketsDataFromOnChain(params); } /** * Fetch markets from on-chain contracts (original implementation) */ async function getMorphoMarketsDataFromOnChain(params: { environments: Environment[]; markets?: string[] | undefined; includeRewards?: boolean | undefined; }): Promise<MorphoMarket[]> { const { environments } = params; const environmentsWithMarkets = environments.filter( (environment) => Object.keys(environment.config.morphoMarkets).length > 0 && environment.contracts.morphoViews, ); if (environmentsWithMarkets.length === 0) { return []; } const marketInfoSettlements = await Promise.allSettled( environmentsWithMarkets.map((environment) => { const marketsIds = Object.values(environment.config.morphoMarkets) .map((item) => item.id as Address) .filter((id) => params.markets ? params.markets .map((id) => id.toLowerCase()) .includes(id.toLowerCase()) : true, ); try { return environment.contracts.morphoViews!.read.getMorphoBlueMarketsInfo( [marketsIds], ); } catch (error) { return Promise.reject(error); } }), ); const fulfilledMarketsInfo = marketInfoSettlements.flatMap((s, i) => s.status === "fulfilled" ? [{ environment: environmentsWithMarkets[i]!, marketsInfo: s.value }] : [], ); const initialMarkets: MorphoMarket[] = []; fulfilledMarketsInfo.forEach(({ environment, marketsInfo }) => { marketsInfo.forEach((marketInfo) => { const marketKey = Object.keys(environment.config.morphoMarkets).find( (item) => environment.config.morphoMarkets[item].id.toLowerCase() === marketInfo.marketId.toLowerCase(), ); if (!marketKey) { return; } initialMarkets.push({ chainId: environment.chainId, marketId: marketInfo.marketId, marketKey, } as MorphoMarket); }); }); const rewardEnvironment = params.environments.find((env) => env.custom?.morpho?.apiUrl) ?? params.environments[0]; const rewardsData = await getMorphoMarketRewards( rewardEnvironment, initialMarkets, ); const rewardsDataByChainAndMarket = new Map< string, GetMorphoMarketsRewardsReturnType >(); rewardsData.forEach((reward) => { const key = `${reward.chainId}-${reward.marketId.toLowerCase()}`; rewardsDataByChainAndMarket.set(key, reward); }); const result = fulfilledMarketsInfo.reduce( (aggregator, { environment, marketsInfo }) => { const markets = marketsInfo.flatMap((marketInfo) => { const marketKey = Object.keys(environment.config.morphoMarkets).find( (item) => environment.config.morphoMarkets[item].id.toLowerCase() === marketInfo.marketId.toLowerCase(), ); if (!marketKey) { return []; } const marketConfig = Object.values( environment.config.morphoMarkets, ).find( (item) => item.id.toLowerCase() === marketInfo.marketId.toLowerCase(), )!; const loanToken = environment.config.tokens[marketConfig.loanToken]; const collateralToken = environment.config.tokens[marketConfig.collateralToken]; const oraclePrice = new Amount( BigInt(marketInfo.oraclePrice), 36 + loanToken.decimals - collateralToken.decimals, ).value; let collateralTokenPrice = new Amount(marketInfo.collateralPrice, 18) .value; let loanTokenPrice = new Amount(marketInfo.loanPrice, 18).value; if (collateralTokenPrice === 0 && loanTokenPrice > 0) { collateralTokenPrice = loanTokenPrice * oraclePrice; } if (loanTokenPrice === 0 && collateralTokenPrice > 0) { loanTokenPrice = collateralTokenPrice / oraclePrice; } // stkWELL is 1:1 with WELL, so use WELL price for stkWELL if (collateralToken.symbol === "stkWELL") { const wellMarketInfo = marketsInfo.find((mi) => { const wellMarketConfig = Object.values( environment.config.morphoMarkets, ).find( (item) => item.id.toLowerCase() === mi.marketId.toLowerCase() && (item.collateralToken === "WELL" || item.loanToken === "WELL"), ); return wellMarketConfig !== undefined; }); if (wellMarketInfo) { const wellMarketConfig = Object.values( environment.config.morphoMarkets, ).find( (item) => item.id.toLowerCase() === wellMarketInfo.marketId.toLowerCase(), ); let wellPrice = 0; if ( wellMarketConfig && wellMarketConfig.collateralToken === "WELL" ) { wellPrice = new Amount(wellMarketInfo.collateralPrice, 18).value; if (wellPrice === 0) { const wellLoanToken = environment.config.tokens[wellMarketConfig.loanToken]; const wellLoanPrice = new Amount(wellMarketInfo.loanPrice, 18) .value; const wellOraclePrice = new Amount( BigInt(wellMarketInfo.oraclePrice), 36 + wellLoanToken.decimals - 18, // WELL has 18 decimals ).value; wellPrice = wellLoanPrice * wellOraclePrice; } } else if ( wellMarketConfig && wellMarketConfig.loanToken === "WELL" ) { wellPrice = new Amount(wellMarketInfo.loanPrice, 18).value; if (wellPrice === 0) { const wellCollateralToken = environment.config.tokens[wellMarketConfig.collateralToken]; const wellCollateralPrice = new Amount( wellMarketInfo.collateralPrice, 18, ).value; const wellOraclePrice = new Amount( BigInt(wellMarketInfo.oraclePrice), 36 + 18 - wellCollateralToken.decimals, ).value; wellPrice = wellCollateralPrice / wellOraclePrice; } } if (wellPrice > 0) { collateralTokenPrice = wellPrice; } } } const performanceFee = new Amount(marketInfo.fee, 18).value; const loanToValue = new Amount(marketInfo.lltv, 18).value; const totalSupplyInLoanToken = new Amount( BigInt(marketInfo.totalSupplyAssets), loanToken.decimals, ); const totalSupply = new Amount( Number(totalSupplyInLoanToken.value / oraclePrice), collateralToken.decimals, ); const totalBorrows = new Amount( marketInfo.totalBorrowAssets, loanToken.decimals, ); // Supply APR is used only for vaults, zeroing it for now to avoid confusion // const supplyApy = new Amount(marketInfo.supplyApy, 18).value * 100; const borrowApy = new Amount(marketInfo.borrowApy, 18).value * 100; const availableLiquidity = new Amount( marketInfo.totalSupplyAssets - marketInfo.totalBorrowAssets, loanToken.decimals, ); const availableLiquidityUsd = availableLiquidity.value * loanTokenPrice; const rewardKey = `${environment.chainId}-${marketInfo.marketId.toLowerCase()}`; const marketRewardData = rewardsDataByChainAndMarket.get(rewardKey); const mapping: MorphoMarket = { chainId: environment.chainId, marketId: marketInfo.marketId, marketKey, deprecated: marketConfig.deprecated === true, loanToValue, performanceFee, loanToken, loanTokenPrice, collateralToken, collateralTokenPrice, // Note: collateralAssets and collateralAssetsUsd may be null when the Morpho API // returns null values for markets with no collateral or during API data sync delays. // Consumers should handle null to distinguish between "no data" vs "zero collateral". collateralAssets: marketRewardData?.collateralAssets ?? null, collateralAssetsUsd: marketRewardData?.collateralAssetsUsd ?? null, totalSupply, totalSupplyUsd: totalSupply.value * collateralTokenPrice, totalSupplyInLoanToken, totalBorrows, totalBorrowsUsd: totalBorrows.value * loanTokenPrice, baseBorrowApy: borrowApy, totalBorrowApr: borrowApy, baseSupplyApy: 0, //supplyApy, totalSupplyApr: 0, //supplyApy, rewardsSupplyApy: 0, rewardsBorrowApy: 0, availableLiquidity, availableLiquidityUsd, marketParams: { loanToken: marketInfo.loanToken, collateralToken: marketInfo.collateralToken, irm: marketInfo.irm, lltv: marketInfo.lltv, oracle: marketInfo.oracle, }, rewards: [], publicAllocatorSharedLiquidity: marketRewardData?.publicAllocatorSharedLiquidity ?? [], }; return [mapping]; }); return { ...aggregator, [environment.chainId]: markets, }; }, {} as MultichainReturnType<MorphoMarket[]>, ); if (params.includeRewards) { const markets = Object.values(result) .flat() .filter((market) => { const environment = params.environments.find( (environment) => environment.chainId === market.chainId, ); return environment?.custom.morpho?.minimalDeployment === false; }); const rewards = await getMorphoMarketRewards( params.environments.find((env) => env.custom?.morpho?.apiUrl) ?? params.environments[0], markets, ); markets.forEach((market) => { const marketReward = rewards.find( (reward) => reward.marketId === market.marketId && reward.chainId === market.chainId, ); if (marketReward) { market.rewards = marketReward.rewards; market.collateralAssets = marketReward.collateralAssets; market.publicAllocatorSharedLiquidity = marketReward.publicAllocatorSharedLiquidity; } market.rewardsSupplyApy = market.rewards.reduce<number>( (acc, curr) => acc + curr.supplyApr, 0, ); market.rewardsBorrowApy = market.rewards.reduce<number>( (acc, curr) => acc + curr.borrowApr, 0, ); market.totalSupplyApr = market.rewardsSupplyApy + market.baseSupplyApy; market.totalBorrowApr = market.rewardsBorrowApy + market.baseBorrowApy; }); } return environmentsWithMarkets.flatMap((environment) => { return result[environment.chainId] || []; }); } type GetMorphoMarketsPublicAllocatorSharedLiquidityReturnType = { chainId: number; marketId: string; reallocatableLiquidityAssets: Amount; publicAllocatorSharedLiquidity: PublicAllocatorSharedLiquidityType[]; }; type GetMorphoMarketsRewardsReturnType = { chainId: number; marketId: string; collateralAssets: Amount | null; collateralAssetsUsd: number | null; reallocatableLiquidityAssets: Amount; publicAllocatorSharedLiquidity: PublicAllocatorSharedLiquidityType[]; rewards: Required<MorphoReward>[]; }; async function getMorphoMarketRewards( environment: Environment, markets: { marketId: string; chainId: number }[], ): Promise<GetMorphoMarketsRewardsReturnType[]> { if (markets.length === 0) { return []; } const query = ` { markets(where: { uniqueKey_in: [${markets.map((market) => `"${market.marketId.toLowerCase()}"`).join(",")}], chainId_in: [${markets.map((market) => market.chainId).join(",")}] }) { items { morphoBlue { chain { id } } reallocatableLiquidityAssets publicAllocatorSharedLiquidity { assets vault { address name publicAllocatorConfig { fee flowCaps { maxIn maxOut market { uniqueKey } } } } allocationMarket { uniqueKey loanAsset { address } collateralAsset { address } oracleAddress irmAddress lltv } } collateralAsset { decimals } loanAsset { decimals priceUsd } state { collateralAssets collateralAssetsUsd rewards { asset { address symbol decimals name } supplyApr borrowApr amountPerBorrowedToken amountPerSuppliedToken } } uniqueKey } } } `; const result = await getGraphQL<{ markets: { items: { morphoBlue: { chain: { id: number; }; }; uniqueKey: string; reallocatableLiquidityAssets: string; publicAllocatorSharedLiquidity: { assets: string; vault: { address: string; name: string; publicAllocatorConfig: { fee: number; flowCaps: { market: { uniqueKey: string; }; maxIn: number; maxOut: number; }[]; }; }; allocationMarket: { uniqueKey: string; loanAsset: { address: string; }; collateralAsset?: { address: string; }; oracleAddress: string; irmAddress: string; lltv: string; }; }[]; collateralAsset: { decimals: number; }; loanAsset: { decimals: number; priceUsd: number; }; state: { collateralAssets: string; collateralAssetsUsd: number; rewards: { asset: { address: Address; symbol: string; decimals: number; name: string; }; supplyApr: number; amountPerSuppliedToken: string; borrowApr: number; amountPerBorrowedToken: string; }[]; }; }[]; }; }>(environment, query); if (result) { const markets = result.markets.items.map((item) => { const loanAssetDecimals = item.loanAsset.decimals; const mapping: GetMorphoMarketsRewardsReturnType = { chainId: item.morphoBlue.chain.id, marketId: item.uniqueKey, reallocatableLiquidityAssets: new Amount( BigInt(item.reallocatableLiquidityAssets), loanAssetDecimals, ), // Note: The Morpho GraphQL API may return null for collateralAssets and // collateralAssetsUsd for markets with no collateral deposited or during data sync. // We preserve null to let consumers distinguish between "no data" vs "zero collateral". collateralAssets: item.state.collateralAssets != null ? new Amount( BigInt(item.state.collateralAssets), item.collateralAsset.decimals, ) : null, collateralAssetsUsd: item.state.collateralAssetsUsd ?? null, publicAllocatorSharedLiquidity: item.publicAllocatorSharedLiquidity.map( (item) => ({ assets: Number(item.assets) / 10 ** loanAssetDecimals, vault: { address: item.vault.address, name: item.vault.name, publicAllocatorConfig: item.vault.publicAllocatorConfig, }, allocationMarket: item.allocationMarket, }), ), rewards: item.state?.rewards.map((reward) => { const tokenDecimals = 10 ** reward.asset.decimals; //Supply APR is used only for vaults, zeroing it for now to avoid confusion //const tokenAmountPer1000 = ((parseFloat(reward.amountPerSuppliedToken) / item.loanAsset.priceUsd) * 1000) || "0" //const amount = (Number(tokenAmountPer1000) / tokenDecimals) const borrowTokenAmountPer1000 = (Number.parseFloat(reward.amountPerBorrowedToken) / item.loanAsset.priceUsd) * 1000; const borrowAmount = borrowTokenAmountPer1000 / tokenDecimals; return { marketId: item.uniqueKey, asset: reward.asset, supplyApr: 0, //(reward.supplyApr || 0) * 100, supplyAmount: 0, //amount, borrowApr: (reward.borrowApr || 0) * 100 * -1, borrowAmount: borrowAmount, }; }), }; return mapping; }); return markets; } else { return []; } } /** * Fetch markets from Lunar Indexer for environments that have the lunar indexer URL configured * Falls back to on-chain if indexer fails */ async function getMorphoMarketsDataFromIndexer(params: { environments: Environment[]; markets?: string[] | undefined; includeRewards?: boolean | undefined; }): Promise<MorphoMarket[]> { const { environments } = params; // Filter environments that have lunar-indexer URL configured const environmentsWithIndexer = environments.filter( (environment) => environment.lunarIndexerUrl && Object.keys(environment.config.morphoMarkets).length > 0, ); if (environmentsWithIndexer.length === 0) { return []; } // Fetch markets from lunar-indexer for each environment const marketsSettlements = await Promise.allSettled( environmentsWithIndexer.map(async (environment) => { const lunarIndexerUrl = environment.lunarIndexerUrl!; try { const response = await fetchMarketsFromIndexer( lunarIndexerUrl, environment.chainId, params.includeRewards ? { includeRewards: true } : undefined, ); // Filter markets if specific ones were requested let markets = response.results; if (params.markets) { const requestedMarkets = params.markets.map((id) => id.toLowerCase()); markets = markets.filter((market) => requestedMarkets.includes(market.marketId.toLowerCase()), ); } return { environment, markets }; } catch (error) { console.warn( `Failed to fetch markets from Lunar Indexer for chain ${environment.chainId}, falling back to on-chain:`, error, ); environment.onError?.(error, { source: "morpho-markets", chainId: environment.chainId, }); return Promise.reject({ environment, error }); } }), ); const fulfilledMarkets = marketsSettlements.flatMap((s) => s.status === "fulfilled" ? [s.value] : [], ); // Collect environments that failed to fetch from indexer for fallback const failedEnvironments = marketsSettlements .filter((s): s is PromiseRejectedResult => s.status === "rejected") .map((s) => (s.reason as { environment?: Environment }).environment) .filter((env): env is Environment => env !== undefined); // Fall back to on-chain for environments where indexer failed let fallbackMarkets: MorphoMarket[] = []; if (failedEnvironments.length > 0) { console.warn( `Falling back to on-chain for ${failedEnvironments.length} environment(s)`, ); try { fallbackMarkets = await getMorphoMarketsDataFromOnChain({ environments: failedEnvironments, markets: params.markets, includeRewards: params.includeRewards, }); } catch (rpcError) { console.warn( `RPC fallback also failed for ${failedEnvironments.length} environment(s):`, rpcError, ); for (const env of failedEnvironments) { env.onError?.(rpcError, { source: "morpho-markets-rpc-fallback", chainId: env.chainId, }); } } } // Fetch shared liquidity from lunar-indexer. const sharedLiquiditySettlements = await Promise.allSettled( fulfilledMarkets.map(async ({ environment, markets }) => { const lunarIndexerUrl = environment.lunarIndexerUrl; if (!lunarIndexerUrl) return { environment, data: [] as GetMorphoMarketsPublicAllocatorSharedLiquidityReturnType[], }; const marketParamsMap = new Map( markets.map((m) => [ m.marketId.toLowerCase(), { oracle: m.oracle, irm: m.irm, lltv: m.lltv, loanToken: { address: m.loanToken.address, decimals: m.loanToken.decimals, }, collateralToken: { address: m.collateralToken.address, decimals: m.collateralToken.decimals, }, }, ]), ); try { const rawData = await fetchSharedLiquidityFromLunar( lunarIndexerUrl, environment.chainId, ); const marketIds = markets.map((m) => m.marketId); const data = computeSharedLiquidityFromLunar( rawData, marketIds, marketParamsMap, environment.chainId, ); return { environment, data }; } catch (error) { console.warn( `[getMorphoMarketsData] Lunar shared liquidity failed for chain ${environment.chainId}:`, error, ); environment.onError?.(error, { source: "morpho-shared-liquidity", chainId: environment.chainId, }); return { environment, data: [] as GetMorphoMarketsPublicAllocatorSharedLiquidityReturnType[], }; } }), ); const fulfilledSharedLiquidity = sharedLiquiditySettlements.flatMap((s) => s.status === "fulfilled" ? [s.value] : [], ); // Create shared liquidity map by chainId and marketId const sharedLiquidityMap = new Map< string, PublicAllocatorSharedLiquidityType[] >(); fulfilledSharedLiquidity.forEach(({ environment, data }) => { data.forEach((item) => { const key = `${environment.chainId}-${item.marketId.toLowerCase()}`; sharedLiquidityMap.set(key, item.publicAllocatorSharedLiquidity); }); }); // Seed rewardsDataMap with collateralAssets directly from the lunar-indexer // market data — no Morpho API call on the happy path. const rewardsDataMap = new Map<string, LunarIndexerRewardsType>(); fulfilledMarkets.forEach(({ environment, markets }) => { markets.forEach((market) => { const key = `${environment.chainId}-${market.marketId.toLowerCase()}`; const collateralDecimals = market.collateralToken.decimals; const collateralAssets = market.totalCollateralAssets ? new Amount( Number.parseFloat(market.totalCollateralAssets), collateralDecimals, ) : null; const collateralAssetsUsd = market.totalCollateralAssetsUsd ? Number.parseFloat(market.totalCollateralAssetsUsd) : null; rewardsDataMap.set(key, { chainId: environment.chainId, marketId: market.marketId, collateralAssets, collateralAssetsUsd, rewardsSupplyApy: 0, rewardsBorrowApy: 0, rewards: [], }); }); }); // Overlay rewards from the indexer when requested if (params.includeRewards) { fulfilledMarkets.forEach(({ environment, markets }) => { markets.forEach((market) => { if (!market.rewards?.length) return; const key = `${environment.chainId}-${market.marketId.toLowerCase()}`; const rewards: Required<MorphoReward>[] = market.rewards.map((r) => ({ marketId: market.marketId, asset: { address: r.token as Address, symbol: r.tokenSymbol, decimals: r.tokenDecimals, name: r.tokenName, }, supplyApr: Number.parseFloat(r.supplyApr), supplyAmount: 0, borrowApr: Number.parseFloat(r.borrowApr), borrowAmount: 0, })); const existing = rewardsDataMap.get(key); rewardsDataMap.set(key, { chainId: environment.chainId, marketId: market.marketId, collateralAssets: existing?.collateralAssets ?? null, collateralAssetsUsd: existing?.collateralAssetsUsd ?? null, rewardsSupplyApy: rewards.reduce((acc, r) => acc + r.supplyApr, 0), rewardsBorrowApy: rewards.reduce((acc, r) => acc + r.borrowApr, 0), rewards, }); }); }); } // Transform markets from indexer format to SDK format const transformedMarkets = fulfilledMarkets.flatMap( ({ environment, markets }) => { return transformMarketsFromIndexer( markets, environment, rewardsDataMap, sharedLiquidityMap, ); }, ); // Combine indexer results with fallback results return [...transformedMarkets, ...fallbackMarkets]; }