Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ NEXT_PUBLIC_ARBITRUM_RPC=
NEXT_PUBLIC_ETHERLINK_RPC=
NEXT_PUBLIC_HYPEREVM_RPC=
NEXT_PUBLIC_MONAD_RPC=
# Set to "false" locally to skip RPC historical-rate fallback when Morpho API rolling rates fail.
NEXT_PUBLIC_ENABLE_MARKET_RATE_RPC_FALLBACK=true
# Set to "true" only when RPC historical-rate fallback is intentionally needed.
NEXT_PUBLIC_ENABLE_MARKET_RATE_RPC_FALLBACK=false


# ==================== End of RPC Settings ====================
Expand Down
4 changes: 1 addition & 3 deletions src/components/DataPrefetcher.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
'use client';

import { usePathname } from 'next/navigation';
import { useMarketsQuery } from '@/hooks/queries/useMarketsQuery';
import { useMorphoWhitelistStatusQuery } from '@/hooks/queries/useMorphoWhitelistStatusQuery';
import { useTokensQuery } from '@/hooks/queries/useTokensQuery';
import { useMerklCampaignsQuery } from '@/hooks/queries/useMerklCampaignsQuery';

function DataPrefetcherContent() {
useMorphoWhitelistStatusQuery();
useMarketsQuery();
useTokensQuery();
useMerklCampaignsQuery();

return null;
}

/**
* Triggeres data prefetching for markets, tokens, and Merkl campaigns.
* Triggeres data prefetching for tokens, whitelist metadata, and Merkl campaigns.
* These hooks use React Query under the hood, which will cache the data for future use.
* @returns
*/
Expand Down
13 changes: 12 additions & 1 deletion src/constants/markets.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
export const DEFAULT_MIN_SUPPLY_USD = 1000;
export const DEFAULT_MIN_LIQUIDITY_USD = 10_000;
export const LOCKED_MARKET_APY_THRESHOLD = 15; // APY where 1.0 = 100%, so 15 = 1500%

// APY values are stored as decimals: 1 = 100%, 15 = 1500%.
// The default lock guard only hides clearly frozen markets with absurd APY.
// ETH-pegged loan markets get a tighter guard because several locked WETH
// markets sit around 200%, which is far below the generic 1500% cutoff but
// still not a useful live market signal.
export const LOCKED_MARKET_APY_THRESHOLDS = {
default: 15,
ethPegLoanAsset: 1,
} as const;

export const LOCKED_MARKET_APY_THRESHOLD = LOCKED_MARKET_APY_THRESHOLDS.default;
52 changes: 44 additions & 8 deletions src/data-sources/monarch-api/markets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { isMarketRegistryEntryAllowed } from '@/utils/markets';
import { getMorphoAddress } from '@/utils/morpho';
import { isSupportedChain, type SupportedNetworks } from '@/utils/networks';
import { infoToKey } from '@/utils/tokens';
import type { ERC20Token } from '@/utils/tokens';
import { resolveTokenInfos, type ResolvedTokenInfo, type TokenAddressInput } from '@/utils/tokenMetadata';
import type { Market, MarketWarning } from '@/utils/types';
import { UNRECOGNIZED_COLLATERAL, UNRECOGNIZED_LOAN } from '@/utils/warnings';
Expand Down Expand Up @@ -35,6 +36,15 @@ type MonarchMarketsPageResponse = {
};
};

type MapMonarchMarketRowsOptions = {
resolveUnknownTokens?: boolean;
trustedTokens?: ERC20Token[];
};

type MapMonarchMarketOptions = {
warnOnMissingTokenInfo?: boolean;
};

const MONARCH_MARKETS_PAGE_SIZE = 1_000;
const MONARCH_MARKETS_TIMEOUT_MS = 15_000;
const MONARCH_MARKETS_ZERO_ADDRESS = zeroAddress.toLowerCase();
Expand Down Expand Up @@ -127,7 +137,11 @@ const getMarketTokenInputs = (markets: MonarchMarketRow[]): TokenAddressInput[]
return tokens;
};

const mapMonarchMarketToMarket = (market: MonarchMarketRow, tokenInfos: Map<string, ResolvedTokenInfo>): Market | null => {
const mapMonarchMarketToMarket = (
market: MonarchMarketRow,
tokenInfos: Map<string, ResolvedTokenInfo>,
options: MapMonarchMarketOptions = {},
): Market | null => {
if (!isSupportedChain(market.chainId)) {
return null;
}
Expand All @@ -150,7 +164,9 @@ const mapMonarchMarketToMarket = (market: MonarchMarketRow, tokenInfos: Map<stri
const collateralAsset = tokenInfos.get(infoToKey(collateralAssetAddress, chainId));

if (!loanAsset || !collateralAsset) {
console.warn(`Skipping Monarch market ${market.marketId} on chain ${chainId}: token decimals could not be resolved.`);
if (options.warnOnMissingTokenInfo ?? true) {
console.warn(`Skipping Monarch market ${market.marketId} on chain ${chainId}: token decimals could not be resolved.`);
}
return null;
}

Expand Down Expand Up @@ -203,18 +219,36 @@ const fetchMonarchMarketsPage = async (query: string, variables: Record<string,
}
};

const mapMonarchMarketRows = async (rows: MonarchMarketRow[], customRpcUrls: CustomRpcUrls = {}): Promise<Market[]> => {
const mapMonarchMarketRows = async (
rows: MonarchMarketRow[],
customRpcUrls: CustomRpcUrls = {},
options: MapMonarchMarketRowsOptions = {},
): Promise<Market[]> => {
if (rows.length === 0) {
return [];
}

const tokenInfos = await resolveTokenInfos(getMarketTokenInputs(rows), customRpcUrls);
const shouldResolveUnknownTokens = options.resolveUnknownTokens ?? true;
const tokenInfos = await resolveTokenInfos(getMarketTokenInputs(rows), customRpcUrls, {
resolveUnknownTokens: shouldResolveUnknownTokens,
trustedTokens: options.trustedTokens,
});

return rows.map((market) => mapMonarchMarketToMarket(market, tokenInfos)).filter((market): market is Market => market !== null);
return rows
.map((market) =>
mapMonarchMarketToMarket(market, tokenInfos, {
warnOnMissingTokenInfo: shouldResolveUnknownTokens,
}),
)
.filter((market): market is Market => market !== null);
};

// If `network` is omitted, this fetches the merged multi-chain market registry in one query path.
export const fetchMonarchMarkets = async (network?: SupportedNetworks, customRpcUrls: CustomRpcUrls = {}): Promise<Market[]> => {
export const fetchMonarchMarkets = async (
network?: SupportedNetworks,
customRpcUrls: CustomRpcUrls = {},
options: MapMonarchMarketRowsOptions = {},
): Promise<Market[]> => {
const query = buildEnvioMarketsPageQuery({
useChainIdFilter: network !== undefined,
});
Expand Down Expand Up @@ -242,7 +276,7 @@ export const fetchMonarchMarkets = async (network?: SupportedNetworks, customRpc
offset += rows.length;
}

return mapMonarchMarketRows(allRows, customRpcUrls);
return mapMonarchMarketRows(allRows, customRpcUrls, options);
};

export const fetchMonarchMarket = async (
Expand All @@ -256,6 +290,8 @@ export const fetchMonarchMarket = async (
zeroAddress: MONARCH_MARKETS_ZERO_ADDRESS,
});

const [market] = await mapMonarchMarketRows(rows, customRpcUrls);
const [market] = await mapMonarchMarketRows(rows, customRpcUrls, {
resolveUnknownTokens: true,
});
return market ?? null;
};
11 changes: 8 additions & 3 deletions src/features/markets/components/table/market-table-body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { MarketRiskIndicators } from '@/features/markets/components/market-risk-
import OracleVendorBadge from '@/features/markets/components/oracle-vendor-badge';
import { TrustedByCell } from '@/features/autovault/components/trusted-vault-badges';
import { getVaultKey, type TrustedVault } from '@/constants/vaults/known_vaults';
import { useProcessedMarkets } from '@/hooks/useProcessedMarkets';
import { useRateLabel } from '@/hooks/useRateLabel';
import { useStyledToast } from '@/hooks/useStyledToast';
import { useMarketPreferences } from '@/stores/useMarketPreferences';
Expand All @@ -26,14 +25,20 @@ type MarketTableBodyProps = {
expandedRowId: string | null;
setExpandedRowId: (id: string | null) => void;
trustedVaultMap: Map<string, TrustedVault>;
rateEnrichmentPendingChainIds: Set<number>;
};

type HistoricalRateField = Exclude<keyof MarketRateEnrichment, 'apyAtTarget' | 'rateAtTarget'>;

export function MarketTableBody({ currentEntries, expandedRowId, setExpandedRowId, trustedVaultMap }: MarketTableBodyProps) {
export function MarketTableBody({
currentEntries,
expandedRowId,
setExpandedRowId,
trustedVaultMap,
rateEnrichmentPendingChainIds,
}: MarketTableBodyProps) {
const { columnVisibility, starredMarkets, starMarket, unstarMarket } = useMarketPreferences();
const { success: toastSuccess } = useStyledToast();
const { rateEnrichmentPendingChainIds } = useProcessedMarkets();

const { label: supplyRateLabel } = useRateLabel({ prefix: 'Supply' });
const { label: borrowRateLabel } = useRateLabel({ prefix: 'Borrow' });
Expand Down
39 changes: 27 additions & 12 deletions src/features/markets/components/table/markets-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,6 @@ type MarketsTableProps = {
};

function MarketsTable({ currentPage, setCurrentPage, className, tableClassName, onRefresh, isMobile }: MarketsTableProps) {
// Get loading states directly from query (no prop drilling!)
const { isLoading: loading, isRefetching, data: rawMarkets, dataUpdatedAt } = useMarketsQuery();

// Get trusted vaults directly from store (no prop drilling!)
const { vaults: trustedVaults } = useTrustedVaults();

const { markets, isLoading: filteredMarketsLoading, isWhitelistUnavailable } = useFilteredMarkets();
const isEmpty = !rawMarkets;
const [expandedRowId, setExpandedRowId] = useState<string | null>(null);
const { label: supplyRateLabel } = useRateLabel({ prefix: 'Supply' });
const { label: borrowRateLabel } = useRateLabel({ prefix: 'Borrow' });

const {
columnVisibility,
sortColumn,
Expand All @@ -55,6 +43,32 @@ function MarketsTable({ currentPage, setCurrentPage, className, tableClassName,
starredMarkets,
} = useMarketPreferences();

// Get loading states directly from query (no prop drilling!)
const {
isLoading: loading,
isRefetching,
data: rawMarkets,
dataUpdatedAt,
} = useMarketsQuery({
includeUnknownTokens,
});

// Get trusted vaults directly from store (no prop drilling!)
const { vaults: trustedVaults } = useTrustedVaults();

const {
markets,
isLoading: filteredMarketsLoading,
isWhitelistUnavailable,
rateEnrichmentPendingChainIds,
} = useFilteredMarkets({
currentPage,
});
const isEmpty = !rawMarkets;
const [expandedRowId, setExpandedRowId] = useState<string | null>(null);
const { label: supplyRateLabel } = useRateLabel({ prefix: 'Supply' });
const { label: borrowRateLabel } = useRateLabel({ prefix: 'Borrow' });

const { starredOnly } = useMarketsFilters();

// Handle column header clicks for sorting
Expand Down Expand Up @@ -339,6 +353,7 @@ function MarketsTable({ currentPage, setCurrentPage, className, tableClassName,
expandedRowId={expandedRowId}
setExpandedRowId={setExpandedRowId}
trustedVaultMap={trustedVaultMap}
rateEnrichmentPendingChainIds={rateEnrichmentPendingChainIds}
/>
</Table>
)}
Expand Down
18 changes: 15 additions & 3 deletions src/features/markets/markets-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,23 @@ export default function Markets() {
const toast = useStyledToast();
const appliedUrlSignatureRef = useRef<string | null>(null);
const [currentSearchParams, setCurrentSearchParams] = useState('');
const { tableViewMode, includeUnknownTokens } = useMarketPreferences();

// Data fetching with React Query
const { data: rawMarkets, isLoading: loading, refetch } = useMarketsQuery();
const { markets, isLoading: filteredMarketsLoading, isWhitelistUnavailable } = useFilteredMarkets();
const {
data: rawMarkets,
isLoading: loading,
refetch,
} = useMarketsQuery({
includeUnknownTokens,
});
const {
markets,
isLoading: filteredMarketsLoading,
isWhitelistUnavailable,
} = useFilteredMarkets({
enableRateEnrichment: false,
});

const filters = useMarketsFilters();
const persistedFilters = useMarketFilterPreferences();
Expand All @@ -39,7 +52,6 @@ export default function Markets() {
// Store hooks
const { currentPage, setCurrentPage, resetPage } = usePagination();
const { allTokens } = useTokensQuery();
const { tableViewMode, includeUnknownTokens } = useMarketPreferences();

useLayoutEffect(() => {
setCurrentSearchParams(window.location.search.startsWith('?') ? window.location.search.slice(1) : window.location.search);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { PulseLoader } from 'react-spinners';
import { RateFormatted } from '@/components/shared/rate-formatted';
import { TooltipContent } from '@/components/shared/tooltip-content';
import { Tooltip } from '@/components/ui/tooltip';
import { useMarketRateEnrichmentQuery } from '@/hooks/queries/useMarketRateEnrichmentQuery';
import { useProcessedMarkets } from '@/hooks/useProcessedMarkets';
import {
computeLiquidationOraclePrice,
Expand All @@ -19,6 +20,7 @@ import {
isInfiniteLtv,
} from '@/modals/borrow/components/helpers';
import { getMarketIdentityKey } from '@/utils/market-identity';
import { getMarketRateEnrichmentKey } from '@/utils/market-rate-enrichment';
import type { BorrowPositionRow } from '@/utils/positions';

type BorrowedMorphoBlueRowDetailProps = {
Expand Down Expand Up @@ -142,7 +144,9 @@ function renderHistoricalRateValue(value: number | null | undefined, isRateEnric
}

export function BorrowedMorphoBlueRowDetail({ row }: BorrowedMorphoBlueRowDetailProps) {
const { allMarkets, rateEnrichmentPendingChainIds } = useProcessedMarkets();
const { allMarkets } = useProcessedMarkets({
enableRateEnrichment: false,
});
const marketIdentityKey = useMemo(
() => getMarketIdentityKey(row.market.morphoBlue.chain.id, row.market.uniqueKey),
[row.market.morphoBlue.chain.id, row.market.uniqueKey],
Expand All @@ -151,15 +155,28 @@ export function BorrowedMorphoBlueRowDetail({ row }: BorrowedMorphoBlueRowDetail
() => allMarkets.find((market) => getMarketIdentityKey(market.morphoBlue.chain.id, market.uniqueKey) === marketIdentityKey),
[allMarkets, marketIdentityKey],
);
const rateMarket = liveMarket ?? row.market;
const { data: marketRateEnrichments, pendingChainIds: rateEnrichmentPendingChainIds } = useMarketRateEnrichmentQuery([rateMarket]);
const marketWithRates = useMemo(() => {
const enrichment = marketRateEnrichments.get(getMarketRateEnrichmentKey(rateMarket.uniqueKey, rateMarket.morphoBlue.chain.id));
if (!enrichment) {
return rateMarket;
}

return {
...rateMarket,
state: {
...rateMarket.state,
...enrichment,
},
};
}, [marketRateEnrichments, rateMarket]);
const resolvedRow = useMemo(
() =>
liveMarket
? {
...row,
market: liveMarket,
}
: row,
[liveMarket, row],
() => ({
...row,
market: marketWithRates,
}),
[marketWithRates, row],
);
const isRateEnrichmentPending = rateEnrichmentPendingChainIds.has(resolvedRow.market.morphoBlue.chain.id);

Expand Down
12 changes: 10 additions & 2 deletions src/hooks/queries/useMarketsQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { supportsMorphoApi } from '@/config/dataSources';
import { fetchMonarchMarkets } from '@/data-sources/monarch-api';
import { fetchMorphoMarkets } from '@/data-sources/morpho-api/market';
import { fetchSubgraphMarkets } from '@/data-sources/subgraph/market';
import { useTokensQuery } from '@/hooks/queries/useTokensQuery';
import { getMarketIdentityKey } from '@/utils/market-identity';
import { ALL_SUPPORTED_NETWORKS, isSupportedChain, type SupportedNetworks } from '@/utils/networks';
import type { Market } from '@/utils/types';
Expand All @@ -16,6 +17,7 @@ const toError = (error: unknown): Error => {
type UseMarketsQueryOptions = {
refetchInterval?: number | false;
refetchOnWindowFocus?: boolean;
includeUnknownTokens?: boolean;
};

/**
Expand All @@ -40,10 +42,12 @@ type UseMarketsQueryOptions = {
*/
export const useMarketsQuery = (options?: UseMarketsQueryOptions) => {
const { customRpcUrls } = useCustomRpcContext();
const { allTokens, isLoading: tokensLoading } = useTokensQuery();
const rpcIdentity = Object.entries(customRpcUrls).sort(([left], [right]) => Number(left) - Number(right));
const includeUnknownTokens = options?.includeUnknownTokens ?? false;

return useQuery({
queryKey: ['markets', rpcIdentity],
queryKey: ['markets', rpcIdentity, includeUnknownTokens, allTokens.length],
queryFn: async () => {
const fetchErrors: Error[] = [];
const marketsByChain = new Map<SupportedNetworks, Market[]>();
Expand Down Expand Up @@ -76,7 +80,10 @@ export const useMarketsQuery = (options?: UseMarketsQueryOptions) => {
};

try {
const monarchMarkets = await fetchMonarchMarkets(undefined, customRpcUrls);
const monarchMarkets = await fetchMonarchMarkets(undefined, customRpcUrls, {
resolveUnknownTokens: includeUnknownTokens,
trustedTokens: allTokens,
});
const monarchMarketsByChain = partitionMarketsByChain(monarchMarkets);

for (const [network, markets] of monarchMarketsByChain.entries()) {
Expand Down Expand Up @@ -153,5 +160,6 @@ export const useMarketsQuery = (options?: UseMarketsQueryOptions) => {
staleTime: 5 * 60 * 1000, // Data is fresh for 5 minutes
refetchInterval: options?.refetchInterval ?? 5 * 60 * 1000, // Auto-refetch every 5 minutes in background by default
refetchOnWindowFocus: options?.refetchOnWindowFocus ?? true, // Refetch when user returns to tab by default
enabled: !tokensLoading,
});
};
Loading