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
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,12 @@ When touching transaction and position flows, validation MUST include all releva
41. **Envio market-detail read integrity**: when Envio backs market-detail participants or activity tables, share-to-asset conversions must use the already-loaded live market state keyed by `chainId + market.uniqueKey` instead of a second indexer totals query; participant caches must not store state-derived converted values behind a key that ignores the live state; event/liquidation tables must fetch only the requested page window with correct merged ordering rather than scanning full market history unless the UI explicitly requires an exact total count; provider fallbacks must page at the provider boundary or fail closed with typed source/network errors instead of fetching full history and slicing client-side or returning empty success on missing subgraph configuration; unknown-total pagination must use an explicit open-ended `hasNextPage` mode instead of synthesizing a moving “last page”; and page transitions in that mode must start from a neutral loading state rather than reusing stale rows from the previous page.
42. **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.
43. **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.
44. **Grouped realized-rate aggregation integrity**: when aggregating realized APY/APR across multiple markets or positions, aggregate raw earned value and capital-time exposure first, then annualize once at the grouped level. Do not weight or average already-annualized per-market realized rates, because dust positions with tiny capital can dominate the result spuriously.
45. **Primary-empty fallback integrity**: when a primary indexer/provider backs market participants, user-position discovery, user history, or similar source-discovery reads, treat an empty primary result as authoritative only when the primary query completed successfully and fully covered the exact requested scope. Empty results from partial pages, coverage-limited endpoints, or lag-prone non-authoritative reads must still fall through to the next provider; empty results from scoped, fully paginated primary reads must not trigger fallback.
46. **External address-filter casing integrity**: when querying external indexers by address string fields (`user`, `onBehalf`, `borrower`, etc.), do not assume case-insensitive matching. Use the backend’s documented canonical normalization, or query safe canonical/exact-case variants of the same address and keep downstream identity/dedup keyed by canonical lowercase `chainId + address` or `chainId + txHash`.
47. **Indexed event-history pagination integrity**: when stitching user history from raw indexer event tables, page each table with a stable unique order (include an event-unique tie-breaker such as indexed row `id`, not just `timestamp` or `txHash`), freeze the page window against new-head inserts during pagination, dedupe API pages by source event identity, and reconcile local receipt caches to indexed history with a cross-source event merge key (`hash + type + market + assets + shares`, or stronger) rather than raw tx-hash presence.
48. **Paged history source consistency integrity**: when a paged transaction/history read falls back from a primary provider, choose the fallback provider once per query and keep it fixed across all pages. Do not mix providers page-by-page, and do not silently return truncated history when page caps are hit; fail closed with an explicit error instead.
49. **Merged-history pagination query integrity**: when a UI paginates over a merged transaction/history dataset that is fetched as a full filtered set (for example because the backend cannot page a merged multi-table stream correctly), keep page changes local to the UI and do not include `skip`/`first` or current-page state in the upstream query key. Fetch once per filter scope, then slice locally, so pagination controls do not re-trigger the full upstream history cycle on every click.


### REQUIRED: Regression Rule Capture
Expand Down
72 changes: 65 additions & 7 deletions docs/TECHNICAL_OVERVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,18 +195,76 @@ Market metrics: Monarch metrics API via `/api/monarch/metrics`
|-----------|--------|---------|------------|
| Markets list | Morpho API/Subgraph | 5 min stale | `useMarketsQuery` |
| Market metrics (flows, trending) | Monarch API | 5 min stale | `useMarketMetricsQuery` |
| Market state (APY, utilization) | Morpho API | 30s stale | `useMarketData` |
| User positions | Morpho API + on-chain | 5 min | `useUserPositions` |
| Market state (APY, utilization) | RPC snapshot + Morpho API/Subgraph | 30s stale | `useMarketData` |
| User positions | Monarch position discovery + on-chain snapshots + market registry from `useProcessedMarkets` | 5 min | `useUserPositions` |
| User transaction history | Monarch GraphQL → Morpho API → Subgraph (`assetIds` queries still skip Monarch) | 60s | `useUserTransactionsQuery` |
| Vaults list | Morpho API | 5 min | `useAllMorphoVaultsQuery` |
| User autovault metadata | Monarch GraphQL + on-chain enrichment | 60s | `useUserVaultsV2Query` |
| Vault detail/settings metadata | Monarch GraphQL + narrow RPC fallback | 30s | `useVaultV2Data` |
| Market detail participants/activity | Monarch GraphQL + Morpho API/Subgraph fallback | 2-5 min stale | `useMarketSuppliers` / `useMarketBorrowers` / `useMarketSupplies` / `useMarketBorrows` |
| Vault allocations | On-chain multicall | 30s | `useAllocationsQuery` |
| Token balances | On-chain multicall | 5 min | `useUserBalancesQuery` |
| Oracle metadata | Scanner Gist | 30 min | `useOracleMetadata` / `useAllOracleMetadata` |
| Merkl rewards | Merkl API | On demand | `useMerklCampaignsQuery` |
| User rewards and distributions | Morpho rewards REST + Merkl API | 30s | `useUserRewardsQuery` |
| Reward campaigns | Merkl API | 5 min stale | `useMerklCampaignsQuery` |
| Market liquidations | Monarch GraphQL + Morpho API/Subgraph fallback | 5 min stale | `useMarketLiquidations` |
| Admin stats transactions | Monarch GraphQL | 2 min stale | `useMonarchTransactions` |
| Admin stats transactions | Monarch GraphQL + market registry/token price enrichment | 2 min stale | `useMonarchTransactions` |

### Data Hook Responsibility Matrix

This is the migration checklist for the Monarch API (Envio GraphQL endpoint). "Full Monarch support" here means the feature would still work if Morpho API and subgraph reads were unavailable.

Hooks omitted from this matrix are local-state hooks or pure view/composition helpers that do not own remote transport reads.

#### Core Markets And Positions

| Hook / Family | Responsibility | Infra Today | Full Monarch Support Still Needs |
|---------------|----------------|-------------|----------------------------------|
| `useMarketsQuery` | Global market registry used across the app | Morpho API first per chain, then subgraph | Monarch market registry and market detail parity |
| `useProcessedMarkets` | Blacklist/filtering layer on top of market registry, plus USD backfill | `useMarketsQuery` + `useTokenPrices` | Inherits `useMarketsQuery`; also needs a Monarch-native token price source if we want to remove Morpho price reads |
| `useMarketData` | Single-market detail shell with freshest live state | RPC snapshot + Morpho API, then subgraph | Monarch single-market metadata/detail path |
| `useMarketHistoricalData` | Historical market chart series | Morpho historical API, then subgraph | Monarch historical market snapshots/timeseries |
| `useTokenPrices` | Token USD price lookup and peg fallback used by markets/admin stats | Morpho price API + major price fallback | Monarch price endpoint or another canonical replacement |
| `useUserPositions` | Discover all markets where a user has positions, then attach live balances | Monarch batched `Position` discovery + RPC snapshots/oracle reads + market metadata from `useProcessedMarkets`; Morpho/Subgraph fallback for discovery | Monarch market registry/detail if position objects should no longer depend on Morpho/Subgraph market metadata |
| `useUserPosition` | Single-market user position | RPC snapshot first; if snapshot unavailable, Monarch position state when local market exists; then Morpho/Subgraph fallback | Same market-registry/detail gap as `useUserPositions` |
| `useUserTransactionsQuery` / `fetchUserTransactions` | User history across one or many chains | Monarch user-event tables first; fallback Morpho API, then subgraph; `assetIds` filter still bypasses Monarch | Asset-address filtered history support to fully back reports and any asset-scoped history views |
| `useUserPositionsSummaryData` | Portfolio earnings summary for active positions | `useUserPositions` + `useUserTransactionsQuery` + RPC block/snapshot helpers | Inherits the remaining `useUserPositions` and `useUserTransactionsQuery` gaps |
| `usePositionReport` | Asset-scoped earnings/report generation | `fetchUserTransactions(assetIds=...)` + RPC block/snapshot helpers | Still blocked on Monarch support for `assetIds`-scoped user history |
| `usePositionHistoryChart` | Derive chart points for one asset/market group | Pure derivation from transactions + snapshots already fetched elsewhere | No backend gap; inherits upstream history/snapshot gaps |

#### Market Detail And Admin Reads

| Hook / Family | Responsibility | Infra Today | Full Monarch Support Still Needs |
|---------------|----------------|-------------|----------------------------------|
| `useMarketSuppliers` / `useMarketBorrowers` | Paginated open positions on one market | Monarch first, then Morpho API, then subgraph | Already Monarch-first; no new Envio schema gap identified |
| `useAllMarketSuppliers` / `useAllMarketBorrowers` | Non-paginated top positions for concentration charts | Monarch first, then Morpho API, then subgraph | Already Monarch-first; no new Envio schema gap identified |
| `useMarketSupplies` / `useMarketBorrows` | Paginated supply/withdraw and borrow/repay activity | Monarch first, then Morpho API, then subgraph | Already Monarch-first; no new Envio schema gap identified |
| `useMarketLiquidations` | Paginated liquidations | Monarch first, then Morpho API, then subgraph | Already Monarch-first; no new Envio schema gap identified |
| `useMonarchTransactions` | Admin stats feed and aggregated flow dashboards | Monarch transactions + `useProcessedMarkets` + `useTokenPrices` | If admin stats should be fully independent, Monarch also needs market registry/metadata and a non-Morpho price source |

#### Vaults And Allocators

| Hook / Family | Responsibility | Infra Today | Full Monarch Support Still Needs |
|---------------|----------------|-------------|----------------------------------|
| `useUserVaultsV2Query` | User vault list with optional balance, TVL, and yield enrichment | Monarch vault metadata + RPC balances/totalAssets + RPC 4626 yield snapshots | Already off Morpho for yield; no new Envio schema gap identified |
| `useVaultV2Data` | Vault detail/settings metadata for a single vault | Monarch vault detail first, narrow RPC fallback if metadata unavailable | Already aligned with Monarch-first design |
| `useAllMorphoVaultsQuery` | Global whitelisted vault registry | Morpho API only | Monarch/public vault registry parity |
| `usePublicAllocatorVaults` | Public allocator config for supplying vaults in a market | Morpho API only | Monarch/public allocator config endpoint parity |
| `useAllocationsQuery` | Live vault `allocation(capId)` values | Pure RPC multicall | No Envio gap |
| `usePublicAllocatorLiveData` | Live flow caps, vault supply, and liquidity for allocator UX | Pure RPC multicall | No Envio gap |
| `useVaultHistoricalApy` / `use4626VaultAPR` | Historical 4626 yield and expected carry calculations | Pure RPC share-price snapshots + RPC Morpho market reads | No Envio gap |

#### RPC Helpers And External Reads

| Hook / Family | Responsibility | Infra Today | Full Monarch Support Still Needs |
|---------------|----------------|-------------|----------------------------------|
| `useCurrentBlocks` / `useBlockTimestamps` / `usePositionSnapshots` / `useFreshMarketsState` / `useHistoricalSupplierPositions` | Block, snapshot, and live-state helpers used by positions/charts | Pure RPC reads via viem/wagmi | No Envio gap |
| `useUserBalancesQuery` | ERC20 wallet balances across chains | Pure RPC multicall via wagmi | No Envio gap |
| `useTokensQuery` | Token metadata lookup for app UI | Local token registry + Pendle assets API | Not part of Monarch migration |
| `useOracleMetadata` / `useAllOracleMetadata` | Oracle classification and feed metadata | Scanner gist JSON | Not part of Monarch migration |
| `useMarketMetricsQuery` | Enhanced market metrics, flows, trending, scores | Monarch metrics API via `/api/monarch/metrics` | Already Monarch-backed |
| `useUserRewardsQuery` | Claimable rewards and distributions | Morpho rewards REST + Merkl API | Outside Monarch/Envio scope today |
| `useMerklCampaignsQuery` / `useMerklHoldIncentivesQuery` | Campaign and HOLD incentive enrichment | Merkl API + hardcoded opportunity mapping | Outside Monarch/Envio scope today |

### Data Flow Patterns

Expand All @@ -218,9 +276,9 @@ Split: allMarkets vs whitelistedMarkets

**Position Data Flow:**
```
1. Fetch market keys from API (which markets user has positions in)
2. Fetch on-chain snapshots per market (usePositionSnapshots)
3. Combine with market metadata
1. Discover market keys via Monarch batched `Position` reads when possible; fall back to Morpho API/Subgraph
2. Fetch on-chain snapshots per market (`usePositionSnapshots`)
3. Combine live balances with market metadata from `useProcessedMarkets`
4. Group by loan asset
5. Calculate earnings
```
Expand Down
7 changes: 7 additions & 0 deletions src/abis/erc4626.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ import type { Abi } from 'viem';
* Keep this small on purpose to avoid importing the giant vault ABI.
*/
export const erc4626Abi = [
{
inputs: [],
name: 'decimals',
outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'asset',
Expand Down
6 changes: 6 additions & 0 deletions src/data-sources/monarch-api/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
export { monarchGraphqlFetcher } from './fetchers';
export {
fetchMonarchUserPositionMarketsForNetworks,
fetchMonarchUserPositionStateForMarket,
type MonarchUserPositionState,
} from './positions';
export { fetchMonarchUserTransactions } from './user-transactions';
export {
fetchMonarchMarketBorrowers,
fetchMonarchMarketBorrows,
Expand Down
113 changes: 113 additions & 0 deletions src/data-sources/monarch-api/positions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { envioUserPositionForMarketQuery, envioUserPositionsPageQuery } from '@/graphql/envio-queries';
import { ALL_SUPPORTED_NETWORKS, type SupportedNetworks } from '@/utils/networks';
import { monarchGraphqlFetcher } from './fetchers';

type PositionMarket = {
marketUniqueKey: string;
chainId: number;
};

type MonarchUserPositionRow = {
marketId: string;
chainId: number;
supplyShares: string;
borrowShares: string;
collateral: string;
};

type MonarchUserPositionsPageResponse = {
data?: {
Position?: MonarchUserPositionRow[];
};
};

export type MonarchUserPositionState = {
supplyShares: string;
borrowShares: string;
collateral: string;
};

const MONARCH_POSITION_MARKETS_PAGE_SIZE = 500;

const isNonZero = (value: string | null | undefined): boolean => {
return value !== null && value !== undefined && value !== '0';
};

export const fetchMonarchUserPositionMarketsForNetworks = async (
userAddress: string,
networks: SupportedNetworks[],
): Promise<PositionMarket[]> => {
if (networks.length === 0) {
return [];
}

const requestedNetworks = new Set(networks);
const supportedNetworks = new Set(ALL_SUPPORTED_NETWORKS);
const positionMarkets = new Map<string, PositionMarket>();
let offset = 0;

while (true) {
const response = await monarchGraphqlFetcher<MonarchUserPositionsPageResponse>(envioUserPositionsPageQuery, {
user: userAddress.toLowerCase(),
chainIds: networks,
limit: MONARCH_POSITION_MARKETS_PAGE_SIZE,
offset,
});

const positions = response.data?.Position ?? [];

for (const position of positions) {
const chainId = position.chainId as SupportedNetworks;
if (!supportedNetworks.has(chainId) || !requestedNetworks.has(chainId)) {
continue;
}

if (!isNonZero(position.supplyShares) && !isNonZero(position.borrowShares) && !isNonZero(position.collateral)) {
continue;
}

const positionMarket = {
marketUniqueKey: position.marketId,
chainId,
};

positionMarkets.set(`${positionMarket.marketUniqueKey.toLowerCase()}-${positionMarket.chainId}`, positionMarket);
}

if (positions.length < MONARCH_POSITION_MARKETS_PAGE_SIZE) {
break;
}

offset += positions.length;
}

return Array.from(positionMarkets.values());
};

export const fetchMonarchUserPositionStateForMarket = async (
marketUniqueKey: string,
userAddress: string,
network: SupportedNetworks,
): Promise<MonarchUserPositionState | null> => {
const response = await monarchGraphqlFetcher<MonarchUserPositionsPageResponse>(envioUserPositionForMarketQuery, {
user: userAddress.toLowerCase(),
chainId: network,
marketId: marketUniqueKey.toLowerCase(),
});

const position = response.data?.Position?.[0];

if (!position) {
return null;
}

if (!isNonZero(position.supplyShares) && !isNonZero(position.borrowShares) && !isNonZero(position.collateral)) {
return null;
}

return {
supplyShares: position.supplyShares,
borrowShares: position.borrowShares,
collateral: position.collateral,
};
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
9 changes: 3 additions & 6 deletions src/data-sources/monarch-api/transactions.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
/**
* Monarch API Transactions
* Monarch admin/time-range transactions.
*
* Fetches Monarch supply and withdraw transactions across all chains through the
* shared Monarch GraphQL endpoint.
*
* Uses separate pagination for supplies and withdraws to ensure complete data.
* Freezes endTimestamp at fetch start to ensure consistent pagination.
* This file is intentionally scoped to the Monarch dashboard feed.
* User-history fetch/normalize logic lives in `user-transactions.ts`.
*/

import { monarchGraphqlFetcher } from './fetchers';
Expand Down
Loading