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
13 changes: 9 additions & 4 deletions app/market/[chainId]/[marketid]/content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,9 @@ function MarketContent() {

// Non-async wrapper for components that expect void returns
const handleRefreshAllSync = useCallback(() => {
void handleRefreshAll();
void handleRefreshAll().catch((error) => {
console.error('Failed to refresh data:', error);
});
}, [handleRefreshAll]);

// Unified handler for timeframe changes
Expand Down Expand Up @@ -200,9 +202,12 @@ function MarketContent() {
<Button
size="md"
className="mb-4"
onClick={() =>
window.open(getMarketURL(market.uniqueKey, market.morphoBlue.chain.id), '_blank')
}
onClick={() => {
void window.open(
getMarketURL(market.uniqueKey, market.morphoBlue.chain.id),
'_blank',
);
}}
Comment thread
antoncoding marked this conversation as resolved.
>
View on Morpho
<Image src={MORPHO_LOGO} alt="Morpho Logo" width={20} height={20} className="ml-2" />
Expand Down
24 changes: 4 additions & 20 deletions src/config/dataSources.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,14 @@
import { SupportedNetworks } from '@/utils/networks';

/**
* Determines the primary data source for market details based on the network.
* Check if a network supports Morpho API as a data source
*/
export const getMarketDataSource = (network: SupportedNetworks): 'morpho' | 'subgraph' => {
export const supportsMorphoApi = (network: SupportedNetworks): boolean => {
switch (network) {
case SupportedNetworks.Mainnet:
return 'morpho';
case SupportedNetworks.Base:
return 'morpho';
return true;
default:
return 'subgraph'; // Default to Subgraph
}
};

/**
* Determines the data source for historical market data.
* Assumes only Morpho API provides this, unless explicitly excluded.
*/
export const getHistoricalDataSource = (network: SupportedNetworks): 'morpho' | 'subgraph' => {
switch (network) {
case SupportedNetworks.Mainnet:
return 'morpho';
case SupportedNetworks.Base:
return 'morpho';
default:
return 'subgraph';
return false;
}
};
55 changes: 41 additions & 14 deletions src/contexts/MarketsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
useState,
useMemo,
} from 'react';
import { getMarketDataSource } from '@/config/dataSources';
import { supportsMorphoApi } from '@/config/dataSources';
import { fetchMorphoMarkets } from '@/data-sources/morpho-api/market';
import { fetchSubgraphMarkets } from '@/data-sources/subgraph/market';
import useLiquidations from '@/hooks/useLiquidations';
Expand Down Expand Up @@ -78,7 +78,7 @@ export function MarketsProvider({ children }: MarketsProviderProps) {
SupportedNetworks.Mainnet,
SupportedNetworks.Base,
SupportedNetworks.Polygon,
SupportedNetworks.Unichain
SupportedNetworks.Unichain,
];
let combinedMarkets: Market[] = [];
let fetchErrors: unknown[] = [];
Expand All @@ -88,17 +88,34 @@ export function MarketsProvider({ children }: MarketsProviderProps) {
await Promise.all(
networksToFetch.map(async (network) => {
try {
const dataSource = getMarketDataSource(network);
let networkMarkets: Market[] = [];

console.log(`Fetching markets for ${network} via ${dataSource}`);
// Try Morpho API first if supported
if (supportsMorphoApi(network)) {
try {
console.log(`Attempting to fetch markets via Morpho API for ${network}`);
networkMarkets = await fetchMorphoMarkets(network);
} catch (morphoError) {
console.error(
`Failed to fetch markets via Morpho API for ${network}:`,
morphoError,
);
// Continue to Subgraph fallback
}
}

if (dataSource === 'morpho') {
networkMarkets = await fetchMorphoMarkets(network);
} else if (dataSource === 'subgraph') {
networkMarkets = await fetchSubgraphMarkets(network);
} else {
console.warn(`No valid data source found for network ${network}`);
// If Morpho API failed or not supported, try Subgraph
if (networkMarkets.length === 0) {
try {
console.log(`Attempting to fetch markets via Subgraph for ${network}`);
networkMarkets = await fetchSubgraphMarkets(network);
} catch (subgraphError) {
console.error(
`Failed to fetch markets via Subgraph for ${network}:`,
subgraphError,
);
throw subgraphError; // Throw to be caught by outer catch
}
}

combinedMarkets.push(...networkMarkets);
Expand All @@ -114,7 +131,7 @@ export function MarketsProvider({ children }: MarketsProviderProps) {
const filtered = combinedMarkets
.filter((market) => market.collateralAsset != undefined)
.filter((market) => isSupportedChain(market.morphoBlue.chain.id)) // Keep this filter
.filter((market) => !blacklistedMarkets.includes(market.uniqueKey)); // Filter out blacklisted markets
.filter((market) => !blacklistedMarkets.includes(market.uniqueKey));

const processedMarkets = filtered.map((market) => {
const warningsWithDetail = getMarketWarningsWithDetail(market, true); // Recalculate warnings if needed, though fetchers might do this
Expand Down Expand Up @@ -166,14 +183,24 @@ export function MarketsProvider({ children }: MarketsProviderProps) {

useEffect(() => {
if (!liquidationsLoading && whitelistedMarkets.length === 0) {
fetchMarkets().catch(console.error);
void fetchMarkets().catch(console.error);
}

// Set up refresh interval
const refreshInterval = setInterval(
() => {
void fetchMarkets(true).catch(console.error);
},
5 * 60 * 1000,
); // Refresh every 5 minutes

return () => clearInterval(refreshInterval);
}, [liquidationsLoading, fetchMarkets, whitelistedMarkets.length]);

const refetch = useCallback(
async (onSuccess?: () => void) => {
try {
refetchLiquidations();
await refetchLiquidations();
await fetchMarkets(true);
onSuccess?.();
} catch (err) {
Expand All @@ -189,7 +216,7 @@ export function MarketsProvider({ children }: MarketsProviderProps) {
setAllMarkets([]);
setError(null);
try {
refetchLiquidations();
await refetchLiquidations();
await fetchMarkets();
} catch (_error) {
console.error('Failed to refresh markets:', _error);
Expand Down
7 changes: 4 additions & 3 deletions src/data-sources/morpho-api/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ export const fetchMorphoTransactions = async (
chainId_in: filters.chainIds ?? [SupportedNetworks.Base, SupportedNetworks.Mainnet],
};

if (filters.marketUniqueKeys && filters.marketUniqueKeys.length > 0) {
whereClause.marketUniqueKey_in = filters.marketUniqueKeys;
}
// disable cuz it's too long
// if (filters.marketUniqueKeys && filters.marketUniqueKeys.length > 0) {
// whereClause.marketUniqueKey_in = filters.marketUniqueKeys;
// }
Comment on lines +24 to +27
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Ignoring marketUniqueKeys drops an expected filter

filters.marketUniqueKeys is still accepted by callers but silently discarded.
Down-stream code that relies on this filter will now process extra markets, which can blow up payload size and break UI assumptions.

Options:

-// disable cuz it's too long
-// if (filters.marketUniqueKeys && filters.marketUniqueKeys.length > 0) {
-//   whereClause.marketUniqueKey_in = filters.marketUniqueKeys;
-// }
+// Morpho API rejects very long `IN` arrays (~5 KB limit). Chunk and merge.
+if (filters.marketUniqueKeys?.length) {
+  const CHUNK = 50;
+  const chunks = [];
+  for (let i = 0; i < filters.marketUniqueKeys.length; i += CHUNK) {
+    chunks.push({ ...whereClause, marketUniqueKey_in: filters.marketUniqueKeys.slice(i, i + CHUNK) });
+  }
+  return mergeResponses(
+    await Promise.all(chunks.map((w) => query(w)))
+  );
+}

If the size limit is the real reason, chunking or a server-side alias is safer than dropping the filter.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// disable cuz it's too long
// if (filters.marketUniqueKeys && filters.marketUniqueKeys.length > 0) {
// whereClause.marketUniqueKey_in = filters.marketUniqueKeys;
// }
// Morpho API rejects very long `IN` arrays (~5 KB limit). Chunk and merge.
if (filters.marketUniqueKeys?.length) {
const CHUNK = 50;
const chunks = [];
for (let i = 0; i < filters.marketUniqueKeys.length; i += CHUNK) {
chunks.push({
...whereClause,
marketUniqueKey_in: filters.marketUniqueKeys.slice(i, i + CHUNK),
});
}
return mergeResponses(
await Promise.all(chunks.map((w) => query(w)))
);
}
🤖 Prompt for AI Agents
In src/data-sources/morpho-api/transactions.ts around lines 24 to 27, the code
currently ignores the filters.marketUniqueKeys filter by commenting it out,
which causes unexpected behavior downstream. To fix this, re-enable the filter
logic for marketUniqueKeys and implement a solution to handle large arrays
safely, such as chunking the filter values into smaller batches or using a
server-side alias to limit the size, ensuring the filter is applied without
causing payload or UI issues.

if (filters.timestampGte !== undefined && filters.timestampGte !== null) {
whereClause.timestamp_gte = filters.timestampGte;
}
Expand Down
66 changes: 41 additions & 25 deletions src/hooks/useLiquidations.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react';
import { getMarketDataSource } from '@/config/dataSources';
import { supportsMorphoApi } from '@/config/dataSources';
import { fetchMorphoApiLiquidatedMarketKeys } from '@/data-sources/morpho-api/liquidations';
import { fetchSubgraphLiquidatedMarketKeys } from '@/data-sources/subgraph/liquidations';
import { SupportedNetworks } from '@/utils/networks';
Expand Down Expand Up @@ -31,57 +31,73 @@ const useLiquidations = () => {
await Promise.all(
networksToCheck.map(async (network) => {
try {
const dataSource = getMarketDataSource(network);
let networkLiquidatedKeys: Set<string>;

console.log(`Fetching liquidated markets for ${network} via ${dataSource}`);

if (dataSource === 'morpho') {
networkLiquidatedKeys = await fetchMorphoApiLiquidatedMarketKeys(network);
} else if (dataSource === 'subgraph') {
networkLiquidatedKeys = await fetchSubgraphLiquidatedMarketKeys(network);
// Try Morpho API first if supported
if (supportsMorphoApi(network)) {
try {
console.log(`Attempting to fetch liquidated markets via Morpho API for ${network}`);
networkLiquidatedKeys = await fetchMorphoApiLiquidatedMarketKeys(network);
} catch (morphoError) {
console.error(`Failed to fetch liquidated markets via Morpho API:`, morphoError);
// Continue to Subgraph fallback
networkLiquidatedKeys = new Set();
}
} else {
console.warn(`No valid data source found for network ${network} for liquidations.`);
networkLiquidatedKeys = new Set<string>(); // Assume none if no source
networkLiquidatedKeys = new Set();
}

// If Morpho API failed or not supported, try Subgraph
if (networkLiquidatedKeys.size === 0) {
try {
console.log(`Attempting to fetch liquidated markets via Subgraph for ${network}`);
networkLiquidatedKeys = await fetchSubgraphLiquidatedMarketKeys(network);
} catch (subgraphError) {
console.error(`Failed to fetch liquidated markets via Subgraph:`, subgraphError);
throw subgraphError; // Throw to be caught by outer catch
}
}
Comment on lines +36 to 59
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Uninitialised variable risk

let networkLiquidatedKeys: Set<string>; is declared but may stay undefined if both data sources throw.
Using const networkLiquidatedKeys = new Set<string>(); at declaration avoids the extra else { new Set() } and the possible undefined dereference later.

🤖 Prompt for AI Agents
In src/hooks/useLiquidations.ts around lines 36 to 59, the variable
networkLiquidatedKeys is declared without initialization and may remain
undefined if both Morpho API and Subgraph fetches fail, risking undefined
dereference. To fix this, initialize networkLiquidatedKeys as an empty
Set<string> at declaration using const, then update it as needed. This removes
the need for else blocks assigning new Sets and ensures networkLiquidatedKeys is
always defined.


// Add keys from this network to the combined set
// Add the keys to the combined set
networkLiquidatedKeys.forEach((key) => combinedLiquidatedKeys.add(key));
} catch (networkError) {
console.error(
`Failed to fetch liquidated market keys for network ${network}:`,
`Failed to fetch liquidated markets for network ${network}:`,
networkError,
);
fetchErrors.push(networkError); // Collect errors
fetchErrors.push(networkError);
}
}),
);

setLiquidatedMarketKeys(combinedLiquidatedKeys);

// Set overall error if any network fetch failed
if (fetchErrors.length > 0) {
setError(fetchErrors[0]); // Or aggregate errors if needed
setError(fetchErrors[0]);
}
} catch (err) {
// Catch potential errors from Promise.all itself
console.error('Overall error fetching liquidations:', err);
console.error('Error fetching liquidated markets:', err);
setError(err);
} finally {
setLoading(false);
setIsRefetching(false);
if (isRefetch) {
setIsRefetching(false);
} else {
setLoading(false);
}
}
}, []); // Dependencies: None needed directly, fetchers are self-contained
}, []);

useEffect(() => {
fetchLiquidations().catch(console.error);
}, [fetchLiquidations]);

const refetch = useCallback(() => {
fetchLiquidations(true).catch(console.error);
}, [fetchLiquidations]);

return { loading, isRefetching, liquidatedMarketKeys, error, refetch };
return {
loading,
isRefetching,
liquidatedMarketKeys,
error,
refetch: async () => fetchLiquidations(true),
};
};

export default useLiquidations;
46 changes: 24 additions & 22 deletions src/hooks/useMarketBorrows.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query';
import { getMarketDataSource } from '@/config/dataSources';
import { supportsMorphoApi } from '@/config/dataSources';
import { fetchMorphoMarketBorrows } from '@/data-sources/morpho-api/market-borrows';
import { fetchSubgraphMarketBorrows } from '@/data-sources/subgraph/market-borrows';
import { SupportedNetworks } from '@/utils/networks';
Expand All @@ -20,37 +20,40 @@ export const useMarketBorrows = (
) => {
const queryKey = ['marketBorrows', marketId, loanAssetId, network];

// Determine the data source
const dataSource = network ? getMarketDataSource(network) : null;

const { data, isLoading, error, refetch } = useQuery<MarketActivityTransaction[] | null>({
queryKey: queryKey,
queryFn: async (): Promise<MarketActivityTransaction[] | null> => {
// Guard clauses
if (!marketId || !loanAssetId || !network || !dataSource) {
if (!marketId || !loanAssetId || !network) {
return null;
}

console.log(
`Fetching market borrows for market ${marketId} (loan asset ${loanAssetId}) on ${network} via ${dataSource}`,
);
let borrows: MarketActivityTransaction[] | null = null;

try {
if (dataSource === 'morpho') {
// Morpho API might only need marketId for borrows
return await fetchMorphoMarketBorrows(marketId);
} else if (dataSource === 'subgraph') {
return await fetchSubgraphMarketBorrows(marketId, loanAssetId, network);
// Try Morpho API first if supported
if (supportsMorphoApi(network)) {
try {
console.log(`Attempting to fetch borrows via Morpho API for ${marketId}`);
borrows = await fetchMorphoMarketBorrows(marketId);
} catch (morphoError) {
console.error(`Failed to fetch borrows via Morpho API:`, morphoError);
// Continue to Subgraph fallback
}
}

// If Morpho API failed or not supported, try Subgraph
if (!borrows) {
try {
console.log(`Attempting to fetch borrows via Subgraph for ${marketId}`);
borrows = await fetchSubgraphMarketBorrows(marketId, loanAssetId, network);
} catch (subgraphError) {
console.error(`Failed to fetch borrows via Subgraph:`, subgraphError);
borrows = null;
}
} catch (fetchError) {
console.error(`Failed to fetch market borrows via ${dataSource}:`, fetchError);
return null;
}

console.warn('Unknown market data source determined for borrows');
return null;
return borrows;
},
enabled: !!marketId && !!loanAssetId && !!network && !!dataSource,
enabled: !!marketId && !!loanAssetId && !!network,
staleTime: 1000 * 60 * 2, // 2 minutes
placeholderData: (previousData) => previousData ?? null,
retry: 1,
Expand All @@ -62,7 +65,6 @@ export const useMarketBorrows = (
isLoading: isLoading,
error: error,
refetch: refetch,
dataSource: dataSource,
};
};

Expand Down
Loading