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
38 changes: 38 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ Always consult these docs for detailed information:

---

## Core Philosophy

1. Anchor on the user-critical failure before proposing solutions.
2. Prefer the smallest chokepoint change over broad instrumentation.
3. Avoid scope creep unless it is required to solve the root cause.
4. Do not claim repo facts without evidence (no invented counts).
5. Prevent double-capture, noisy heuristics, or duplicate logic.

---

## 🛠️ Skills System

This project uses **skills** for domain-specific patterns. Agents should load the relevant skill before working on related tasks.
Expand Down Expand Up @@ -68,6 +78,34 @@ The skill injects detailed patterns and conventions into the conversation contex

---

## First-Principles Self-Review (Before Proposing Fixes)

Before proposing a solution, add a short self-review:

1. What user-critical failure are we solving?
2. Is the change scoped to the smallest chokepoint that fixes the root cause?
3. Does it avoid scope creep (new features) unless required to solve the root cause?
4. What evidence in the repo supports the claim? (no invented counts)
5. What is the simplest safe rollout path?
6. What would we NOT do to keep the change auditable and safe?
7. What could cause double-capture, noise, or duplicate logic?

If you cannot answer these briefly, do not propose the change yet.

---

## Plan Gate (Scope Check)

If the proposed work touches more than 2 files, adds a new module, or changes runtime behavior, provide a short plan first and wait for confirmation.

Plan format:
1. Goal
2. Smallest viable change
3. Files touched
4. Risk/rollback note

---

## MANDATORY: Validate After Every Implementation Step

**STOP after each implementation step and validate before moving on.** This is NOT optional. Do NOT batch all validation to the end.
Expand Down
6 changes: 6 additions & 0 deletions app/api/admin/monarch-indexer/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { reportApiRouteError } from '@/utils/sentry-server';

/**
* Proxy API route for Monarch Indexer
Expand Down Expand Up @@ -47,6 +48,11 @@ export async function POST(request: NextRequest) {
const data = await response.json();
return NextResponse.json(data);
} catch (error) {
reportApiRouteError(error, {
route: '/api/admin/monarch-indexer',
method: 'POST',
status: 500,
});
console.error('Monarch indexer proxy error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
Expand Down
6 changes: 6 additions & 0 deletions app/api/monarch/metrics/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type NextRequest, NextResponse } from 'next/server';
import { MONARCH_API_KEY, getMonarchUrl } from '../utils';
import { reportApiRouteError } from '@/utils/sentry-server';

export async function GET(req: NextRequest) {
if (!MONARCH_API_KEY) {
Expand Down Expand Up @@ -29,6 +30,11 @@ export async function GET(req: NextRequest) {

return NextResponse.json(await response.json());
} catch (error) {
reportApiRouteError(error, {
route: '/api/monarch/metrics',
method: 'GET',
status: 500,
});
console.error('[Monarch Metrics API] Failed to fetch:', error);
return NextResponse.json({ error: 'Failed to fetch market metrics' }, { status: 500 });
}
Expand Down
9 changes: 9 additions & 0 deletions app/api/oracle-metadata/[chainId]/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NextResponse } from 'next/server';
import { reportApiRouteError } from '@/utils/sentry-server';

const ORACLE_GIST_BASE_URL = process.env.ORACLE_GIST_BASE_URL;

Expand Down Expand Up @@ -31,6 +32,14 @@ export async function GET(request: Request, { params }: { params: Promise<{ chai

return NextResponse.json(data);
} catch (error) {
reportApiRouteError(error, {
route: '/api/oracle-metadata/[chainId]',
method: 'GET',
status: 500,
extras: {
chainId,
},
});
console.error('Failed to fetch oracle metadata:', error);
return NextResponse.json({ error: 'Failed to fetch oracle metadata' }, { status: 500 });
}
Expand Down
5 changes: 5 additions & 0 deletions docs/TECHNICAL_OVERVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ Monarch is a client-side DeFi dashboard for the Morpho Blue lending protocol. It
| @morpho-org/blue-sdk | 5.3.0 | Morpho Blue protocol SDK |
| @cowprotocol/cow-sdk | 7.2.9 | Intent-based swaps |

**Wagmi v3 integration notes:**
- Prefer `useConnection()` when you need wallet state (`address`, `chainId`, `isConnected`) in one place.
- Avoid introducing `useAccount()` + `useChainId()` pairs for new wallet-state sync logic.
- `useTransactionWithToast` already reports `useSendTransaction` failures to Sentry; global React Query mutation telemetry should not re-capture `sendTransaction` mutation errors.

### State Management
| Technology | Version | Purpose |
|-----------|---------|---------|
Expand Down
26 changes: 25 additions & 1 deletion src/OnchainProviders.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,37 @@
'use client';

import { useEffect } from 'react';
import type { ReactNode } from 'react';
import { WagmiProvider } from 'wagmi';
// biome-ignore lint/performance/noNamespaceImport: Sentry SDK requires namespace import
import * as Sentry from '@sentry/nextjs';
import { useConnection, WagmiProvider } from 'wagmi';
import { wagmiAdapter } from '@/config/appkit';
import { createWagmiConfig } from '@/store/createWagmiConfig';
import { ConnectRedirectProvider } from './components/providers/ConnectRedirectProvider';
import { CustomRpcProvider, useCustomRpcContext } from './components/providers/CustomRpcProvider';

type Props = { children: ReactNode };

function SentryWalletScopeSync() {
const { address, chainId, isConnected } = useConnection();

useEffect(() => {
if (isConnected && address) {
const truncatedAddress = `${address.slice(0, 6)}...${address.slice(-4)}`;
Sentry.setUser({ id: truncatedAddress });
Sentry.setTag('wallet_connected', 'true');
Sentry.setTag('chain_id', String(chainId));
return;
}

Sentry.setUser(null);
Sentry.setTag('wallet_connected', 'false');
Sentry.setTag('chain_id', 'unknown');
}, [address, chainId, isConnected]);

return null;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function WagmiConfigProvider({ children }: Props) {
const { customRpcUrls } = useCustomRpcContext();

Expand All @@ -22,6 +45,7 @@ function WagmiConfigProvider({ children }: Props) {
config={wagmiConfig}
reconnectOnMount
>
<SentryWalletScopeSync />
<ConnectRedirectProvider>{children}</ConnectRedirectProvider>
</WagmiProvider>
);
Expand Down
95 changes: 94 additions & 1 deletion src/components/providers/QueryProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,108 @@
'use client';

import type { ReactNode } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MutationCache, QueryCache, QueryClient, QueryClientProvider, type QueryKey } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { reportHandledError } from '@/utils/sentry';

type QueryProviderProps = {
children: ReactNode;
};

const ACTIONABLE_QUERY_ROOT_KEYS = new Set<string>([
'all-position-snapshots',
'enhanced-positions',
'fresh-markets-state',
'historicalSupplierPositions',
'marketData',
'marketLiquidations',
'market-metrics',
'markets',
'monarch-transactions',
'merkl-campaigns',
'morpho-market-v1-adapters',
'oracle-data',
'oracle-metadata',
'morpho-vaults',
'public-allocator-vaults',
'tokenPrices',
'majorPrices',
'positions',
'tokens',
'user-vaults-v2',
'user-rewards',
'user-transactions',
'vault-historical-apy',
'vault-v2-data',
'vault-allocations',
]);

const TRANSACTION_MUTATION_ROOT_KEYS = new Set<string>([
'sendTransaction',
]);

const getQueryRootKey = (queryKey: QueryKey): string => {
const root = queryKey[0];
return typeof root === 'string' ? root : String(root);
};

// Create a single QueryClient with merged configuration from both previous clients
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
// Skip background refetch failures when stale data is already available.
// These usually do not break UX and create noisy events.
if (query.state.data !== undefined) {
return;
}

const rootKey = getQueryRootKey(query.queryKey);
if (!ACTIONABLE_QUERY_ROOT_KEYS.has(rootKey)) {
return;
}

reportHandledError(error, {
scope: 'react_query',
operation: `query:${rootKey}`,
level: 'error',
tags: {
query_root_key: rootKey,
query_failure_count: query.state.fetchFailureCount,
},
extras: {
query_key: query.queryKey,
query_failure_reason:
query.state.fetchFailureReason instanceof Error
? query.state.fetchFailureReason.message
: String(query.state.fetchFailureReason),
},
});
},
}),
mutationCache: new MutationCache({
onError: (error, _variables, _context, mutation) => {
const mutationKey = mutation.options.mutationKey;
const rootKey = Array.isArray(mutationKey) ? getQueryRootKey(mutationKey) : 'unknown';

// Transaction mutation failures are reported in `useTransactionWithToast`.
// Skip them here to avoid duplicate telemetry events.
if (TRANSACTION_MUTATION_ROOT_KEYS.has(rootKey)) {
return;
}

reportHandledError(error, {
scope: 'react_query_mutation',
operation: `mutation:${rootKey}`,
level: 'error',
tags: {
mutation_root_key: rootKey,
},
extras: {
mutation_key: mutationKey ?? null,
},
});
},
}),
defaultOptions: {
queries: {
// From ClientProviders - good for caching and UX
Expand Down
11 changes: 11 additions & 0 deletions src/hooks/queries/useUserRewardsQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useCallback } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import type { Address } from 'viem';
import { merklClient } from '@/utils/merklApi';
import { reportHandledError } from '@/utils/sentry';
import type { RewardResponseType } from '@/utils/types';
import { URLS } from '@/utils/urls';
import { ALL_SUPPORTED_NETWORKS } from '@/utils/networks';
Expand Down Expand Up @@ -152,10 +153,20 @@ async function fetchAllRewards(userAddress: string): Promise<UserRewardsData> {
const [morphoData, merklData] = await Promise.all([
fetchMorphoRewards(userAddress).catch((err) => {
console.error('Morpho rewards fetch failed:', err);
reportHandledError(err, {
scope: 'user_rewards',
operation: 'fetch:morpho',
level: 'warning',
});
return { rewards: [] as RewardResponseType[], distributions: [] as DistributionResponseType[] };
}),
fetchMerklRewards(userAddress).catch((err) => {
console.error('Merkl rewards fetch failed:', err);
reportHandledError(err, {
scope: 'user_rewards',
operation: 'fetch:merkl',
level: 'warning',
});
return { rewards: [] as RewardResponseType[], rewardsWithProofs: [] as MerklRewardWithProofs[] };
}),
]);
Expand Down
Loading