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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ When touching transaction and position flows, validation MUST include all releva
38. **Morpho vault query schema integrity**: shared Morpho vault metadata/rate queries must only request fields confirmed on the live `Vault`/`VaultState` schema. Do not add speculative top-level fields to the registry query, and do not swallow schema errors in a way that turns the global vault registry into an empty success state.
39. **User position discovery integrity**: when a shared upstream supports chain-scoped bulk position discovery (`userAddress_in` plus `chainId_in` or equivalent), use that batched chokepoint to collect position market keys before falling back to per-chain queries. Do not force one `userByAddress` request per chain when the backend can already return mixed-chain positions in one response.
40. **Source-discovery failure integrity**: market/position source-discovery hooks must fail closed when both primary and fallback providers fail for a chain. Do not convert dual-source fetch failures into empty success states; surface typed errors with source and network metadata so callers can fall back explicitly or show the failure.
41. **Oracle metadata source integrity**: oracle vendor/type/feed classification must resolve from the scanner metadata source keyed by `chainId + oracleAddress`. Do not reintroduce Morpho API `oracles` feed enrichment into market objects or UI/filter/warning logic as a fallback source for oracle structure.
42. **Mixed oracle badge signal integrity**: when a standard or meta oracle contains both classified feeds and unknown/unverified feeds, vendor badges and their tooltips must preserve both signals together (known vendor icon(s) plus unknown indicator/text) instead of collapsing to only the recognized vendor.


### REQUIRED: Regression Rule Capture
Expand Down
39 changes: 26 additions & 13 deletions docs/TECHNICAL_OVERVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ RootLayout
liquidityAssets, liquidityUsd;
utilization;
};
oracle?: { data: MorphoChainlinkOracleData };
}
```

Expand Down Expand Up @@ -118,9 +117,25 @@ GroupedPosition { // Grouped by loan asset

### Oracle
```typescript
MorphoChainlinkOracleData {
baseFeedOne, baseFeedTwo: OracleFeed; // Base token feeds
quoteFeedOne, quoteFeedTwo: OracleFeed; // Quote token feeds
StandardOracleOutput {
address: string;
chainId: number;
type: 'standard';
data: OracleOutputData; // { baseFeedOne, baseFeedTwo, quoteFeedOne, quoteFeedTwo, baseVault, quoteVault }
}

MetaOracleOutput {
address: string;
chainId: number;
type: 'meta';
data: MetaOracleOutputData; // { primaryOracle, backupOracle, currentOracle, oracleSources, ... }
}

NonStandardOracleOutput {
address: string;
chainId: number;
type: 'custom' | 'unknown';
data: { reason: string };
}
```

Expand Down Expand Up @@ -167,9 +182,6 @@ Market metrics: Monarch metrics API via `/api/monarch/metrics`
### Static Data (Build-time or cached)
| Data Type | Source | Location |
|-----------|--------|----------|
| Oracle definitions | Pre-generated | `/src/constants/oracle/oracle-cache.json` |
| Chainlink feeds | Pre-generated | `/src/constants/oracle/chainlink/` |
| Redstone feeds | Pre-generated | `/src/constants/oracle/redstone/` |
| Network configs | Hardcoded | `/src/utils/networks.ts` |
| Default blacklist | Hardcoded | `/src/constants/markets/blacklisted.ts` |

Expand All @@ -185,15 +197,15 @@ Market metrics: Monarch metrics API via `/api/monarch/metrics`
| Vault detail/settings metadata | Monarch GraphQL + narrow RPC fallback | 30s | `useVaultV2Data` |
| Vault allocations | On-chain multicall | 30s | `useAllocationsQuery` |
| Token balances | On-chain multicall | 5 min | `useUserBalancesQuery` |
| Oracle prices | Morpho API | 5 min | `useOracleDataQuery` |
| Oracle metadata | Scanner Gist | 30 min | `useOracleMetadata` / `useAllOracleMetadata` |
| Merkl rewards | Merkl API | On demand | `useMerklCampaignsQuery` |
| Market liquidations | Morpho API/Subgraph | 5 min stale | `useMarketLiquidations` |

### Data Flow Patterns

**Market Data Flow:**
```
Raw API fetch → Blacklist filtering → Oracle enrichment →
Raw API fetch → Blacklist filtering →
Split: allMarkets vs whitelistedMarkets
```

Expand Down Expand Up @@ -263,7 +275,6 @@ All hooks in `/src/hooks/queries/` follow React Query patterns:
| `useMarketsQuery` | `['markets']` | 5 min | 5 min | Yes |
| `useMarketMetricsQuery` | `['market-metrics', ...]` | 5 min | 5 min | No |
| `useTokensQuery` | `['tokens']` | 5 min | 5 min | Yes |
| `useOracleDataQuery` | `['oracle-data']` | 5 min | 5 min | Yes |
| `useUserBalancesQuery` | `['user-balances', addr, networks]` | 30s | - | Yes |
| `useUserVaultsV2Query` | `['user-vaults-v2', addr]` | 60s | - | Yes |
| `useVaultV2Data` | `['vault-v2-data', addr, chainId]` | 30s | - | No |
Expand Down Expand Up @@ -309,15 +320,17 @@ Fallback Strategy:
2. Parallel queries start:
- usePublicClient() for on-chain reads
- useOracleDataQuery() for oracle enrichment
- useOracleMetadata() for oracle classification and feed details
3. Market fetch:
a. Try on-chain snapshot (viem multicall)
b. Try Morpho API (if supported)
c. Fallback to Subgraph
d. Merge snapshot with API state
4. Oracle enrichment via useMemo()
4. Oracle metadata resolves separately by `chainId + oracleAddress`
- Standard/meta oracle UI reads scanner-native `OracleOutputData` / `MetaOracleOutputData`
- No Morpho API oracle feed enrichment or local feed-shape conversion
5. Return { data: enrichedMarket, isLoading, error }
```
Expand All @@ -328,7 +341,7 @@ Fallback Strategy:
2. **Parallel Execution**: `Promise.all()` for multi-network
3. **Graceful Degradation**: Partial data > Error
4. **Two-Phase Market**: On-chain snapshot + API state
5. **Hybrid Caching**: Static JSON + dynamic API (oracles)
5. **Hybrid Reads**: Scanner metadata for oracle structure + live RPC/API for market state

---

Expand Down
2 changes: 0 additions & 2 deletions src/components/DataPrefetcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@ import { usePathname } from 'next/navigation';
import { useMarketsQuery } from '@/hooks/queries/useMarketsQuery';
import { useTokensQuery } from '@/hooks/queries/useTokensQuery';
import { useMerklCampaignsQuery } from '@/hooks/queries/useMerklCampaignsQuery';
import { useOracleDataQuery } from '@/hooks/queries/useOracleDataQuery';

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

return null;
}
Expand Down
4 changes: 1 addition & 3 deletions src/data-sources/morpho-api/positions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,7 @@ type RawPositionMarketItem = {

const MORPHO_POSITION_MARKETS_PAGE_SIZE = 500;

const hasNonZeroPositionState = (
state: ValidPositionMarketItem['state'],
): boolean => {
const hasNonZeroPositionState = (state: ValidPositionMarketItem['state']): boolean => {
if (!state) {
return false;
}
Expand Down
47 changes: 0 additions & 47 deletions src/data-sources/morpho-api/vault-allocations.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import { vaultAllocationQuery } from '@/graphql/vault-allocation-query';
import type { SupportedNetworks } from '@/utils/networks';
import { morphoGraphqlFetcher } from './fetchers';

export type VaultAllocationMarket = {
uniqueKey: string;
loanAsset: {
Expand Down Expand Up @@ -31,46 +27,3 @@ export type VaultAllocation = {
supplyAssets: string;
supplyCap: string;
};

export type VaultAllocationData = {
address: string;
name: string;
symbol: string;
asset: {
address: string;
symbol: string;
decimals: number;
};
state: {
totalAssets: string;
allocation: VaultAllocation[];
};
};

type VaultAllocationApiResponse = {
data?: {
vaultByAddress?: VaultAllocationData | null;
};
errors?: { message: string }[];
};

/**
* Fetches a MetaMorpho vault's allocation data from the Morpho Blue API.
* Returns the vault's markets with their supply amounts and liquidity.
*/
export const fetchVaultAllocations = async (vaultAddress: string, chainId: SupportedNetworks): Promise<VaultAllocationData | null> => {
const response = await morphoGraphqlFetcher<VaultAllocationApiResponse>(vaultAllocationQuery, {
address: vaultAddress,
chainId,
});

if (response?.errors?.length) {
console.warn('fetchVaultAllocations errors:', response.errors);
}

if (!response?.data?.vaultByAddress) {
return null;
}

return response.data.vaultByAddress;
};
1 change: 0 additions & 1 deletion src/features/market-detail/components/market-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -756,7 +756,6 @@ export function MarketHeader({
</div>

<OracleTypeInfo
oracleData={market.oracle?.data}
oracleAddress={market.oracleAddress}
chainId={market.morphoBlue.chain.id}
useBadge
Expand Down
2 changes: 0 additions & 2 deletions src/features/markets/components/market-details-block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@ export function MarketDetailsBlock({
<div className="flex items-center gap-2 text-xs opacity-70">
<span>·</span>
<OracleVendorBadge
oracleData={market.oracle?.data}
oracleAddress={market.oracleAddress}
showText={false}
chainId={market.morphoBlue.chain.id}
Expand Down Expand Up @@ -180,7 +179,6 @@ export function MarketDetailsBlock({
<div className="mb-4 flex items-center gap-4">
<div className="flex items-center gap-2">
<OracleVendorBadge
oracleData={market.oracle?.data}
oracleAddress={market.oracleAddress}
showText
useTooltip={false}
Expand Down
6 changes: 0 additions & 6 deletions src/features/markets/components/market-identity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,6 @@ export function MarketIdentity({
{showLltv && <span className="rounded bg-hovered px-1.5 py-0.5 text-xs font-medium text-secondary">{lltv}%</span>}
{showOracle && (
<OracleVendorBadge
oracleData={market.oracle?.data}
oracleAddress={market.oracleAddress}
chainId={chainId}
useTooltip
Expand Down Expand Up @@ -218,7 +217,6 @@ export function MarketIdentity({
{showLltv && <span className="rounded bg-hovered px-1.5 py-0.5 text-xs font-medium text-secondary">{lltv}% LLTV</span>}
{showOracle && (
<OracleVendorBadge
oracleData={market.oracle?.data}
oracleAddress={market.oracleAddress}
chainId={chainId}
useTooltip
Expand Down Expand Up @@ -260,7 +258,6 @@ export function MarketIdentity({
{showLltv && <span className="rounded bg-hovered px-1.5 py-0.5 text-xs font-medium text-secondary">{lltv}% LLTV</span>}
{showOracle && (
<OracleVendorBadge
oracleData={market.oracle?.data}
oracleAddress={market.oracleAddress}
chainId={chainId}
useTooltip
Expand Down Expand Up @@ -294,7 +291,6 @@ export function MarketIdentity({
{showLltv && <span className="rounded bg-hovered px-1.5 py-0.5 text-xs font-medium text-secondary">{lltv}% LLTV</span>}
{showOracle && (
<OracleVendorBadge
oracleData={market.oracle?.data}
oracleAddress={market.oracleAddress}
chainId={chainId}
useTooltip
Expand Down Expand Up @@ -331,7 +327,6 @@ export function MarketIdentity({
{showLltv && <span className="rounded bg-hovered px-1.5 py-0.5 text-xs font-medium text-secondary">{lltv}% LLTV</span>}
{showOracle && (
<OracleVendorBadge
oracleData={market.oracle?.data}
oracleAddress={market.oracleAddress}
chainId={chainId}
useTooltip
Expand Down Expand Up @@ -362,7 +357,6 @@ export function MarketIdentity({
{showLltv && <span className="rounded bg-hovered px-1.5 py-0.5 text-xs font-medium text-secondary">{lltv}% LLTV</span>}
{showOracle && (
<OracleVendorBadge
oracleData={market.oracle?.data}
oracleAddress={market.oracleAddress}
chainId={chainId}
useTooltip
Expand Down
2 changes: 0 additions & 2 deletions src/features/markets/components/market-info-block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ export function MarketInfoBlock({ market, amount, className }: MarketInfoBlockPr
) : (
<OracleVendorBadge
showText
oracleData={market.oracle?.data}
oracleAddress={market.oracleAddress}
chainId={market.morphoBlue.chain.id}
useTooltip={false}
Expand Down Expand Up @@ -119,7 +118,6 @@ export function MarketInfoBlockCompact({ market, amount, className }: MarketInfo
) : (
<OracleVendorBadge
showText
oracleData={market.oracle?.data}
oracleAddress={market.oracleAddress}
useTooltip={false}
chainId={market.morphoBlue.chain.id}
Expand Down
1 change: 0 additions & 1 deletion src/features/markets/components/market-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ export function MarketSelector({ market, onAdd, disabled = false }: MarketSelect
</div>
<div className="flex items-center gap-2 text-xs opacity-70">
<OracleVendorBadge
oracleData={market.oracle?.data}
oracleAddress={market.oracleAddress}
showText={false}
chainId={market.morphoBlue.chain.id}
Expand Down
9 changes: 4 additions & 5 deletions src/features/markets/components/markets-table-same-loan.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { TrustedByCell } from '@/features/autovault/components/trusted-vault-bad
import { getVaultKey, type TrustedVault } from '@/constants/vaults/known_vaults';
import { useFreshMarketsState } from '@/hooks/useFreshMarketsState';
import { useModal } from '@/hooks/useModal';
import { useAllOracleMetadata } from '@/hooks/useOracleMetadata';
import { getStandardOracleDataFromMetadata, useAllOracleMetadata } from '@/hooks/useOracleMetadata';
import { useRateLabel } from '@/hooks/useRateLabel';
import { useTrustedVaults } from '@/stores/useTrustedVaults';
import { useMarketPreferences } from '@/stores/useMarketPreferences';
Expand Down Expand Up @@ -455,10 +455,9 @@ export function MarketsTableWithSameLoanAsset({

markets.forEach((m) => {
if (!m?.market?.morphoBlue?.chain?.id) return;
const vendorInfo = parsePriceFeedVendors(m.market.oracle?.data, m.market.morphoBlue.chain.id, {
metadataMap: oracleMetadataMap,
oracleAddress: m.market.oracleAddress,
});
const vendorInfo = parsePriceFeedVendors(
getStandardOracleDataFromMetadata(oracleMetadataMap, m.market.oracleAddress, m.market.morphoBlue.chain.id),
);
if (vendorInfo?.coreVendors) {
vendorInfo.coreVendors.forEach((vendor) => oracleSet.add(vendor));
}
Expand Down
Loading