-
Notifications
You must be signed in to change notification settings - Fork 3
refactor: vault v2 indexer #455
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b5af840
e0e104b
f3a61ab
0d33f95
396092d
0d5e1dd
d8bf702
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,38 @@ | ||
| export const MONARCH_API_ENDPOINT = process.env.MONARCH_API_ENDPOINT; | ||
| export const MONARCH_API_KEY = process.env.MONARCH_API_KEY; | ||
| export const MONARCH_METRICS_API_ENDPOINT = process.env.MONARCH_METRICS_API_ENDPOINT; | ||
| export const MONARCH_METRICS_API_KEY = process.env.MONARCH_METRICS_API_KEY; | ||
| export const MONARCH_METRICS_TIMEOUT_MS = 5_000; | ||
|
|
||
| export const getMonarchUrl = (path: string): URL => { | ||
| if (!MONARCH_API_ENDPOINT) throw new Error('MONARCH_API_ENDPOINT not configured'); | ||
| return new URL(path, MONARCH_API_ENDPOINT.replace(/\/$/, '')); | ||
| const isAbortError = (error: unknown): error is Error => error instanceof Error && error.name === 'AbortError'; | ||
|
|
||
| export const getMonarchRouteFailure = ( | ||
| error: unknown, | ||
| fallbackMessage: string, | ||
| timeoutMessage: string, | ||
| ): { | ||
| message: string; | ||
| status: number; | ||
| } => { | ||
| return isAbortError(error) ? { message: timeoutMessage, status: 504 } : { message: fallbackMessage, status: 500 }; | ||
| }; | ||
|
|
||
| export const fetchMonarchUpstream = async (input: URL | string, timeoutMs: number, init: RequestInit): Promise<Response> => { | ||
| const controller = new AbortController(); | ||
| const timeoutId = setTimeout(() => { | ||
| controller.abort(); | ||
| }, timeoutMs); | ||
|
|
||
| try { | ||
| return await fetch(input, { | ||
| ...init, | ||
| cache: 'no-store', | ||
| signal: controller.signal, | ||
| }); | ||
| } finally { | ||
| clearTimeout(timeoutId); | ||
| } | ||
| }; | ||
|
|
||
| export const getMonarchMetricsUrl = (path: string): URL => { | ||
| if (!MONARCH_METRICS_API_ENDPOINT) throw new Error('MONARCH_METRICS_API_ENDPOINT not configured'); | ||
| return new URL(path, MONARCH_METRICS_API_ENDPOINT.replace(/\/$/, '')); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,7 @@ Monarch is a client-side DeFi dashboard for the Morpho Blue lending protocol. It | |
|
|
||
| **Key Architectural Decisions:** | ||
| - Next.js 15 App Router with React 18 | ||
| - Dual data source strategy: Morpho API (primary) → Subgraph (fallback) | ||
| - Multi-source strategy: Morpho API + Monarch API + selective subgraph fallback | ||
| - Zustand for client state, React Query for server state | ||
| - All user data in localStorage (no backend DB) | ||
| - Multi-chain support with custom RPC override capability | ||
|
|
@@ -148,12 +148,18 @@ MorphoChainlinkOracleData { | |
|
|
||
| ## Data Sources | ||
|
|
||
| ### Dual-Source Strategy | ||
| ### Multi-Source Strategy | ||
|
|
||
| ``` | ||
| Primary: Morpho API (https://blue-api.morpho.org/graphql) | ||
| ↓ (if unavailable or unsupported chain) | ||
| Fallback: Subgraph (The Graph / Goldsky) | ||
| Markets / positions: Morpho API (https://blue-api.morpho.org/graphql) | ||
| ↓ (if unavailable or unsupported chain) | ||
| Subgraph (The Graph / Goldsky) | ||
|
|
||
| Autovault metadata: Monarch GraphQL (https://api.monarchlend.xyz/graphql) | ||
| ↓ (if indexer lag / API failure) | ||
| Narrow on-chain RPC fallback | ||
|
|
||
| Market metrics: Monarch metrics API via `/api/monarch/metrics` | ||
| ``` | ||
|
Comment on lines
153
to
163
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a language to the fenced block at Line 153. Markdownlint Suggested fix-```
+```text
Markets / positions: Morpho API (https://blue-api.morpho.org/graphql)
↓ (if unavailable or unsupported chain)
Subgraph (The Graph / Goldsky)
Autovault metadata: Monarch GraphQL (https://api.monarchlend.xyz/graphql)
↓ (if indexer lag / API failure)
Narrow on-chain RPC fallback
Market metrics: Monarch metrics API via `/api/monarch/metrics`Verify each finding against the current code and only fix it if needed. In |
||
|
|
||
| **Morpho API Supported Chains:** Mainnet, Base, Unichain, Polygon, Arbitrum, HyperEVM, Monad | ||
|
|
@@ -175,7 +181,9 @@ Fallback: Subgraph (The Graph / Goldsky) | |
| | Market state (APY, utilization) | Morpho API | 30s stale | `useMarketData` | | ||
| | User positions | Morpho API + on-chain | 5 min | `useUserPositions` | | ||
| | Vaults list | Morpho API | 5 min | `useAllMorphoVaultsQuery` | | ||
| | Vault allocations | On-chain (Wagmi) | On demand | `useAllocations` | | ||
| | User autovault metadata | Monarch GraphQL + on-chain enrichment | 60s | `useUserVaultsV2Query` | | ||
| | 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` | | ||
| | Merkl rewards | Merkl API | On demand | `useMerklCampaignsQuery` | | ||
|
|
@@ -200,9 +208,11 @@ Split: allMarkets vs whitelistedMarkets | |
|
|
||
| **Vault Data Flow:** | ||
| ``` | ||
| 1. Fetch vault list from API | ||
| 2. Wagmi contract reads for owner, curator, caps | ||
| 3. Historical allocations via subgraph | ||
| 1. Fetch chain-scoped vault metadata from Monarch GraphQL | ||
| 2. Enrich user-specific balances / totalAssets via multicall where needed | ||
| 3. Use narrow RPC fallback only when Monarch vault metadata is unavailable | ||
| 4. Fetch live allocations from on-chain `allocation(capId)` reads | ||
| 5. After vault writes, use shared bounded retry refreshes so Monarch indexing can catch up | ||
| ``` | ||
|
|
||
| --- | ||
|
|
@@ -256,6 +266,7 @@ All hooks in `/src/hooks/queries/` follow React Query patterns: | |
| | `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 | | ||
| | `useMarketLiquidations` | `['marketLiquidations', id, net]` | 5 min | - | Yes | | ||
| | `useUserTransactionsQuery` | `['user-transactions', ...]` | 60s | - | No | | ||
| | `useAllocationsQuery` | `['vault-allocations', ...]` | 30s | - | No | | ||
|
|
@@ -281,6 +292,11 @@ Fallback Strategy: | |
| - `cache: 'no-store'` (disable browser cache) | ||
| - Throws on GraphQL errors (strict) | ||
|
|
||
| **Monarch GraphQL** (`/src/data-sources/monarch-api/fetchers.ts`): | ||
| - Endpoint: `NEXT_PUBLIC_MONARCH_API_NEW` | ||
| - Browser fetch with `NEXT_PUBLIC_MONARCH_API_KEY` | ||
| - Used as the primary read path for autovault V2 metadata | ||
|
|
||
| **Subgraph** (`/src/data-sources/subgraph/fetchers.ts`): | ||
| - Configurable URL per network | ||
| - Logs GraphQL errors but continues (lenient) | ||
|
|
@@ -322,6 +338,8 @@ Fallback Strategy: | |
| | Service | Endpoint | Purpose | | ||
| |---------|----------|---------| | ||
| | Morpho API | `https://blue-api.morpho.org/graphql` | Markets, vaults, positions | | ||
| | Monarch GraphQL | `https://api.monarchlend.xyz/graphql` | Autovault metadata, adapters, caps | | ||
| | Monarch Metrics | `/api/monarch/metrics` → external Monarch metrics API | Market metrics and admin stats | | ||
| | The Graph | Per-chain subgraph URLs | Fallback data, suppliers, borrowers | | ||
| | Merkl API | `https://api.merkl.xyz` | Reward campaigns | | ||
| | Velora API | `https://api.paraswap.io` | Swap quotes and executable tx payloads | | ||
|
|
||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import type { Abi } from 'viem'; | ||
|
|
||
| export const adapterV2FactoryAbi = [ | ||
| { | ||
| inputs: [{ internalType: 'address', name: 'parentVault', type: 'address' }], | ||
| name: 'createMorphoMarketV1AdapterV2', | ||
| outputs: [], | ||
| stateMutability: 'nonpayable', | ||
| type: 'function', | ||
| }, | ||
| ] as const satisfies Abi; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| type GraphQLVariables = Record<string, unknown>; | ||
|
|
||
| type GraphQLError = { | ||
| message?: string; | ||
| }; | ||
|
|
||
| type MonarchGraphqlFetcherOptions = { | ||
| signal?: AbortSignal; | ||
| }; | ||
|
|
||
| const MONARCH_GRAPHQL_API_ENDPOINT = process.env.NEXT_PUBLIC_MONARCH_API_NEW; | ||
| const MONARCH_GRAPHQL_API_KEY = process.env.NEXT_PUBLIC_MONARCH_API_KEY; | ||
|
|
||
| export const monarchGraphqlFetcher = async <T extends Record<string, unknown>>( | ||
| query: string, | ||
| variables: GraphQLVariables = {}, | ||
| options: MonarchGraphqlFetcherOptions = {}, | ||
| ): Promise<T> => { | ||
| if (!MONARCH_GRAPHQL_API_ENDPOINT || !MONARCH_GRAPHQL_API_KEY) { | ||
| throw new Error('Monarch GraphQL client not configured'); | ||
| } | ||
|
|
||
| const response = await fetch(MONARCH_GRAPHQL_API_ENDPOINT, { | ||
| method: 'POST', | ||
| headers: { | ||
| Authorization: `Bearer ${MONARCH_GRAPHQL_API_KEY}`, | ||
| 'Content-Type': 'application/json', | ||
| }, | ||
| body: JSON.stringify({ | ||
| query, | ||
| variables, | ||
| }), | ||
| cache: 'no-store', | ||
| signal: options.signal, | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error(`Monarch API request failed: ${response.status} ${response.statusText}`); | ||
| } | ||
|
|
||
| const result = (await response.json()) as T & { errors?: GraphQLError[] }; | ||
|
|
||
| if (result.errors && result.errors.length > 0) { | ||
| const message = | ||
| result.errors | ||
| .map((error) => error.message) | ||
| .filter(Boolean) | ||
| .join('; ') || 'Unknown Monarch GraphQL error'; | ||
| throw new Error(message); | ||
| } | ||
|
|
||
| return result; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| export { monarchGraphqlFetcher } from './fetchers'; | ||
| export { | ||
| fetchMonarchVaultDetails, | ||
| fetchUserVaultV2DetailsAllNetworks, | ||
| type VaultAdapterDetails, | ||
| type UserVaultV2, | ||
| type VaultV2Cap, | ||
| type VaultV2Details, | ||
| } from './vaults'; |
Uh oh!
There was an error while loading. Please reload this page.