@moonwell-fi/moonwell-sdk
Version:
TypeScript Interface for Moonwell
1,029 lines (929 loc) • 33.4 kB
text/typescript
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];
}