fix(api): handle Morpho API returning NOT_FOUND error with valid data#370
fix(api): handle Morpho API returning NOT_FOUND error with valid data#370antoncoding merged 3 commits intomasterfrom
Conversation
The Morpho API sometimes returns a NOT_FOUND error alongside valid data. Previously we would discard all data when seeing this error, causing supplier/borrower charts to show 'No suppliers found' even when data exists. Now we check if valid data exists before returning null on NOT_FOUND errors.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📝 WalkthroughWalkthroughAdjusts Morpho GraphQL fetcher NOT_FOUND handling to return valid data when present; adds a new hook (useSupplierPositionHistory) that reconstructs supplier position time-series from Morpho API/Subgraph; and adds a SupplierHoldingsChart React component to render interactive supplier holding charts and stats. Changes
Sequence Diagram(s)sequenceDiagram
participant Chart as SupplierHoldingsChart
participant Hook as useSupplierPositionHistory
participant API as Morpho API/Subgraph
participant Processor as Data Processor
Chart->>Hook: request(marketId, timeframe)
Hook->>API: fetch top suppliers
Hook->>API: paginate transactions (Morpho API, fallback to Subgraph)
API-->>Hook: transactions + counts
Hook->>Processor: filter by timeframe & compute per-supplier shares
Processor-->>Hook: per-supplier timeline (forward-filled)
Hook-->>Chart: time-series data + supplier roster
Chart->>Chart: render lines, legend, tooltip, stats
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Some positions returned by Morpho API have state: null, causing crashes when mapping. Now we filter these out before mapping to our types.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/features/market-detail/components/charts/supplier-holdings-chart.tsx`:
- Around line 1-46: The initialization of visibleLines currently uses useMemo
(should be a side-effect) and only sets state when visibleLines is empty,
preventing new suppliers from being added on timeframe changes; replace the
useMemo block with a useEffect that runs when suppliers or selectedTimeframe
change, and inside compute a new visibility map by merging existing visibleLines
with entries for any supplier in suppliers (use supplier.address.toLowerCase()
as the key) defaulting to true for new keys, then call setVisibleLines with the
merged object; reference the visibleLines state, suppliers array,
setVisibleLines function, and selectedTimeframe in the effect dependencies.
In `@src/hooks/useSupplierPositionHistory.ts`:
- Around line 140-142: The early return when relevantTxs.length === 0 in
useSupplierPositionHistory prevents the constant-position backfill from running
and clears the suppliers list; remove this early return (or instead build and
return suppliers via the existing backfill logic) so that when relevantTxs is
empty the hook still calls the backfill/constant-position logic (e.g.,
backfillConstantPositions or the code that populates suppliers and data) and
returns data: [] but with suppliers populated; keep other control flow and error
handling intact.
| import { useState, useMemo, useCallback } from 'react'; | ||
| import type { Address } from 'viem'; | ||
| import { Card } from '@/components/ui/card'; | ||
| import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select'; | ||
| import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; | ||
| import { Spinner } from '@/components/ui/spinner'; | ||
| import { TokenIcon } from '@/components/shared/token-icon'; | ||
| import { useChartColors } from '@/constants/chartColors'; | ||
| import { useVaultRegistry } from '@/contexts/VaultRegistryContext'; | ||
| import { formatReadable } from '@/utils/balance'; | ||
| import { formatChartTime } from '@/utils/chart'; | ||
| import { getSlicedAddress } from '@/utils/address'; | ||
| import { useSupplierPositionHistory, type SupplierHoldingsTimeframe } from '@/hooks/useSupplierPositionHistory'; | ||
| import { chartTooltipCursor, chartLegendStyle } from './chart-utils'; | ||
| import type { Market } from '@/utils/types'; | ||
| import type { SupportedNetworks } from '@/utils/networks'; | ||
|
|
||
| type SupplierHoldingsChartProps = { | ||
| marketId: string; | ||
| chainId: SupportedNetworks; | ||
| market: Market; | ||
| }; | ||
|
|
||
| const TIMEFRAME_LABELS: Record<SupplierHoldingsTimeframe, string> = { | ||
| '7d': '7D', | ||
| '30d': '30D', | ||
| }; | ||
|
|
||
| function SupplierHoldingsChart({ marketId, chainId, market }: SupplierHoldingsChartProps) { | ||
| const [selectedTimeframe, setSelectedTimeframe] = useState<SupplierHoldingsTimeframe>('7d'); | ||
| const [visibleLines, setVisibleLines] = useState<Record<string, boolean>>({}); | ||
| const chartColors = useChartColors(); | ||
| const { getVaultByAddress } = useVaultRegistry(); | ||
|
|
||
| const { data, suppliers, isLoading } = useSupplierPositionHistory(marketId, chainId, market, selectedTimeframe); | ||
|
|
||
| // Initialize visible lines when suppliers change | ||
| useMemo(() => { | ||
| if (suppliers.length > 0 && Object.keys(visibleLines).length === 0) { | ||
| const initial: Record<string, boolean> = {}; | ||
| for (const supplier of suppliers) { | ||
| initial[supplier.address.toLowerCase()] = true; | ||
| } | ||
| setVisibleLines(initial); | ||
| } | ||
| }, [suppliers, visibleLines]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/features/market-detail/components/charts/supplier-holdings-chart.tsx | head -60Repository: antoncoding/monarch
Length of output: 3096
🏁 Script executed:
# Check if there are other usages of visibleLines in the file to understand the full context
rg "visibleLines" src/features/market-detail/components/charts/supplier-holdings-chart.tsx -A 2 -B 2Repository: antoncoding/monarch
Length of output: 1380
🏁 Script executed:
# Check the entire file to see if there are any other references or context we need
wc -l src/features/market-detail/components/charts/supplier-holdings-chart.tsxRepository: antoncoding/monarch
Length of output: 139
🏁 Script executed:
# Check the useSupplierPositionHistory hook to understand if suppliers can be updated dynamically
rg "useSupplierPositionHistory" -A 10 | head -40Repository: antoncoding/monarch
Length of output: 3498
Replace useMemo with useEffect and merge new suppliers into visibleLines.
Using useMemo for state updates is improper. Additionally, the condition Object.keys(visibleLines).length === 0 prevents new suppliers from being added once visibleLines is populated. When selectedTimeframe changes, the suppliers list updates but new suppliers won't have visibility toggles initialized.
Suggested change
-import { useState, useMemo, useCallback } from 'react';
+import { useState, useMemo, useCallback, useEffect } from 'react';
...
- useMemo(() => {
- if (suppliers.length > 0 && Object.keys(visibleLines).length === 0) {
- const initial: Record<string, boolean> = {};
- for (const supplier of suppliers) {
- initial[supplier.address.toLowerCase()] = true;
- }
- setVisibleLines(initial);
- }
- }, [suppliers, visibleLines]);
+ useEffect(() => {
+ if (suppliers.length === 0) return;
+ setVisibleLines((prev) => {
+ const next = { ...prev };
+ for (const supplier of suppliers) {
+ const addr = supplier.address.toLowerCase();
+ if (!(addr in next)) next[addr] = true;
+ }
+ return next;
+ });
+ }, [suppliers]);📝 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.
| import { useState, useMemo, useCallback } from 'react'; | |
| import type { Address } from 'viem'; | |
| import { Card } from '@/components/ui/card'; | |
| import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select'; | |
| import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; | |
| import { Spinner } from '@/components/ui/spinner'; | |
| import { TokenIcon } from '@/components/shared/token-icon'; | |
| import { useChartColors } from '@/constants/chartColors'; | |
| import { useVaultRegistry } from '@/contexts/VaultRegistryContext'; | |
| import { formatReadable } from '@/utils/balance'; | |
| import { formatChartTime } from '@/utils/chart'; | |
| import { getSlicedAddress } from '@/utils/address'; | |
| import { useSupplierPositionHistory, type SupplierHoldingsTimeframe } from '@/hooks/useSupplierPositionHistory'; | |
| import { chartTooltipCursor, chartLegendStyle } from './chart-utils'; | |
| import type { Market } from '@/utils/types'; | |
| import type { SupportedNetworks } from '@/utils/networks'; | |
| type SupplierHoldingsChartProps = { | |
| marketId: string; | |
| chainId: SupportedNetworks; | |
| market: Market; | |
| }; | |
| const TIMEFRAME_LABELS: Record<SupplierHoldingsTimeframe, string> = { | |
| '7d': '7D', | |
| '30d': '30D', | |
| }; | |
| function SupplierHoldingsChart({ marketId, chainId, market }: SupplierHoldingsChartProps) { | |
| const [selectedTimeframe, setSelectedTimeframe] = useState<SupplierHoldingsTimeframe>('7d'); | |
| const [visibleLines, setVisibleLines] = useState<Record<string, boolean>>({}); | |
| const chartColors = useChartColors(); | |
| const { getVaultByAddress } = useVaultRegistry(); | |
| const { data, suppliers, isLoading } = useSupplierPositionHistory(marketId, chainId, market, selectedTimeframe); | |
| // Initialize visible lines when suppliers change | |
| useMemo(() => { | |
| if (suppliers.length > 0 && Object.keys(visibleLines).length === 0) { | |
| const initial: Record<string, boolean> = {}; | |
| for (const supplier of suppliers) { | |
| initial[supplier.address.toLowerCase()] = true; | |
| } | |
| setVisibleLines(initial); | |
| } | |
| }, [suppliers, visibleLines]); | |
| import { useState, useMemo, useCallback, useEffect } from 'react'; | |
| import type { Address } from 'viem'; | |
| import { Card } from '@/components/ui/card'; | |
| import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select'; | |
| import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; | |
| import { Spinner } from '@/components/ui/spinner'; | |
| import { TokenIcon } from '@/components/shared/token-icon'; | |
| import { useChartColors } from '@/constants/chartColors'; | |
| import { useVaultRegistry } from '@/contexts/VaultRegistryContext'; | |
| import { formatReadable } from '@/utils/balance'; | |
| import { formatChartTime } from '@/utils/chart'; | |
| import { getSlicedAddress } from '@/utils/address'; | |
| import { useSupplierPositionHistory, type SupplierHoldingsTimeframe } from '@/hooks/useSupplierPositionHistory'; | |
| import { chartTooltipCursor, chartLegendStyle } from './chart-utils'; | |
| import type { Market } from '@/utils/types'; | |
| import type { SupportedNetworks } from '@/utils/networks'; | |
| type SupplierHoldingsChartProps = { | |
| marketId: string; | |
| chainId: SupportedNetworks; | |
| market: Market; | |
| }; | |
| const TIMEFRAME_LABELS: Record<SupplierHoldingsTimeframe, string> = { | |
| '7d': '7D', | |
| '30d': '30D', | |
| }; | |
| function SupplierHoldingsChart({ marketId, chainId, market }: SupplierHoldingsChartProps) { | |
| const [selectedTimeframe, setSelectedTimeframe] = useState<SupplierHoldingsTimeframe>('7d'); | |
| const [visibleLines, setVisibleLines] = useState<Record<string, boolean>>({}); | |
| const chartColors = useChartColors(); | |
| const { getVaultByAddress } = useVaultRegistry(); | |
| const { data, suppliers, isLoading } = useSupplierPositionHistory(marketId, chainId, market, selectedTimeframe); | |
| // Initialize visible lines when suppliers change | |
| useEffect(() => { | |
| if (suppliers.length === 0) return; | |
| setVisibleLines((prev) => { | |
| const next = { ...prev }; | |
| for (const supplier of suppliers) { | |
| const addr = supplier.address.toLowerCase(); | |
| if (!(addr in next)) next[addr] = true; | |
| } | |
| return next; | |
| }); | |
| }, [suppliers]); |
🤖 Prompt for AI Agents
In `@src/features/market-detail/components/charts/supplier-holdings-chart.tsx`
around lines 1 - 46, The initialization of visibleLines currently uses useMemo
(should be a side-effect) and only sets state when visibleLines is empty,
preventing new suppliers from being added on timeframe changes; replace the
useMemo block with a useEffect that runs when suppliers or selectedTimeframe
change, and inside compute a new visibility map by merging existing visibleLines
with entries for any supplier in suppliers (use supplier.address.toLowerCase()
as the key) defaulting to true for new keys, then call setVisibleLines with the
merged object; reference the visibleLines state, suppliers array,
setVisibleLines function, and selectedTimeframe in the effect dependencies.
| if (relevantTxs.length === 0) { | ||
| return { data: [], suppliers: [] }; | ||
| } |
There was a problem hiding this comment.
No‑transaction path drops all suppliers.
This early return skips the constant‑position backfill later, so the chart shows “No history” even when suppliers exist but had no activity. Consider removing the early return.
Suggested change
- if (relevantTxs.length === 0) {
- return { data: [], suppliers: [] };
- }📝 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.
| if (relevantTxs.length === 0) { | |
| return { data: [], suppliers: [] }; | |
| } |
🤖 Prompt for AI Agents
In `@src/hooks/useSupplierPositionHistory.ts` around lines 140 - 142, The early
return when relevantTxs.length === 0 in useSupplierPositionHistory prevents the
constant-position backfill from running and clears the suppliers list; remove
this early return (or instead build and return suppliers via the existing
backfill logic) so that when relevantTxs is empty the hook still calls the
backfill/constant-position logic (e.g., backfillConstantPositions or the code
that populates suppliers and data) and returns data: [] but with suppliers
populated; keep other control flow and error handling intact.
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/data-sources/morpho-api/market-suppliers.ts (1)
74-77:⚠️ Potential issue | 🟡 Minor
totalCountdoesn't reflect filtered items.
totalCountfrom the API includes positions with null state, butmappedItemsexcludes them. Pagination logic relying ontotalCountmay show incorrect page counts.If this is intentional (e.g., UI doesn't paginate filtered results), ignore. Otherwise, consider returning
mappedItems.lengthor documenting the behavior.
🧹 Nitpick comments (1)
src/data-sources/morpho-api/market-suppliers.ts (1)
66-72: TypeScript won't narrow the type afterfilter.The
.filter((item) => item.state !== null)doesn't create a type guard, soitem.stateremains{ supplyShares: string } | nullin the.map(). This may cause a TS error in strict mode.Use a type predicate to narrow properly
// Map to unified type, filtering out items with null state const mappedItems = items - .filter((item) => item.state !== null) + .filter((item): item is (typeof items)[number] & { state: NonNullable<(typeof items)[number]['state']> } => item.state !== null) .map((item) => ({ userAddress: item.user.address, supplyShares: item.state.supplyShares, }));
Problem
The Morpho API sometimes returns a
NOT_FOUNDerror alongside valid data. For example, when fetching suppliers for market0x9103c3b4e834476c9a62ea009ba2c884ee42e94e6e314a26f04d312434191836on Base (chainId 8453):{ "errors": [{ "message": "No results matching given parameters", "status": "NOT_FOUND" }], "data": { "marketPositions": { "items": [...192 suppliers...] } } }Previously, our fetcher would see the
NOT_FOUNDerror and returnnull, discarding all 192 valid suppliers. This caused the analysis tab charts to show "No suppliers found" even when data existed.Solution
Check if valid data exists before returning
nullonNOT_FOUNDerrors. If data is present, use it despite the error.Test
Navigate to:
/market/8453/0x9103c3b4e834476c9a62ea009ba2c884ee42e94e6e314a26f04d312434191836Summary by CodeRabbit
New Features
Bug Fixes