Skip to content

fix(api): handle Morpho API returning NOT_FOUND error with valid data#370

Merged
antoncoding merged 3 commits intomasterfrom
fix/morpho-api-not-found-with-data
Feb 5, 2026
Merged

fix(api): handle Morpho API returning NOT_FOUND error with valid data#370
antoncoding merged 3 commits intomasterfrom
fix/morpho-api-not-found-with-data

Conversation

@starksama
Copy link
Copy Markdown
Collaborator

@starksama starksama commented Feb 5, 2026

Problem

The Morpho API sometimes returns a NOT_FOUND error alongside valid data. For example, when fetching suppliers for market 0x9103c3b4e834476c9a62ea009ba2c884ee42e94e6e314a26f04d312434191836 on 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_FOUND error and return null, 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 null on NOT_FOUND errors. If data is present, use it despite the error.

Test

Navigate to: /market/8453/0x9103c3b4e834476c9a62ea009ba2c884ee42e94e6e314a26f04d312434191836

  • Before: Analysis tab shows "No suppliers found"
  • After: Analysis tab shows supplier concentration charts with 192 suppliers

Summary by CodeRabbit

  • New Features

    • Added a Supplier Holdings chart in market details (7D/30D selector, stats, interactive legend).
    • Added a hook to provide per-supplier time-series positions and supplier roster for charting.
  • Bug Fixes

    • Improved GraphQL error handling to return valid data when present despite NOT_FOUND errors.
    • Handled nullable supplier/borrower states from the Morpho API to ignore null entries.

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.
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Feb 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
monarch Ready Ready Preview, Comment Feb 5, 2026 9:33am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 5, 2026

Warning

Rate limit exceeded

@starksama has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 17 minutes and 1 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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.

📝 Walkthrough

Walkthrough

Adjusts 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

Cohort / File(s) Summary
Morpho API fetcher
src/data-sources/morpho-api/fetchers.ts
NOT_FOUND path now checks for non-null result.data and returns the result when valid, adds log; GraphQL errors cast to any when logging.
Morpho API -> Market borrowers
src/data-sources/morpho-api/market-borrowers.ts
Allow item.state to be null; filter out null states before mapping borrower entries; use item.user.address and item.state.* for mapping.
Morpho API -> Market suppliers
src/data-sources/morpho-api/market-suppliers.ts
Allow item.state to be null; filter out null states before mapping supplier entries to unified shape.
Supplier position data
src/hooks/useSupplierPositionHistory.ts
New hook and types (SupplierHoldingsTimeframe, SupplierPositionDataPoint, SupplierInfo) that fetches top suppliers, paginates transactions (Morpho API with Subgraph fallback), reconstructs per-supplier position histories and returns timeline + metadata.
Chart UI
src/features/market-detail/components/charts/supplier-holdings-chart.tsx
New SupplierHoldingsChart component: timeframe selector, legend toggles, tooltip, multi-line chart per supplier, top-level stats, loading/empty states.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main fix: handling NOT_FOUND errors from Morpho API when valid data is present, which is the core problem solved across multiple files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/morpho-api-not-found-with-data

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added the bug Something isn't working label Feb 5, 2026
Some positions returned by Morpho API have state: null, causing crashes
when mapping. Now we filter these out before mapping to our types.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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.

Comment on lines +1 to +46
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]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/features/market-detail/components/charts/supplier-holdings-chart.tsx | head -60

Repository: 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 2

Repository: 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.tsx

Repository: 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 -40

Repository: 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.

Suggested change
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.

Comment on lines +140 to +142
if (relevantTxs.length === 0) {
return { data: [], suppliers: [] };
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

totalCount doesn't reflect filtered items.

totalCount from the API includes positions with null state, but mappedItems excludes them. Pagination logic relying on totalCount may show incorrect page counts.

If this is intentional (e.g., UI doesn't paginate filtered results), ignore. Otherwise, consider returning mappedItems.length or documenting the behavior.

🧹 Nitpick comments (1)
src/data-sources/morpho-api/market-suppliers.ts (1)

66-72: TypeScript won't narrow the type after filter.

The .filter((item) => item.state !== null) doesn't create a type guard, so item.state remains { supplyShares: string } | null in 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,
       }));

Copy link
Copy Markdown
Owner

@antoncoding antoncoding left a comment

Choose a reason for hiding this comment

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

LGTM

@antoncoding antoncoding merged commit a4319ed into master Feb 5, 2026
4 checks passed
@antoncoding antoncoding deleted the fix/morpho-api-not-found-with-data branch February 5, 2026 10:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants