diff --git a/.claude/README.md b/.claude/README.md new file mode 100644 index 00000000..bf103b33 --- /dev/null +++ b/.claude/README.md @@ -0,0 +1,78 @@ +# `.claude/` — agent infrastructure map + +This directory configures Claude Code for the Fast Protocol App. Nothing here is load-bearing for the app itself — it's a retrieval and attention-budget optimization layer. + +## Layout + +``` +.claude/ +├── README.md # You are here +├── settings.json # Hooks + permission allow/deny lists +├── skills/ # Domain skills (progressive disclosure) +│ ├── skill-creator/ +│ ├── next-app-router/ +│ ├── defi-swap/ +│ ├── web3-wallet/ +│ ├── dashboard-data/ +│ ├── leaderboard-miles/ +│ ├── contract-abis/ +│ ├── ui-shadcn/ +│ └── testing-vitest/ +├── agents/ # Subagent definitions (context firewalls) +│ ├── explore-web3.md +│ ├── security-reviewer.md +│ ├── ui-verifier.md +│ └── abi-tracer.md +├── commands/ # Slash commands +│ ├── prime.md +│ ├── verify.md +│ ├── typecheck.md +│ ├── lint.md +│ ├── test.md +│ ├── new-skill.md +│ └── review-diff.md +└── hooks/ # Shell scripts invoked by settings.json hooks + ├── post-edit-typecheck.sh + └── stop-format-check.sh +``` + +## Design principles (from the research corpus) + +1. **Progressive disclosure.** SKILL.md metadata is always loaded; body loads when triggered; reference files load when explicitly needed. +2. **Silent success, loud failure.** Hooks print nothing when clean. +3. **Pointers over copies.** All content cites `src/path/file.ts:42`; nothing is inlined. +4. **Subagents as firewalls.** Reading many files happens in a subagent, not the parent context. +5. **Verification is a first-class primitive.** `/verify` is the single most important command here. + +## Doc layer convention — one source of truth per topic + +To prevent drift, the three doc surfaces have **non-overlapping roles**: + +| Layer | Audience | Role | Loads | +|---|---|---|---| +| `.claude/skills/` | agents | HOW-TO — patterns, do/don't, code snippets | on relevance | +| `agent_docs/` | agents | MAP — pointers to code + cross-links to skills | on demand | +| `docs/` | humans | NARRATIVE — UX behavior, SQL reference, product flow | banner-gated | + +Rules: +- Each topic has exactly one authoritative home. Others link, they don't duplicate. +- `docs/*.md` files carry an "Audience: humans" banner at the top pointing to the authoritative skill or agent_doc. +- Moving a skill or splitting a module → update `agent_docs/architecture.md` + first (it's the map), then skill cross-links, then any `docs/` pointers. + +## Editing + +- Adding a skill → use `/new-skill `, which invokes `skill-creator`. +- Adding a hook → edit `settings.json` + drop a script in `hooks/`; keep it silent-on-success. +- Adding a subagent → drop a new `.md` under `agents/` with `name`/`description`/`tools`/`model` frontmatter. +- Adding a command → drop a new `.md` under `commands/`; body is the prompt. + +Changes here should be reviewed for **budget impact** — every added skill/tool consumes system-prompt tokens for every future session. + +## Local overrides + +`.claude/settings.local.json` is gitignored. Use it for personal preferences (extra permissions, model overrides). Do not put team-shared config there. + +## Plugins & MCP servers + +MCP servers are user-level, not repo-level. This repo does not require any. If you connect one for work here (e.g., Linear), prune unused tools — tool descriptions are a tax on every turn. See the "instruction budget" section of the plan at `/Users/jasonschwarz/.claude/plans/inherited-herding-penguin.md`. diff --git a/.claude/agents/abi-tracer.md b/.claude/agents/abi-tracer.md new file mode 100644 index 00000000..b70b677f --- /dev/null +++ b/.claude/agents/abi-tracer.md @@ -0,0 +1,48 @@ +--- +name: abi-tracer +description: Given a contract name, ABI function, or address, return every call site across the app grouped by layer (hook / component / server / lib). Read-only research agent for contract refactors and ABI upgrades. +tools: Read, Grep, Glob +model: sonnet +--- + +You are a read-only code-graph agent specialized in contract / ABI tracing. + +## Goal + +Given a query like "find all call sites for `Settlement.deposit`" or "where is the Permit2 address used", return a grouped list of call sites. + +## Workflow + +1. Start at the ABI binding (e.g., `src/lib/fast-settlement-v3-abi.ts`, `src/lib/weth-abi.ts`, `src/lib/erc20-abi.ts`, `src/lib/contract-config.tsx`). +2. `Grep` for the function name or address token across `src/`. +3. Group results by layer: + - **Hooks** (`src/hooks/`) + - **Components** (`src/components/`) + - **Server routes / actions** (`src/app/api/`, `src/actions/`) + - **Lib helpers** (`src/lib/`) +4. For each call site, note `file:line` and the enclosing function name. + +## Output + +``` +Contract: +Function: +Address config: src/lib/contract-config.tsx: + +Call sites: + hooks: + src/hooks/use-foo.ts:42 (inside useFoo → useWriteContract) + ... + components: + ... + server: + ... + lib: + ... +``` + +## Rules + +- Do not open files you don't need. Grep first. +- If the function name is common (e.g., `deposit`), also check import paths to filter out same-named functions in other ABIs. +- Note ambiguity explicitly — if `deposit` appears in both `WETH_ABI` and `SomethingElseABI`, split them. diff --git a/.claude/agents/explore-web3.md b/.claude/agents/explore-web3.md new file mode 100644 index 00000000..5a2ef1ff --- /dev/null +++ b/.claude/agents/explore-web3.md @@ -0,0 +1,33 @@ +--- +name: explore-web3 +description: Research-only subagent for tracing web3 call sites, hook composition, or contract usage across the codebase. Delegate here when the answer requires reading many files and you want to keep the parent context clean. +tools: Read, Grep, Glob, Bash +model: sonnet +--- + +You are a read-only research agent scoped to this Next.js + wagmi + viem repo. You return **condensed findings with `file:line` citations** — never raw dumps. + +## Goal +Answer the parent agent's question about how web3 state, contracts, or hooks are wired in this codebase. + +## Rules +- Read-only. You may not Edit, Write, or run destructive Bash. +- Prefer `Grep` / `Glob` over reading whole files. Only open a file when you need the exact lines. +- When you do read a file, note only the relevant lines and cite them. +- Cap your investigation at ~15 files read. If you need more, return what you have and note the remaining scope. + +## Output format +Return a structured summary: + +1. **TL;DR** — one sentence answering the question. +2. **Key files** — bulleted list of `path:line` with a one-line note each. +3. **Call graph** (if relevant) — A → B → C with paths. +4. **Open questions** — anything you can't answer from the code alone. + +Do not paste code bodies. Do not summarize things the parent can look up in `agent_docs/architecture.md`. + +## Typical tasks +- "Find every call site for ``" +- "Trace the swap flow from input to on-chain submission" +- "List all hooks that invalidate query key X" +- "Find where env var Y is consumed" diff --git a/.claude/agents/security-reviewer.md b/.claude/agents/security-reviewer.md new file mode 100644 index 00000000..273f0eab --- /dev/null +++ b/.claude/agents/security-reviewer.md @@ -0,0 +1,55 @@ +--- +name: security-reviewer +description: Security-focused reviewer for web3 + Next.js changes in this repo. Delegate when reviewing a diff, PR, or set of files for security issues. Reviews are read-only; no code edits. +tools: Read, Grep, Glob, Bash +model: opus +--- + +You are a senior security engineer reviewing code in a Next.js 15 + wagmi/viem DeFi application. You do not write or edit code — you produce a review. + +## Focus areas (in priority order) + +1. **Secrets leakage** + - Server env vars (`env.*` via `@/env/server`) reaching a `"use client"` file + - `NEXT_PUBLIC_` vars holding anything that looks like a real secret + - `console.log` / analytics / error reporting carrying signed payloads, private keys, seed phrases, auth tokens + - Raw API keys in commits + +2. **Signing and transaction safety** + - Permit2: deadline presence, nonce freshness, spender validation, amount bounds + - Slippage bounds respected in swap flow (see `src/lib/swap-constants.ts`, `quote-guard.ts`) + - No "unlimited" token approvals + - Tx errors normalized via `src/lib/transaction-errors.ts` — no raw RPC errors surfaced + +3. **Server-side** + - Server actions and API routes validate input with Zod before side effects + - No `process.env` reads (only `@/env/server`) + - Cron routes gated with a bearer token + - No SSRF-inviting fetches (user-supplied URLs passed to `fetch` server-side) + - No SQL injection via `pg` (prepared statements / parameterized queries) + +4. **Client-side** + - No `dangerouslySetInnerHTML` without sanitization + - No reflected URL params rendered without escaping + - No postMessage / window.ethereum direct access + - localStorage not holding secrets + +5. **Dependency surface** + - New deps added? Check they're reputable, pinned, and not duplicating existing functionality. + +## Output + +Return a bulleted list grouped by severity: + +- **High** (fix before merge) +- **Medium** (fix soon) +- **Low / nit** (worth noting) +- **Out of scope / clear** (explicit all-clears are useful for reviewer confidence) + +Each finding: `path:line` + one-sentence description + suggested fix. + +## Rules + +- Do not edit code. Your output is the review. +- Do not re-verify things handled elsewhere (lint, typecheck) unless you find a specific gap. +- Be explicit about what you did **not** check. diff --git a/.claude/agents/ui-verifier.md b/.claude/agents/ui-verifier.md new file mode 100644 index 00000000..8d058fdc --- /dev/null +++ b/.claude/agents/ui-verifier.md @@ -0,0 +1,30 @@ +--- +name: ui-verifier +description: Boots the dev server and verifies UI changes by loading routes, optionally taking screenshots or driving the page. Use when a UI change needs visual confirmation beyond typecheck/lint/tests. +tools: Read, Bash, Grep, Glob +model: sonnet +--- + +You are a UI verification agent. Your job is to prove that a rendered page matches intent — not to edit code. + +## Workflow + +1. Ensure nothing else is on port 3000: `lsof -i :3000 || true`. +2. Start the dev server in the background: `npm run dev &` (or use existing Bash background feature). +3. Poll `curl -sf http://localhost:3000/` until HTTP 200 (or fail after ~60s). +4. For each route under review, `curl http://localhost:3000/` and check for expected markers in the HTML. +5. If the Claude-in-Chrome extension is available, drive the page and report visual state. +6. Stop the dev server when done (`kill %1` or equivalent). + +## Output + +- **Routes checked** — list with status (200 / 404 / 500) and any content-match result. +- **Observations** — anything surprising (missing provider, hydration warning, console error). +- **Can't verify** — explicit list of things you couldn't check (no browser, no auth, etc.). + +## Rules + +- Do not edit code. If you find a bug, report it — the parent fixes. +- Do not leave the dev server running. Clean up before returning. +- Do not fabricate screenshots. If the tooling isn't available, say so. +- Treat console errors as signal, not noise — include them in the report. diff --git a/.claude/commands/lint.md b/.claude/commands/lint.md new file mode 100644 index 00000000..71d1f0aa --- /dev/null +++ b/.claude/commands/lint.md @@ -0,0 +1,11 @@ +--- +description: Run ESLint (next lint). +--- + +Run: + +```bash +npm run lint +``` + +On success, print "lint: ok". On failure, list the rule + file + line for each violation. `eslint-plugin-only-warn` means most violations print as warnings — still surface them. diff --git a/.claude/commands/new-skill.md b/.claude/commands/new-skill.md new file mode 100644 index 00000000..ef414a17 --- /dev/null +++ b/.claude/commands/new-skill.md @@ -0,0 +1,16 @@ +--- +description: Scaffold a new skill in .claude/skills/ following the skill-creator conventions. Pass the skill name as an argument (kebab-case). +--- + +Scaffold a new skill named `$ARGUMENTS`. + +1. Load `.claude/skills/skill-creator/SKILL.md` and its `anatomy.md` / `checklist.md`. +2. Create `.claude/skills/$ARGUMENTS/SKILL.md` with the frontmatter template, with `name` matching the directory name and a **placeholder** description the user must fill in. +3. Leave the body as a template using the section order from `anatomy.md`. +4. Print the checklist from `.claude/skills/skill-creator/checklist.md` and ask the user for: + - the real `description` (the trigger) + - the key files this skill should point at + - any sibling reference files to scaffold +5. Do not commit. The user reviews and edits before merging. + +If the directory already exists, stop and ask the user whether to replace or edit. diff --git a/.claude/commands/prime.md b/.claude/commands/prime.md new file mode 100644 index 00000000..88512e4a --- /dev/null +++ b/.claude/commands/prime.md @@ -0,0 +1,28 @@ +--- +description: Prime a fresh session with the project mental model. Syncs external workspaces, then reads the orientation files deterministically. +--- + +First, sync every external workspace declared in `.claude/externals.json`. +Run: + +``` +.claude/hooks/externals-sync.sh +``` + +Surface the script's stdout to the user exactly as-is — it reports per-external state (current SHA, commits fast-forwarded, age vs freshness threshold). If it exits non-zero, stop and surface the message; don't attempt to resolve `.external/` state yourself. + +Then read these files in order and summarize back to the user in 5 bullets what this project is, what matters, and how to verify work: + +1. `CLAUDE.md` +2. `AGENTS.md` +3. `agent_docs/stack.md` +4. `agent_docs/architecture.md` +5. `agent_docs/verification.md` + +Do **not** read skills, agent definitions, or the `docs/` folder — those are Tier-2/3 and load on demand. + +Then list the available slash commands (`/verify`, `/typecheck`, `/lint`, `/test`, `/new-skill`, `/review-diff`, `/sync-externals`) and skills by name (from `.claude/skills/`). Do not load their bodies. + +If `.claude/externals.json` declares any externals, also list them by name under an "External workspaces:" heading with their current short SHA and age from the sync output above. + +End with: "Ready. What's the task?" diff --git a/.claude/commands/realign.md b/.claude/commands/realign.md new file mode 100644 index 00000000..4404b79c --- /dev/null +++ b/.claude/commands/realign.md @@ -0,0 +1,92 @@ +--- +description: Audit incoming main changes against the agentic-repo patterns and fix drift (stale imports, missing Zod, missing doc entries, etc.). Runs the merging-main skill's playbook. +--- + +Run the alignment pass after merging / rebasing main into this branch. The +full playbook is at `.claude/skills/merging-main/SKILL.md` — load it before +starting. + +## Steps + +1. **Capture what came in.** + + ```bash + git fetch origin main + git log $(git merge-base HEAD origin/main)..origin/main --oneline + ``` + + Read the commit subjects. Every new route / component / hook / + lib-file needs to be traced through the alignment checklist. + +2. **Stale lib imports — handled by ESLint.** The folderization rename + table is codified in `eslint.config.js` as a `no-restricted-imports` + rule. Run `npm run lint` and look for any `no-restricted-imports` + warning — the message includes the new path to move the import to. + + If you're adding a new rename (e.g. you split another top-level + `src/lib/*.ts` into a subfolder), update both: + 1. The `no-restricted-imports` block in `eslint.config.js`. + 2. The rename table in `.claude/skills/merging-main/SKILL.md`. + +3. **ESLint on new API routes.** The Zod-validation rule is scoped to + `src/app/api/**/route.ts`. A main PR that preceded that rule can + reintroduce imperative validation. + + ```bash + npx next lint --dir src/app/api 2>&1 | grep -B 2 "no-restricted-syntax" || echo "clean" + ``` + + Any hit → migrate that route using `.claude/skills/next-app-router/api-routes.md`. + +4. **TypeScript strictness drift.** + + ```bash + npx tsc --noEmit + git diff $(git merge-base HEAD origin/main)..HEAD -- src/ | grep -E "^\+.*: any\b|^\+.*as any\b|^\+.*@ts-ignore" | head + ``` + + The second command finds new explicit `any` / `@ts-ignore` added since + the merge base. Fix each with a real type (cast through `unknown` + + interface if the source really is untyped). + +5. **Doc indexes.** For every new directory main added under `src/app/` + or `src/components/`, update `agent_docs/architecture.md`. For every + new API route, update `src/app/api/README.md`. For every new hook, + update `src/hooks/README.md`. Also REMOVE entries for routes main + deleted (the skill has a concrete example from PR #109). + +6. **Full verify.** + + ```bash + npm run typecheck + npm run lint + npm run test:run + npm run format:check + ``` + + (`/verify` runs these in sequence.) + +7. **UI smoke** on any route main touched. `/verify-ui` boots the dev + server and curls the critical pages. + +## Output + +Report back to the user: + +- **Incoming commits** from main (short-hashes + subjects). +- **Drift caught** — each hit from steps 2–4, with file:line and the fix. +- **Docs updated** — which index files got edits, and what was added or + removed. +- **Verify result** — each step from #6 pass/fail. +- **Outstanding** — anything you flagged but didn't fix (e.g. a main-side + `any` you couldn't retype without more context; a skipped test seed). + +## Rules + +- Don't modify main's new code for taste or style — only fix **concrete + alignment breaks**: broken imports, ESLint rule violations, `any` that + wasn't there before, missing index entries. +- Don't `git push --force`. This branch is shared; rebases become merges. +- Don't silence the ESLint rule; migrate the route. +- Don't commit yet — return the report and wait for the user to review + before the merge commit is finalized. diff --git a/.claude/commands/review-diff.md b/.claude/commands/review-diff.md new file mode 100644 index 00000000..ddf17441 --- /dev/null +++ b/.claude/commands/review-diff.md @@ -0,0 +1,17 @@ +--- +description: Review the current branch's diff against main using the security-reviewer subagent. Read-only. +--- + +1. Show a brief summary of what's changed: + ```bash + git fetch origin main --quiet + git diff --stat origin/main...HEAD + ``` + +2. Invoke the `security-reviewer` subagent with this prompt: + + > Review the diff of the current branch (`git diff origin/main...HEAD`) for security issues. Focus on the priority list in `.claude/agents/security-reviewer.md`. Return findings grouped by severity with `file:line` citations. + +3. Relay the subagent's findings to the user verbatim (do not re-summarize). + +4. Do not edit any code. This command is read-only review. diff --git a/.claude/commands/sync-externals.md b/.claude/commands/sync-externals.md new file mode 100644 index 00000000..d1cf99f2 --- /dev/null +++ b/.claude/commands/sync-externals.md @@ -0,0 +1,15 @@ +--- +description: Refresh every external workspace (fetch + fast-forward) without re-reading orientation docs. Use mid-session when a PR lands upstream. +--- + +Run: + +``` +.claude/hooks/externals-sync.sh +``` + +Surface the output to the user exactly as-is. The script is strict: it fast-forwards only clean clones, refuses to auto-resolve divergence, and writes `.external/.manifest.lock.json` with the resulting state. + +If anything failed (non-zero exit), surface the error. Do **not** attempt to `git reset --hard` or otherwise repair `.external/` state on your own — if a mirror genuinely diverged, the user should `rm -rf .external/` and re-run. + +Do **not** re-read orientation docs. This command is scoped to externals only — `/prime` is the full session-start orientation. diff --git a/.claude/commands/test.md b/.claude/commands/test.md new file mode 100644 index 00000000..2d111d0e --- /dev/null +++ b/.claude/commands/test.md @@ -0,0 +1,15 @@ +--- +description: Run the Vitest suite in single-pass mode. Never use watch mode from an agent. +--- + +Run: + +```bash +npm run test:run +``` + +On success, print "tests: ok" plus the pass count. On failure, print only the failing test names + the error messages (not full stack traces — those flood context). + +If the user asks for a specific file, append it: `npm run test:run -- `. + +Do **not** run `npm run test` (watch mode) — it never exits. diff --git a/.claude/commands/typecheck.md b/.claude/commands/typecheck.md new file mode 100644 index 00000000..e45c3ab9 --- /dev/null +++ b/.claude/commands/typecheck.md @@ -0,0 +1,11 @@ +--- +description: Run TypeScript typecheck (tsc --noEmit). +--- + +Run: + +```bash +npm run typecheck +``` + +On success, print "typecheck: ok". On failure, print the compiler errors and stop — do not attempt fixes without user direction. diff --git a/.claude/commands/verify-ui.md b/.claude/commands/verify-ui.md new file mode 100644 index 00000000..a0e96a2a --- /dev/null +++ b/.claude/commands/verify-ui.md @@ -0,0 +1,58 @@ +--- +description: Boot the dev server and smoke-load the critical routes via the ui-verifier subagent. +--- + +Delegate to the `ui-verifier` subagent to boot the Next dev server, curl +the three load-bearing routes, and report any non-200 status or visible +content regression. + +The subagent contract is in `.claude/agents/ui-verifier.md` — it starts +`npm run dev`, waits for port 3000 to return 200, then inspects each +target route. It must clean up the server before returning. + +## Target routes + +``` +/ — landing / gate (AnimatedBackground + Swap heading) +/dashboard — personal miles page (tabs: My Miles, etc.) +/leaderboard — the LeaderboardTable route (Miles / Volume / Stats) +/claim — Genesis SBT claim flow (SSR must 200 even without wallet) +``` + +Note: `/leaderboard` is the heavy refactor target (2711 → 447 LoC parent), +not `/dashboard`. Both are worth smoking because the header + providers +load on every authenticated route. + +For each route, the subagent should verify: +- HTTP 200 +- No console errors in the SSR response that would indicate a crashed + provider (look for "Cannot read", "TypeError", stack traces in HTML). + Note: Node Streams polyfill source bundled into pages includes the + string "TypeError" in static error-class definitions — distinguish + from real "Application error" markers. +- A content marker proving the right template rendered + ("LEADERBOARD" on /leaderboard, "Genesis" on /claim, Swap heading on /). + +## Known operational gotchas + +- A stale `.next/` directory from a prior branch can 500 every route on + boot with "Cannot find module './NNNN.js'" — the dev server builds + per-branch. If this happens, `rm -rf .next` and restart. +- Without `ANALYTICS_DB_AUTH_TOKEN` set, analytics SSR fetches return + 401; pages are expected to render skeleton/empty states (not crash). + +## What this command is NOT + +This isn't a visual-regression tool. It catches provider crashes, +missing env vars, and blank-page hydration failures — not pixel-level +drift. For visual parity after a refactor, the human has to load the +route in a browser. + +## Usage + +``` +/verify-ui +``` + +The subagent returns a structured summary; forward it verbatim to the +user and wait for instruction before making code changes. diff --git a/.claude/commands/verify.md b/.claude/commands/verify.md new file mode 100644 index 00000000..7e59258a --- /dev/null +++ b/.claude/commands/verify.md @@ -0,0 +1,18 @@ +--- +description: Run the full verification stack — typecheck, lint, tests, format — and report results. +--- + +Run these in order. Stop on the first failure and print its output. On success of all four, print "verify: ok". + +```bash +npm run typecheck +npm run lint +npm run test:run +npm run format:check +``` + +If any step fails, do not attempt to fix it inline — report the failure and wait for instruction. + +`format:check` runs last because it's cheapest and most noise-prone; putting it first would hide typecheck failures behind prettier diff noise. + +Do not run `npm run build` unless the user explicitly asks. It's slow and the path-gated CI `build` workflow plus the other checks here usually catch regressions. diff --git a/.claude/externals.json b/.claude/externals.json new file mode 100644 index 00000000..3d7629a3 --- /dev/null +++ b/.claude/externals.json @@ -0,0 +1,27 @@ +{ + "$comment": "Declarative manifest of upstream repos vendored read-only for agent context. Shape is documented in agent_docs/external-workspaces-plan.md. /prime and /sync-externals materialize these under .external/ (gitignored). Never commit entries under .external/; edits there would be lost on the next sync.", + "externals": [ + { + "name": "mev-commit", + "origin": "https://github.com/primev/mev-commit.git", + "ref": "main", + "sparse": [ + "/contracts/", + "/contracts-abi/", + "/tools/preconf-rpc/fastswap/", + "/tools/preconf-rpc/service/", + "/tools/preconf-rpc/handlers/", + "/tools/preconf-rpc/store/", + "/tools/preconf-rpc/sender/", + "/tools/preconf-rpc/rpcserver/", + "/tools/preconf-rpc/points/", + "/tools/fastswap-miles/" + ], + "freshness": { + "warnAfterDays": 7, + "failAfterDays": 45 + }, + "purpose": "Upstream protocol source of truth — FastSwap HTTP handler, preconf RPC, miles/Fuul indexer, canonical ABIs. Scope map: agent_docs/external-mev-commit.md" + } + ] +} diff --git a/.claude/hooks/externals-sync.sh b/.claude/hooks/externals-sync.sh new file mode 100755 index 00000000..1cf1e282 --- /dev/null +++ b/.claude/hooks/externals-sync.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +# externals-sync.sh — materialize or refresh every entry in +# .claude/externals.json under .external//. +# +# Invoked by /prime and /sync-externals. Agents should never call git +# operations on .external/ directly — this script is the one place that +# logic lives. Strict semantics: +# +# - Clone (sparse if patterns given) when .external// is absent. +# - Fetch + fast-forward when present and the local HEAD is a strict +# ancestor of upstream . Prints "advanced N commits". +# - Refuse to proceed if the working tree has uncommitted changes or +# HEAD has diverged from upstream. Agents must stop and surface +# the condition; we never auto-resolve inside a vendored mirror. +# +# Output: one line per external summarizing state, age, and fetched +# delta. Machine-parseable if you pipe through grep. Exits 0 when +# every external is either up-to-date or cleanly fast-forwarded; +# exits non-zero on the first hard error. + +set -u +set -o pipefail + +repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +cd "$repo_root" || exit 1 + +manifest="$repo_root/.claude/externals.json" +ext_dir="$repo_root/.external" +lock="$ext_dir/.manifest.lock.json" + +if [[ ! -f "$manifest" ]]; then + echo "externals-sync: no manifest at $manifest — skipping." + exit 0 +fi + +if ! command -v jq >/dev/null 2>&1; then + echo "externals-sync: jq is required. Install with 'brew install jq'." >&2 + exit 1 +fi + +mkdir -p "$ext_dir" + +# Parse each external as a single-line JSON blob for iteration. +externals="$(jq -c '.externals[]' "$manifest")" +if [[ -z "$externals" ]]; then + echo "externals-sync: manifest has an empty externals array — nothing to do." + exit 0 +fi + +# Accumulator for lock file. We rebuild it fresh each run so removed +# externals don't linger as stale entries. +lock_entries="" + +rc=0 +while IFS= read -r entry; do + name="$(jq -r '.name' <<<"$entry")" + origin="$(jq -r '.origin' <<<"$entry")" + ref="$(jq -r '.ref' <<<"$entry")" + pin="$(jq -r '.pin // empty' <<<"$entry")" + warn_after="$(jq -r '.freshness.warnAfterDays // 0' <<<"$entry")" + sparse_count="$(jq -r '.sparse // [] | length' <<<"$entry")" + + target="$ext_dir/$name" + status="" + sha="" + age_line="" + + if [[ ! -d "$target/.git" ]]; then + # First-time clone. Use --filter=blob:none + sparse-checkout when + # the manifest gave us sparse patterns so we don't pay for unused + # file contents. + echo "externals-sync[$name]: cloning $origin ..." + if [[ "$sparse_count" -gt 0 ]]; then + git clone --filter=blob:none --no-checkout "$origin" "$target" >/dev/null 2>&1 || { + echo "externals-sync[$name]: clone failed." >&2 + rc=1 + continue + } + git -C "$target" sparse-checkout init --cone >/dev/null 2>&1 + # Cone mode wants directory paths WITHOUT leading/trailing slashes + # ("tools/preconf-rpc/fastswap"), but the manifest writes them in + # gitignore-style ("/tools/preconf-rpc/fastswap/") for readability. + # Normalize on the way through. + mapfile -t patterns < <(jq -r '.sparse[]' <<<"$entry" | sed -E 's|^/||; s|/$||') + git -C "$target" sparse-checkout set "${patterns[@]}" >/dev/null 2>&1 + else + git clone "$origin" "$target" >/dev/null 2>&1 || { + echo "externals-sync[$name]: clone failed." >&2 + rc=1 + continue + } + fi + # Check out the pin if set, otherwise the ref. + checkout_target="${pin:-$ref}" + git -C "$target" checkout --quiet "$checkout_target" 2>/dev/null || git -C "$target" checkout --quiet "origin/$ref" 2>/dev/null + status="cloned" + else + # Existing clone. Refuse to proceed if there's local state — + # never auto-resolve inside .external/. + if ! git -C "$target" diff --quiet HEAD 2>/dev/null; then + echo "externals-sync[$name]: ✗ working tree has uncommitted changes. Resolve or wipe .external/$name and re-run." >&2 + rc=1 + continue + fi + + # Fetch the tracked ref with no side effects other than updating + # remote-tracking refs. + if ! git -C "$target" fetch --quiet --no-tags origin "$ref" 2>/dev/null; then + echo "externals-sync[$name]: ✗ fetch failed — check network / repo access." >&2 + rc=1 + continue + fi + + local_sha="$(git -C "$target" rev-parse HEAD)" + remote_sha="$(git -C "$target" rev-parse "origin/$ref")" + + if [[ -n "$pin" ]]; then + # Pinned mode — hard-fail on drift. + if [[ "$local_sha" != "$pin" ]]; then + echo "externals-sync[$name]: ✗ pin drift. Manifest wants $pin but local is $local_sha." >&2 + rc=1 + continue + fi + status="pinned" + elif [[ "$local_sha" == "$remote_sha" ]]; then + status="up-to-date" + elif git -C "$target" merge-base --is-ancestor "$local_sha" "$remote_sha" 2>/dev/null; then + # Strict fast-forward only. + advanced="$(git -C "$target" rev-list --count "$local_sha..$remote_sha")" + git -C "$target" reset --hard --quiet "$remote_sha" + status="fast-forwarded +$advanced" + else + echo "externals-sync[$name]: ✗ local HEAD ($local_sha) has diverged from origin/$ref ($remote_sha). Wipe .external/$name and re-run." >&2 + rc=1 + continue + fi + fi + + sha="$(git -C "$target" rev-parse --short HEAD)" + # Age of the current commit (how old is what we're pinned to). + committer_ts="$(git -C "$target" log -1 --format=%ct HEAD 2>/dev/null || echo 0)" + now="$(date +%s)" + age_days=$(( (now - committer_ts) / 86400 )) + age_line="$age_days days old" + if (( warn_after > 0 )) && (( age_days > warn_after )); then + age_line="$age_line (⚠ past $warn_after-day threshold)" + fi + + printf 'externals-sync[%s]: %s @ %s — %s\n' "$name" "$status" "$sha" "$age_line" + + lock_entries+="$(jq -n \ + --arg name "$name" \ + --arg sha "$(git -C "$target" rev-parse HEAD)" \ + --arg ref "$ref" \ + --arg fetchedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + --argjson ageDays "$age_days" \ + '{name: $name, sha: $sha, ref: $ref, fetchedAt: $fetchedAt, ageDays: $ageDays}')," +done <<<"$externals" + +# Write the lock file (trim trailing comma, wrap as array). +printf '{\n "generatedAt": "%s",\n "externals": [%s]\n}\n' \ + "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + "${lock_entries%,}" >"$lock" + +exit "$rc" diff --git a/.claude/hooks/post-edit-build.sh b/.claude/hooks/post-edit-build.sh new file mode 100755 index 00000000..5d39ac66 --- /dev/null +++ b/.claude/hooks/post-edit-build.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# PostToolUse hook: when an edit lands on a file that influences the Next.js +# build boundary, run `next build` so server/client boundary mistakes, +# env-var typos, and API-route signature breaks are caught immediately. +# +# Why only these paths: build is slow (~30-60s), so we gate it to the files +# where typecheck alone cannot prove correctness — API routes, middleware, +# next.config, t3-oss env schemas, and server actions. +# +# Why skip on missing .env: local worktrees and fresh clones frequently lack +# .env values. The t3-oss validator throws "Invalid environment variables" +# before any code compiles. That's a local-setup issue, not a code defect, +# so we degrade to a notice instead of blocking the edit. +# +# Silent on success. Exits 2 on real build failure so Claude sees the error. + +set -u +INPUT="$(cat || true)" + +path="" +if command -v jq >/dev/null 2>&1; then + path="$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null || true)" +fi + +# Only act when we can see a file path. If jq is absent or path missing, skip. +[[ -z "$path" ]] && exit 0 + +# Gate to build-sensitive paths. +case "$path" in + */src/app/api/*|*/src/middleware.ts|*/next.config.mjs|*/src/env/*|*/src/actions/*) ;; + *) exit 0 ;; +esac + +cd "$(git rev-parse --show-toplevel 2>/dev/null || echo .)" || exit 0 + +# Run build silently. Redirect to capture everything for failure diagnosis. +if output="$(npx --no-install next build 2>&1)"; then + exit 0 +fi + +# If the build failed because env vars aren't populated locally, that's not a +# code-quality problem — emit a hint to stderr at info level and exit 0. +if printf '%s' "$output" | grep -q "Invalid environment variables"; then + printf 'build skipped: local .env not populated (t3-oss validation). Copy .env.example to .env.local to enable this hook.\n' >&2 + exit 0 +fi + +# Real build failure — surface it. +printf 'build failed:\n%s\n' "$output" >&2 +exit 2 diff --git a/.claude/hooks/post-edit-test.sh b/.claude/hooks/post-edit-test.sh new file mode 100755 index 00000000..9a5dfffa --- /dev/null +++ b/.claude/hooks/post-edit-test.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# PostToolUse hook: after an Edit/Write on a .ts/.tsx file, run the vitest +# test(s) that correspond to that file — if any exist. +# +# Why scoped (not "run all tests"): we don't want every edit to pay for the +# full suite. We only run tests related to what changed, which scales with +# the test backlog and keeps the feedback loop tight. +# +# Discovery rules (first match wins): +# 1. The edited file is itself a test → run just that file. +# 2. `tests//foo.test.ts(x)` exists → run that. +# 3. `src/**/__tests__/foo.test.ts(x)` colocated → run that. +# 4. No related test → exit 0 silently. +# +# Silent on success. Exits 2 on test failure. + +set -u +INPUT="$(cat || true)" + +path="" +if command -v jq >/dev/null 2>&1; then + path="$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null || true)" +fi + +[[ -z "$path" ]] && exit 0 + +case "$path" in + *.ts|*.tsx) ;; + *) exit 0 ;; +esac + +cd "$(git rev-parse --show-toplevel 2>/dev/null || echo .)" || exit 0 + +# Convert absolute path to repo-relative. +rel="${path#"$PWD/"}" + +# 1. The file is itself a test. +if [[ "$rel" == *.test.ts || "$rel" == *.test.tsx ]]; then + target="$rel" +else + # Strip src/ prefix and extension to build a module key. + base="${rel#src/}" + base="${base%.tsx}" + base="${base%.ts}" + + # 2. tests//foo.test.* + candidate_ts="tests/${base}.test.ts" + candidate_tsx="tests/${base}.test.tsx" + # 3. src/**/__tests__/.test.* + name="${base##*/}" + dir="src/$(dirname "$base")" + colocated_ts="${dir}/__tests__/${name}.test.ts" + colocated_tsx="${dir}/__tests__/${name}.test.tsx" + + target="" + for c in "$candidate_ts" "$candidate_tsx" "$colocated_ts" "$colocated_tsx"; do + if [[ -f "$c" ]]; then + target="$c" + break + fi + done + + # Nothing related — silent exit. + [[ -z "$target" ]] && exit 0 +fi + +if output="$(npx --no-install vitest run "$target" 2>&1)"; then + exit 0 +fi + +printf 'tests failed for %s:\n%s\n' "$target" "$output" >&2 +exit 2 diff --git a/.claude/hooks/post-edit-typecheck.sh b/.claude/hooks/post-edit-typecheck.sh new file mode 100755 index 00000000..187316e3 --- /dev/null +++ b/.claude/hooks/post-edit-typecheck.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# PostToolUse hook: after Edit/Write/MultiEdit on a TypeScript file, run typecheck. +# Silent on success, surfaces errors and exits 2 on failure (so Claude sees them). +# +# Claude Code passes hook input on stdin as JSON. We read tool_input.file_path +# to decide whether this edit touched TypeScript. If jq is unavailable, fall +# back to matching raw stdin. + +set -u +INPUT="$(cat || true)" + +path="" +if command -v jq >/dev/null 2>&1; then + path="$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null || true)" +fi + +# Match .ts/.tsx only; skip .d.ts since they're generated. +if [[ -z "$path" ]]; then + if ! printf '%s' "$INPUT" | grep -qE '\.tsx?"'; then + exit 0 + fi +else + case "$path" in + *.ts|*.tsx) ;; + *) exit 0 ;; + esac +fi + +# Run typecheck from the repo root. +cd "$(git rev-parse --show-toplevel 2>/dev/null || echo .)" || exit 0 + +if output="$(npx --no-install tsc --noEmit 2>&1)"; then + exit 0 +fi + +# Failure: print errors to stderr and exit 2 so Claude receives them. +printf 'typecheck failed:\n%s\n' "$output" >&2 +exit 2 diff --git a/.claude/hooks/pre-commit-external-guard.sh b/.claude/hooks/pre-commit-external-guard.sh new file mode 100755 index 00000000..918537f5 --- /dev/null +++ b/.claude/hooks/pre-commit-external-guard.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# pre-commit-external-guard.sh — reject any staged change under .external/. +# +# Installed as a git pre-commit hook. The .external/ tree is a vendored +# read-only mirror; edits there would be silently overwritten by the +# next /sync-externals run. Failing the commit early is cheaper than +# losing work. +# +# Install once (per clone): +# ln -sf ../../.claude/hooks/pre-commit-external-guard.sh .git/hooks/pre-commit +# Or copy the file; symlink keeps it updating automatically. + +set -u + +staged="$(git diff --cached --name-only --diff-filter=ACMR 2>/dev/null)" +offenders="$(printf '%s\n' "$staged" | grep -E '^\.external/' || true)" + +if [[ -n "$offenders" ]]; then + echo "✗ Refusing commit — staged changes under .external/:" >&2 + printf ' %s\n' "$offenders" >&2 + echo "" >&2 + echo " .external/ is a vendored read-only mirror. Edits belong in" >&2 + echo " the upstream repo (see .claude/externals.json for origins)." >&2 + echo " Unstage with: git restore --staged .external/" >&2 + exit 1 +fi + +exit 0 diff --git a/.claude/hooks/stop-format-check.sh b/.claude/hooks/stop-format-check.sh new file mode 100755 index 00000000..711f0a4d --- /dev/null +++ b/.claude/hooks/stop-format-check.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Stop hook: when Claude finishes a turn, verify formatting is clean. +# Silent on success; lists offending files and exits 2 on failure. + +set -u +cd "$(git rev-parse --show-toplevel 2>/dev/null || echo .)" || exit 0 + +# Only run if package.json has a format:check script (it does in this repo). +if ! grep -q '"format:check"' package.json 2>/dev/null; then + exit 0 +fi + +if output="$(npm run --silent format:check 2>&1)"; then + exit 0 +fi + +printf 'format check failed (run `npm run format` to fix):\n%s\n' "$output" >&2 +exit 2 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..2b793045 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,83 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "permissions": { + "allow": [ + "Bash(npm run lint)", + "Bash(npm run lint:*)", + "Bash(npm run test)", + "Bash(npm run test:run)", + "Bash(npm run test:coverage)", + "Bash(npm run typecheck)", + "Bash(npm run format)", + "Bash(npm run format:check)", + "Bash(npm run build)", + "Bash(npx tsc --noEmit)", + "Bash(npx tsc --noEmit:*)", + "Bash(npx vitest run)", + "Bash(npx vitest run:*)", + "Bash(npx next build)", + "Bash(npx next lint)", + "Bash(git status)", + "Bash(git diff:*)", + "Bash(git log:*)", + "Bash(git show:*)", + "Bash(git branch:*)", + "Bash(git stash:*)", + "Bash(git add:*)", + "Bash(ls:*)", + "Bash(cat:*)", + "Bash(wc:*)", + "Bash(mkdir:*)", + "Bash(mv:*)", + "Bash(cp:*)", + "Bash(node -v)", + "Bash(npm -v)", + "Bash(.claude/hooks/*.sh)" + ], + "deny": [ + "Bash(rm -rf:*)", + "Bash(git push --force:*)", + "Bash(git push -f:*)", + "Bash(git reset --hard:*)", + "Write(./.env)", + "Write(./.env.local)", + "Write(./.env.production)", + "Write(./.env.development)", + "Edit(./.env)", + "Edit(./.env.local)", + "Edit(./.env.production)", + "Edit(./.env.development)" + ] + }, + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write|MultiEdit", + "hooks": [ + { + "type": "command", + "command": ".claude/hooks/post-edit-typecheck.sh" + }, + { + "type": "command", + "command": ".claude/hooks/post-edit-test.sh" + }, + { + "type": "command", + "command": ".claude/hooks/post-edit-build.sh" + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": ".claude/hooks/stop-format-check.sh" + } + ] + } + ] + } +} diff --git a/.claude/skills/contract-abis/SKILL.md b/.claude/skills/contract-abis/SKILL.md new file mode 100644 index 00000000..412b8bf4 --- /dev/null +++ b/.claude/skills/contract-abis/SKILL.md @@ -0,0 +1,78 @@ +--- +name: contract-abis +description: Use when working with the ABIs in contracts-abi/, the typed bindings in src/lib/tokens/*-abi.ts (WETH, ERC20), Genesis SBT bindings in src/lib/contract-config.tsx, or when tracing contract calls across the app. Use also when an ABI changes upstream and the app needs to sync. Note — the FastSettlement contract is NOT called directly from the web app; swaps go through the FastSwap HTTP API under src/app/api/fastswap. +--- + +# Contract ABIs + +How the app consumes Fast Protocol contract ABIs and keeps typed bindings in sync. + +## When to use + +- An ABI changed in `contracts-abi/abi/` +- A new contract method needs to be called from the app +- Tracing "where do we call function X" across hooks and components +- Updating `src/lib/contract-config.tsx` + +## Key files + +- ABI source: `contracts-abi/abi/` (JSON) +- Typed bindings (hand-maintained, const-asserted): + - `src/lib/tokens/weth-abi.ts` — minimal WETH9 (deposit/withdraw/balanceOf) + - `src/lib/tokens/erc20-abi.ts` — generic ERC-20 interface +- Address + ABI composition: `src/lib/contract-config.tsx` (Genesis SBT) +- Server-side helpers: `src/lib/contract-server.ts` +- FastSettlement struct types (for EIP-712 payloads, not direct calls): + `src/types/swap.ts` + +> **FastSettlement is HTTP-only from the web app.** Swaps post signed +> `SwapIntent` payloads to the FastSwap API (`src/app/api/fastswap/route.ts`), +> which relays to the backend executor. The standalone `fast-settlement-*.ts` +> modules under `src/lib/` were removed in the agentic-repo-design refactor +> because nothing imported them. If you need a direct contract call path +> later, use viem's `getContract` against the address from +> `src/lib/config/network.ts` and the ABI JSON under `contracts-abi/abi/`. + +## References + +- ABI directory layout: [`abi-layout.md`](./abi-layout.md) +- Typing patterns (viem `const` ABIs): [`typing-patterns.md`](./typing-patterns.md) +- How contracts & abis relate: `agent_docs/contracts-and-abis.md` + +## Workflow + +### When an ABI changes + +1. Drop the new JSON into `contracts-abi/abi/`. +2. Update the corresponding `*-abi.ts` under `src/lib/tokens/` (or + `src/lib/contract-config.tsx` for Genesis SBT) — keep the `as const` + assertion so viem can infer types. +3. Use the `abi-tracer` subagent to find call sites: + `Ask abi-tracer: "find every call site for function "`. +4. Update callers. +5. Run `/verify`. The compiler will complain about arg/return shape + mismatches — fix them, don't cast away. + +### When adding a new method call + +1. Ensure the method exists in the `*-abi.ts` binding (if not, add it from + the JSON). +2. Use wagmi's `useReadContract` / `useWriteContract` with the `abi` and + `functionName` typed against the const ABI. +3. Add the address to `src/lib/contract-config.tsx` if it's a new contract. + +## Guardrails + +- **Never cast an ABI to `any`.** viem's type inference depends on the const shape. +- **Never duplicate an ABI inline** in a hook. Import from + `src/lib/tokens/*-abi.ts` or `src/lib/contract-config.tsx`. +- **Addresses belong in config**, never in a component or hook. +- **Do not edit `contracts-abi/clients/`** — those are Go clients for other + services; not the web app. + +## Verification + +- `/verify` +- `npm run build` — catches ABI typing regressions across the server/client + boundary. The `post-edit-build.sh` hook runs this automatically when you + edit server routes that consume ABIs. diff --git a/.claude/skills/contract-abis/abi-layout.md b/.claude/skills/contract-abis/abi-layout.md new file mode 100644 index 00000000..c09799bf --- /dev/null +++ b/.claude/skills/contract-abis/abi-layout.md @@ -0,0 +1,32 @@ +# `contracts-abi/` layout + +``` +contracts-abi/ +├── abi/ ← JSON ABIs — authoritative for the web app +├── clients/ ← Go clients — NOT for web consumption +├── go.mod / go.sum +└── script.sh ← ABI generation script (Go-side) +``` + +## `abi/` — what the web app consumes + +JSON files, one per contract interface. The web app does not read JSON directly; typed bindings in `src/lib/*-abi.ts` are hand-maintained to mirror them (`as const` for viem's type inference). + +## `clients/` — off-limits for the web app + +Generated Go bindings used by backend services. Do not import, regenerate, or modify these from the Next.js codebase. + +## Regenerating clients + +If you need to regenerate Go clients, run `contracts-abi/script.sh` from that directory — outside the scope of web-app agent work. + +## Version skew + +Tables to keep aligned when an ABI changes: + +| Layer | File(s) | +|---|---| +| Raw JSON | `contracts-abi/abi/.json` | +| TS binding | `src/lib/-abi.ts` or `fast-settlement-*.ts` | +| Address | `src/lib/contract-config.tsx` | +| Call sites | hooks, components, server helpers (use `abi-tracer`) | diff --git a/.claude/skills/contract-abis/typing-patterns.md b/.claude/skills/contract-abis/typing-patterns.md new file mode 100644 index 00000000..6feb984e --- /dev/null +++ b/.claude/skills/contract-abis/typing-patterns.md @@ -0,0 +1,56 @@ +# Typing patterns (viem + wagmi) + +viem infers types from `as const` ABIs. Losing the const assertion breaks inference and falls back to `any`. + +## Pattern: typed ABI + +```ts +// src/lib/weth-abi.ts +export const WETH_ABI = [ + { + type: 'function', + name: 'deposit', + stateMutability: 'payable', + inputs: [], + outputs: [], + }, + // ... +] as const +``` + +`as const` is non-negotiable. Without it, `functionName` is `string` and args become `unknown[]`. + +## Pattern: using with wagmi + +```ts +import { WETH_ABI } from '@/lib/weth-abi' + +const { writeContractAsync } = useWriteContract() +await writeContractAsync({ + abi: WETH_ABI, + address: WETH_ADDRESS, + functionName: 'deposit', // autocompletes + args: [], // typed from ABI + value: parseEther('1'), // payable methods +}) +``` + +## Pattern: subset ABI + +If you only need one method from a larger interface, inline the method in a const and use it — do not flatten the whole ABI: + +```ts +const DEPOSIT_ABI = [ + { type: 'function', name: 'deposit', stateMutability: 'payable', inputs: [], outputs: [] }, +] as const +``` + +## Anti-patterns + +- `abi: WETH_ABI as Abi` — throws away inference. Don't cast. +- Spreading ABIs (`[...ABI_A, ...ABI_B]`) inline — loses const-ness unless wrapped in a new `as const`. +- Using `ethers`'s `Contract` class for new code — prefer viem + wagmi. `ethers` is present only because some legacy paths use it. + +## Multiple contract versions + +`fast-settlement-v2-1.ts` and `fast-settlement-v3-abi.ts` are separate files on purpose — they have diverging method surfaces. `src/lib/contract-config.tsx` picks the version for the current deployment. Never merge them. diff --git a/.claude/skills/dashboard-data/SKILL.md b/.claude/skills/dashboard-data/SKILL.md new file mode 100644 index 00000000..07e5899c --- /dev/null +++ b/.claude/skills/dashboard-data/SKILL.md @@ -0,0 +1,54 @@ +--- +name: dashboard-data +description: Use when adding or modifying hooks under src/hooks/, particularly dashboard/user-data hooks and anything using TanStack Query. Covers query-key conventions, prefetching (use-page-prefetch, use-prefetch-dashboard), and where to put shared helpers. +--- + +# Dashboard data (hooks & TanStack Query) + +How this repo models server state. Follow existing patterns; do not introduce Redux / SWR / Jotai. + +## When to use + +- Adding or editing a hook in `src/hooks/` +- Introducing a new TanStack Query query or mutation +- Changing prefetching behavior on any route +- Debugging stale or flickering dashboard data + +## Key files + +- Barrel: `src/hooks/index.ts` +- Dashboard surface: `src/hooks/use-dashboard-data.ts`, `use-prefetch-dashboard.ts`, `use-dashboard-tasks.ts` +- User state: `src/hooks/use-user-points.ts`, `use-user-swaps.ts` +- Prefetch helper: `src/hooks/use-page-prefetch.ts` +- Query client: mounted in `src/components/providers.tsx` + +## References + +- Hook patterns: [`hook-patterns.md`](./hook-patterns.md) +- Query keys: [`query-keys.md`](./query-keys.md) + +## Workflow + +1. Check `src/hooks/index.ts` for an existing hook before creating a new one. +2. For a new query: + - Name: `use-[-action]`. Filenames in kebab-case. + - Query key: stable array, starts with the domain prefix (see `query-keys.md`). + - `queryFn` is pure — no side effects, returns raw data. + - `select` for derived state, not transforms that should happen in the component. + - `staleTime` / `gcTime` chosen for the data's freshness requirements. +3. For server writes, use mutations with `onSuccess` invalidation of affected query keys. +4. If the hook reads wallet state, gate with `enabled: Boolean(address)`. +5. Add the export to `src/hooks/index.ts`. + +## Guardrails + +- Never fire a query in render without `enabled: ...` when it depends on wallet / auth state. +- Never share a query key between different data shapes — use distinct prefixes. +- Never put mutable refs in query keys — keys must serialize deterministically. +- Avoid `refetchInterval` unless the data is genuinely time-sensitive. Prefer event-driven invalidation. +- Do not add a new `QueryClient` — there's one, in `providers.tsx`. + +## Verification + +- `/verify` +- Inspect TanStack Query devtools (if enabled in dev) — look for unexpected retries, stale caches, duplicate keys. diff --git a/.claude/skills/dashboard-data/hook-patterns.md b/.claude/skills/dashboard-data/hook-patterns.md new file mode 100644 index 00000000..0bb87983 --- /dev/null +++ b/.claude/skills/dashboard-data/hook-patterns.md @@ -0,0 +1,65 @@ +# Hook patterns + +## Shape of a data hook + +```ts +// src/hooks/use-example.ts +import { useQuery } from '@tanstack/react-query' +import { useAccount } from 'wagmi' + +export function useExample() { + const { address } = useAccount() + return useQuery({ + queryKey: ['example', address], // stable, address-scoped + queryFn: () => fetchExample(address!), // pure + enabled: Boolean(address), // don't fire without deps + staleTime: 30_000, // tune per data + }) +} +``` + +## Shape of a mutation hook + +```ts +// src/hooks/use-example-mutation.ts +import { useMutation, useQueryClient } from '@tanstack/react-query' + +export function useExampleMutation() { + const qc = useQueryClient() + return useMutation({ + mutationFn: (input: Input) => postExample(input), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['example'] }) + }, + }) +} +``` + +## Composition hooks (hooks that compose other hooks) + +Many swap hooks compose multiple primitives. Rules: + +- The outer hook is the orchestrator; inner hooks fire independently. +- Return a discriminated union for state: `{ status: 'idle' } | { status: 'loading' } | ...`, not loose booleans. +- Isolate side effects in `useEffect`; never fire a side effect in the render path. + +## Reading wagmi inside a hook + +- Import `useAccount`, `useChainId`, `useBalance`, etc. — don't read from a context. +- Gate downstream queries on `isConnected` or `Boolean(address)`. + +## Prefetching + +For routes with predictable next-steps (dashboard → swap, etc.): + +```ts +// use-page-prefetch / use-prefetch-dashboard patterns +const qc = useQueryClient() +qc.prefetchQuery({ queryKey: [...], queryFn: ... }) +``` + +Prefetch on hover or on route-change intent — not on every render. + +## Hooks that aren't data hooks + +Utility hooks (`use-mobile`, `use-page-active`, `use-toast`) don't use TanStack Query. Keep them small, pure, and export a stable API. diff --git a/.claude/skills/dashboard-data/query-keys.md b/.claude/skills/dashboard-data/query-keys.md new file mode 100644 index 00000000..6ede84dd --- /dev/null +++ b/.claude/skills/dashboard-data/query-keys.md @@ -0,0 +1,55 @@ +# Query keys + +## Convention + +Every TanStack Query key is an array starting with a domain-scoped string: + +```ts +['swap', 'quote', { fromToken, toToken, amount, chainId }] +['dashboard', 'points', address] +['leaderboard', 'tier', 'gold', chainId] +['miles', 'estimated', address] +['tokens', 'balances', address, chainId] +['barter', 'supported-tokens'] +['fuul', 'miles-leaderboard'] +``` + +Rules: + +1. **First element** = domain name, matching the hook filename prefix where possible. +2. **Second element** = sub-noun or verb. +3. **Remaining elements** = dependencies, in stable order. +4. Use **objects** only when you have 3+ related params; otherwise list them. +5. Never include functions, Dates (stringify to ISO), or non-stable refs. + +## Invalidation patterns + +```ts +// Invalidate a whole domain +qc.invalidateQueries({ queryKey: ['dashboard'] }) + +// Invalidate one specific query +qc.invalidateQueries({ queryKey: ['dashboard', 'points', address] }) + +// Predicate-based +qc.invalidateQueries({ predicate: (q) => q.queryKey[0] === 'dashboard' }) +``` + +## Shared key factory (recommended) + +When a domain has many queries, define a factory: + +```ts +// src/lib/query-keys.ts (add if/when needed) +export const dashboardKeys = { + all: ['dashboard'] as const, + points: (address: string) => ['dashboard', 'points', address] as const, + swaps: (address: string) => ['dashboard', 'swaps', address] as const, +} +``` + +This prevents key drift across hooks. Do not create this pre-emptively — add it when you find yourself repeating keys in three+ places. + +## When you break the convention + +Rare, but if an external SDK (Fuul, Barter) returns data under its own key space and you want cache sharing, document the exception at the top of the hook file. diff --git a/.claude/skills/defi-swap/SKILL.md b/.claude/skills/defi-swap/SKILL.md new file mode 100644 index 00000000..a0cc70b5 --- /dev/null +++ b/.claude/skills/defi-swap/SKILL.md @@ -0,0 +1,58 @@ +--- +name: defi-swap +description: Use when editing the swap flow — quoting, slippage, permit2, WETH wrap/unwrap, ETH-path tx construction, Fast RPC quote polling, or anything under src/components/swap or src/hooks/use-swap-*. Also for tx-confirmation UX and preconfirm handling. +--- + +# DeFi swap + +The swap engine is the critical path of this app. Changes here can move user funds — be exact. + +## When to use + +- Editing any file in `src/components/swap/**` +- Editing any hook matching `src/hooks/use-swap-*.ts` or `use-permit2-*`, `use-weth-wrap-unwrap` +- Editing `src/lib/swap-logic/`, `swap-constants.ts`, `swap-events.ts`, `swap-server.ts`, `quote-guard.ts` +- Changing slippage behavior, permit2 deadlines, or WETH conversions + +## Key files + +- Engine: `src/lib/swap-logic/token-list.ts`, `src/lib/swap-server.ts`, `src/lib/swap-constants.ts`, `src/lib/swap-events.ts` +- Hooks: `src/hooks/use-swap-form.ts`, `use-swap-quote.ts`, `use-swap-intent.ts`, `use-swap-slippage.ts`, `use-swap-confirmation.ts` +- Guards: `src/lib/quote-guard.ts`, `src/hooks/use-quote-guard-config.ts` +- UI: `src/components/swap/SwapInterface.tsx`, `SwapForm.tsx`, `SellCard.tsx`, `BuyCard.tsx`, `ActionButton.tsx`, `TransactionSettings.tsx` +- Tx construction: `src/lib/eth-path-tx.ts`, `src/hooks/use-eth-path-gas-estimate.ts` +- State: `src/stores/swapToastStore.ts`, `src/components/swap/SwapToast*.tsx` +- Types: `src/types/swap.ts` + +## References + +- Engine mechanics: [`swap-engine.md`](./swap-engine.md) +- Permit2: [`permit2.md`](./permit2.md) +- Quote polling: [`quote-polling.md`](./quote-polling.md) +- Human deep-dive: `docs/swap-interface.md` +- Tx confirmation flow: `docs/tx-confirmation-flow.md` +- Quote freshness: `docs/quote-polling-idle-detection.md` + +## Workflow + +1. Read `docs/swap-interface.md` first if you're unfamiliar with the flow. +2. Identify which stage of the flow the change sits in: form input → quote → intent → permit2 approval → confirmation → preconfirm → settlement. +3. Trace existing hook composition before adding a new hook — the flow is chained intentionally. +4. Mirror existing patterns for error handling (see `src/lib/transaction-errors.ts`). +5. Run `npm run test:run` — swap logic has tests in `src/lib/__tests__/`. +6. Verify in dev: run through the swap flow manually, watch the swap-toast events. + +## Guardrails + +- **Never loosen slippage bounds** without explicit user request. Default constraints are in `src/lib/swap-constants.ts`. +- **Respect permit2 deadlines.** See [permit2.md](./permit2.md). +- **Do not skip `quote-guard`** for "simplicity." Stale quotes = user loss. +- **Do not log signed payloads** anywhere — analytics, console, errors. +- **Preserve existing error-normalization** in `src/lib/transaction-errors.ts`. Surfacing raw provider errors to users is a regression. +- **Test ETH-path and ERC20-path separately.** `use-eth-path-gas-estimate` and the regular path diverge. + +## Verification + +- `/verify` for types + lint + tests. +- Manual: run `npm run dev`, perform a real swap on a testnet, watch the toast sequence. +- Edge cases to touch: slippage at limit, permit2 expiring mid-flow, WETH → ETH unwrap, ETH → WETH wrap, network switch mid-quote. diff --git a/.claude/skills/defi-swap/permit2.md b/.claude/skills/defi-swap/permit2.md new file mode 100644 index 00000000..2f01802a --- /dev/null +++ b/.claude/skills/defi-swap/permit2.md @@ -0,0 +1,38 @@ +# Permit2 + +Uniswap's signature-based approval standard. One signed permit authorizes many future transfers to a spender until the deadline expires or the nonce is invalidated. + +## Why this matters + +- Cheaper: one permit signature replaces per-swap `approve` transactions. +- Safer: deadlines limit exposure if a signature leaks. +- Correctness-critical: a wrong nonce or expired deadline silently fails the swap at settlement. + +## Files + +- `src/lib/permit2-utils.ts` — signature construction, typed-data helpers +- `src/hooks/use-permit2-allowance.ts` — checks whether a permit is needed +- `src/hooks/use-permit2-nonce.ts` — fetches the next nonce + +## Invariants + +1. **Deadline ≠ optional.** Always set a bounded deadline (typically minutes, not hours). The guard logic compares against chain time. +2. **Nonce freshness.** Always fetch nonce as close to signing as possible. A cached nonce causes silent failures. +3. **Spender address** must match the settlement contract the app is targeting (see `src/lib/contract-config.tsx`). Cross-contract permits are not interchangeable. +4. **Signature is not a transaction.** Never log, transmit, or persist it beyond the swap-execution path. Drop it from state as soon as the tx is submitted. +5. **Amount vs max-amount.** The app generally signs the exact amount for the current swap. Do not introduce "unlimited" permits without an explicit product decision. + +## When permit2 changes + +If `src/lib/contract-config.tsx` or the Permit2 address changes: + +1. Update `permit2-utils.ts` typed data. +2. Update allowance hook if the interface diverged. +3. Run all swap tests. +4. Manually test a signed swap on testnet before merging. + +## Anti-patterns + +- Do not bypass `use-permit2-allowance` — the "if I just sign every time" pattern leaks signatures and wastes UX. +- Do not hardcode `MaxUint256` deadlines. +- Do not share nonces across different spenders. diff --git a/.claude/skills/defi-swap/quote-polling.md b/.claude/skills/defi-swap/quote-polling.md new file mode 100644 index 00000000..66ff7255 --- /dev/null +++ b/.claude/skills/defi-swap/quote-polling.md @@ -0,0 +1,37 @@ +# Quote polling & idle detection + +Quotes are perishable. Poll, but gate polling on user presence and stale detection. + +## Source docs + +`docs/quote-polling-idle-detection.md` is the human-facing deep-dive — read it first for rationale. + +## Files + +- `src/hooks/use-swap-quote.ts` — the polling hook +- `src/hooks/use-quote-guard-config.ts` — runtime polling config +- `src/hooks/use-page-active.ts` — page-visibility / idle detection +- `src/lib/quote-guard.ts` — staleness evaluation + +## Design + +1. Poll interval is adaptive. When the page is hidden or the user is idle, the interval backs off. +2. Every quote carries a timestamp and expected validity window. +3. Before showing a quote or enabling the action button, `quote-guard.ts` checks: + - Is the quote fresher than the max-age? + - Has the input/output amount deviated beyond the configured threshold since the quote was issued? + - Are slippage bounds still satisfied? +4. Stale → quote is discarded, action button disabled, UI re-fetches. + +## Touching the polling interval + +Be cautious. Too fast = RPC load + rate limits. Too slow = stale quotes, user friction. + +- Prefer tuning thresholds in `use-quote-guard-config.ts` (runtime-configurable) over hardcoded intervals. +- Changes here affect real user cost — test with a throttled network and an idle tab. + +## Anti-patterns + +- Don't disable the guard "to get a test to pass" — rewrite the test. +- Don't poll when the tab is hidden at the same rate as when visible. +- Don't silently refresh the quote mid-sign — the user is looking at a specific number; jumping it is worse than failing the swap. diff --git a/.claude/skills/defi-swap/swap-engine.md b/.claude/skills/defi-swap/swap-engine.md new file mode 100644 index 00000000..9f45774e --- /dev/null +++ b/.claude/skills/defi-swap/swap-engine.md @@ -0,0 +1,50 @@ +# Swap engine + +## Flow (high level) + +1. **User input** — `SwapForm.tsx` binds amount, token pair, slippage. Validation via `use-swap-form`. +2. **Quote** — `use-swap-quote.ts` fetches from the quote source (Fast RPC / Barter depending on config). Polls — see [quote-polling.md](./quote-polling.md). +3. **Quote guard** — `quote-guard.ts` checks staleness / price bounds; disables action if stale. +4. **Intent** — `use-swap-intent.ts` produces the signable payload. +5. **Permit2 approval (if needed)** — `use-permit2-allowance.ts` checks; `use-permit2-nonce.ts` fetches nonce; user signs (NOT a tx). +6. **Tx build** — `eth-path-tx.ts` (for ETH path) or the standard path inside the engine. +7. **User signs tx** — wagmi's write hook. +8. **Wait for preconfirm** — Fast Protocol's sub-second commitment. +9. **Wait for final confirmation** — `use-wait-for-tx-confirmation.ts`. +10. **Toast + telemetry** — `swap-events.ts` logs events; `swapToastStore` drives the toast UI. + +## Source files in order of execution + +``` +src/hooks/use-swap-form.ts +src/hooks/use-swap-quote.ts +src/lib/quote-guard.ts +src/hooks/use-swap-intent.ts +src/hooks/use-permit2-allowance.ts +src/hooks/use-permit2-nonce.ts +src/lib/permit2-utils.ts +src/lib/eth-path-tx.ts +src/hooks/use-eth-path-gas-estimate.ts +src/hooks/use-swap-confirmation.ts +src/hooks/use-wait-for-tx-confirmation.ts +src/lib/swap-events.ts +src/stores/swapToastStore.ts +``` + +## State machines + +Most of the state is implicit in the TanStack Query cache (for quotes and balances) + hook composition (for the intent → approval → send pipeline). Do not introduce a new state-management library. If you need to add state, extend the existing store or add a narrow Zustand slice following `swapToastStore.ts`. + +## ETH path vs ERC20 path + +ETH-input or ETH-output swaps require extra handling (no approval needed for native ETH; unwrap/wrap on either leg). `eth-path-tx.ts` isolates this. Keep the branch explicit — don't try to unify the paths. + +## Touch points for common changes + +| Change | Files | +|---|---| +| Slippage default | `src/lib/swap-constants.ts` | +| Quote poll interval | `src/hooks/use-swap-quote.ts`, `docs/quote-polling-idle-detection.md` | +| Toast copy | `src/components/swap/SwapToast.tsx` | +| Preconfirm sound | `src/lib/preconfirm-sound.ts`, `src/components/swap/PreconfirmCelebration.tsx` | +| New error code surfaced | `src/lib/transaction-errors.ts` | diff --git a/.claude/skills/external-mev-commit/SKILL.md b/.claude/skills/external-mev-commit/SKILL.md new file mode 100644 index 00000000..37c62313 --- /dev/null +++ b/.claude/skills/external-mev-commit/SKILL.md @@ -0,0 +1,61 @@ +--- +name: external-mev-commit +description: Load when a question or edit depends on the mev-commit protocol source of truth — FastSwap HTTP handler shape, preconf RPC behavior, miles/Fuul indexer logic, canonical FastSettlementV3 ABI, or Solidity source needed to debug a revert. The upstream repo is vendored read-only under .external/mev-commit/ by the /prime and /sync-externals commands. Never write to .external/. +--- + +# External: mev-commit + +The upstream protocol repo (`primev/mev-commit`). Vendored read-only +under `.external/mev-commit/` so agents can grep Go source, ABIs, and +protocol types without MCP round-trips. Currently tracks `main` — the +lock file at `.external/.manifest.lock.json` records the current SHA +and age. + +## When to load this skill + +- Editing `src/app/api/fastswap/route.ts`, `src/hooks/use-swap-intent.ts`, + or anything else that shapes the JSON payload sent to the FastSwap + HTTP API +- Debugging a preconf-status error (`src/hooks/use-wait-for-tx-confirmation.ts`, + `src/lib/settlement/rpc-status.ts`) where the root cause is upstream + commitment or sender behavior +- Investigating a miles discrepancy — where a swap executed but the + user's Fuul balance didn't update as expected +- Syncing a local ABI under `contracts-abi/abi/` or + `src/lib/tokens/*-abi.ts` with an upstream change +- Reading Solidity source to understand a specific revert reason + +## Hard rules + +- **Never write to `.external/`.** It's a vendored mirror. The pre-commit + hook (`.claude/hooks/pre-commit-external-guard.sh`) will reject the + commit anyway. Edits belong in the real mev-commit repo + a separate + PR. +- **Never import from `.external/` at build time.** The path is + gitignored; production builds won't have it. +- **Check freshness before relying on a citation.** `/prime` and + `/sync-externals` both print the current SHA and age. If it's more + than 7 days old, run `/sync-externals` before using it as a source + of truth. + +## Navigation — where to look inside `.external/mev-commit/` + +The **authoritative scope map** is at +[`agent_docs/external-mev-commit.md`](../../../agent_docs/external-mev-commit.md) +— open that first on any non-trivial mev-commit question. It includes +the full endpoint-to-handler table, the miles/Fuul flow diagram, and +a reverse table mapping every Fast Protocol App file to its upstream +counterpart. + +Quick-reference subtopics: + +- [`contracts.md`](./contracts.md) — Solidity source (`contracts/contracts/`) +- [`abis.md`](./abis.md) — canonical ABI JSON and drift policy +- [`protocol-types.md`](./protocol-types.md) — Go types + preconf RPC handlers + +## First move on any cross-repo question + +Don't grep blindly. The table in `agent_docs/external-mev-commit.md` +under "Pointers back into Fast Protocol App" maps every dapp file to +its mev-commit source. Check that table first; it's faster than a +recursive grep, and it's maintained with every externals-related PR. diff --git a/.claude/skills/external-mev-commit/abis.md b/.claude/skills/external-mev-commit/abis.md new file mode 100644 index 00000000..9a6bd5d1 --- /dev/null +++ b/.claude/skills/external-mev-commit/abis.md @@ -0,0 +1,46 @@ +# mev-commit ABIs and drift policy + +The canonical ABI JSON lives at +`.external/mev-commit/contracts-abi/abi/`. Our local copies under +`contracts-abi/abi/` in THIS repo are supposed to be bit-identical +copies of those files. + +## What's there + +Both directories contain the same two files: + +| File | What it is | +|---|---| +| `FastSettlementV3.abi` | Full ABI: functions + events, including `IntentExecuted` and the `executeWith*` entry points | +| `IFastSettlementV3.abi` | Interface-only ABI | + +## Drift policy + +The ABI drift test at `tests/contracts-abi/abi-drift.test.ts`: + +1. Validates shape (every entry has `type`, functions have `inputs` / + `outputs` / `stateMutability`, etc.). +2. When `.external/mev-commit/contracts-abi/abi/` is present (i.e., an + agent has run `/prime`), diffs each of OUR files byte-for-byte + against the upstream copy. Divergence fails the test. + +This means: **never hand-edit `contracts-abi/abi/*.abi`**. If upstream +changes an ABI: + +1. Run `/sync-externals` to pull the new upstream. +2. Copy the updated JSON from `.external/mev-commit/contracts-abi/abi/` + into THIS repo's `contracts-abi/abi/`. +3. Regenerate or hand-update the TypeScript ABI bindings in + `src/lib/tokens/*-abi.ts` if the change affects their surface. +4. Run `npm run test:run` — the drift test should pass again, and any + typed-ABI consumer whose call shape changed will fail elsewhere so + you know what to fix. + +## The generated Go client + +`.external/mev-commit/contracts-abi/clients/FastSettlementV3/FastSettlementV3.go` +is the abigen-generated Go binding. Fast Protocol App doesn't use it +directly, but it's the fastest way to see the *canonical* Go struct +layouts for `Intent`, `SwapCall`, and event payloads — the upstream +Go services (including `fastswap-miles` and the preconf-rpc server) +consume it, so it's the ground truth for struct ordering. diff --git a/.claude/skills/external-mev-commit/contracts.md b/.claude/skills/external-mev-commit/contracts.md new file mode 100644 index 00000000..75df1272 --- /dev/null +++ b/.claude/skills/external-mev-commit/contracts.md @@ -0,0 +1,46 @@ +# mev-commit Solidity source + +Lives at `.external/mev-commit/contracts/contracts/`. Read when a +transaction reverts with an opaque reason and you need to ground the +message in the contract that produced it. + +## Layout + +``` +.external/mev-commit/contracts/ +├── contracts/ Solidity source — contracts + interfaces +│ ├── FastSettlementV3.sol # THE settlement contract; swap entry +│ ├── IFastSettlementV3.sol # interface (re-exported to our app) +│ └── ... +├── lib/ foundry deps (openzeppelin, etc.) +├── scripts/ deploy scripts +└── test/ foundry tests — useful as reference for how + upstream calls the contract it ships +``` + +## Load-bearing files + +- `FastSettlementV3.sol` — executes `executeWithPermit` (ERC-20 flow) + and `executeWithETH` (native ETH flow). Contains the + `WITNESS_TYPE_STRING` constant our `src/lib/swap/permit2-utils.ts` + must match byte-for-byte, and emits `IntentExecuted` (the event + `fastswap-miles` indexes). +- `IFastSettlementV3.sol` — interface. Mirrors the typed structs + (`Intent`, `SwapCall`, `TokenPermissions`) we declare in + `src/types/swap.ts`. If the interface changes, our types drift. +- `test/*.sol` — Foundry tests. Best reference for *how* upstream + expects the contract to be called, including edge cases and + expected reverts. + +## Debugging a revert + +1. `grep -rn "revert" .external/mev-commit/contracts/contracts/` to + list every revert site. +2. Match on the hex selector or the string in the error message. +3. Read the surrounding function to understand the precondition that + failed. + +For EIP-712-related reverts ("invalid signature", "permit expired"), +also read `src/lib/swap/permit2-utils.ts` in THIS repo and compare +against `WITNESS_TYPE_STRING` in `FastSettlementV3.sol` — any +single-character mismatch breaks every signature. diff --git a/.claude/skills/external-mev-commit/protocol-types.md b/.claude/skills/external-mev-commit/protocol-types.md new file mode 100644 index 00000000..b1d2311c --- /dev/null +++ b/.claude/skills/external-mev-commit/protocol-types.md @@ -0,0 +1,92 @@ +# mev-commit protocol types + RPC handlers + +Go source for everything Fast Protocol App consumes over HTTP or +JSON-RPC. All paths below are under `.external/mev-commit/`. + +## HTTP surface — `tools/preconf-rpc/` + +`tools/preconf-rpc/service/service.go` is the HTTP router. Registers: + +| Method + path | Handler source | +|---|---| +| `POST /fastswap` | `fastswap.Handler()` in `tools/preconf-rpc/fastswap/fastswap.go` | +| `POST /fastswap/eth` | `fastswap.ETHHandler()` same file | +| `GET /status/{txnHash}` | inline in `service.go` | +| `GET /user-transactions` | inline in `service.go` | +| `mevcommit_*` JSON-RPC namespace | `tools/preconf-rpc/handlers/handlers.go` | + +### `SwapRequest` schema — the contract our `/api/fastswap` must match + +In `tools/preconf-rpc/fastswap/fastswap.go`: + +```go +type SwapRequest struct { + User common.Address `json:"user"` + InputToken common.Address `json:"inputToken"` + OutputToken common.Address `json:"outputToken"` + InputAmt *big.Int `json:"inputAmt"` + UserAmtOut *big.Int `json:"userAmtOut"` + Recipient common.Address `json:"recipient"` + Deadline *big.Int `json:"deadline"` + Nonce *big.Int `json:"nonce"` + Signature []byte `json:"signature"` // EIP-712 Permit2 + Slippage string `json:"slippage,omitempty"` +} +``` + +Our `fastswapSchema` in `src/app/api/fastswap/route.ts` must mirror +this shape. Adding / renaming a field upstream without updating our +Zod schema results in us 400-ing ourselves before the request even +leaves. + +### Preconf RPC errors + +`tools/preconf-rpc/handlers/handlers.go` declares +`GetTransactionCommitments(ctx, txnHash) ([]*bidderapiv1.Commitment, +error)`. Errors here bubble up to our +`src/lib/settlement/rpc-status.ts` polling loop. The most common +sources: + +- `tools/preconf-rpc/store/store.go` — persistence-layer timeouts or + missing commitments +- `tools/preconf-rpc/sender/sender.go` — tx rejected upstream (balance + check, nonce collision, invalid signature); the error string + surfaces through the RPC response + +When you see an unfamiliar error string in the app, `grep -rn "" +.external/mev-commit/tools/preconf-rpc/` will land you on the source. + +## Miles indexer — `tools/fastswap-miles/` + +Separate Go service, NOT part of the preconf-rpc server. This is where +a completed swap becomes Fuul miles. + +Read order when debugging miles: + +1. `tools/fastswap-miles/README.md` — authoritative flow diagram and + the net-profit formula +2. `tools/fastswap-miles/sweep.go` — L1 indexer + StarRocks insert + (populates the `mevcommit_57173.fastswap_miles` table that our + `/api/fastswap-miles/by-address` reads) +3. `tools/fastswap-miles/miles.go` — computes miles per user, submits + to Fuul API + +Fast Protocol App never talks to this service directly; it reads the +results via the StarRocks analytics layer and the Fuul SDK. The +debugging chain for "user's miles look wrong": + +``` +UI → /api/fastswap-miles/by-address → StarRocks table + ↑ + fastswap-miles/sweep.go wrote it (or didn't) + ↑ + FastSettlementV3 emitted IntentExecuted (or didn't) +``` + +## What to ignore inside `tools/preconf-rpc/` + +Sparse-checkout pulls in the points/ subdir for completeness but Fast +Protocol App doesn't consume it directly yet. Also skip: +- `backrunner/`, `notifier/`, `pricer/`, `blocktracker/`, `bidder/` — + internal to the RPC server, not consumer surface. +- `main.go` — entrypoint / CLI, not logic. diff --git a/.claude/skills/leaderboard-miles/SKILL.md b/.claude/skills/leaderboard-miles/SKILL.md new file mode 100644 index 00000000..98989bea --- /dev/null +++ b/.claude/skills/leaderboard-miles/SKILL.md @@ -0,0 +1,49 @@ +--- +name: leaderboard-miles +description: Use when editing leaderboard UI, ranking logic, miles/rewards display, the Fuul integration, tier badges (Gold/Silver/Bronze), or the show_miles_estimate feature flag. Includes Referral Leaders tabs and the miles-gating rules from recent PRs. +--- + +# Leaderboard & miles + +The reward-surface layer. Miles = points; tiers = ranking buckets. + +## When to use + +- Editing `src/hooks/use-leaderboard-data.ts`, `use-fuul-miles-leaderboard.ts`, `use-estimated-miles.ts`, `use-user-points.ts`, `use-surplus-rate.ts` +- Changing `src/lib/leaderboard-config.ts`, `src/lib/fuul.ts`, `src/lib/miles-events.ts` +- Touching the `show_miles_estimate` feature flag in `src/lib/feature-flags.ts` +- Editing leaderboard or miles components in `src/components/dashboard/` / `src/components/referral/` + +## Key files + +- Config: `src/lib/leaderboard-config.ts`, `src/lib/feature-flags.ts` +- Data: `src/hooks/use-leaderboard-data.ts`, `use-fuul-miles-leaderboard.ts`, `use-estimated-miles.ts`, `use-user-points.ts`, `use-surplus-rate.ts` +- SDK integration: `src/lib/fuul.ts`, `src/lib/miles-events.ts` +- API: `src/app/api/fastswap-miles/`, `src/app/api/fuul/` +- Human docs: `docs/leaderboard-queries.md`, `docs/miles-estimation.md` + +## References + +- Tier system: [`tiers.md`](./tiers.md) +- Feature flag behavior: [`feature-flag.md`](./feature-flag.md) + +## Workflow + +1. Determine whether the change is **data** (ranking rules, thresholds, query logic) or **display** (UI, labels, gating). +2. For data: consult `docs/leaderboard-queries.md` — the queries are non-trivial and have been tuned. +3. For miles display: ALL miles UI must respect `show_miles_estimate` — check the flag, gate the render. +4. For tier thresholds: edit `src/lib/leaderboard-config.ts`; do not hardcode tiers in components. +5. For Fuul SDK changes: wrap in `src/lib/fuul.ts`; do not import `@fuul/sdk` in components. + +## Guardrails + +- **`show_miles_estimate` is load-bearing.** Recent PRs (see git log) gated `UserSwapsTable`, the miles toggle, and the Referral Leaders Miles tab behind this flag. When adding miles UI, gate it too. +- **Do not display miles as a currency.** Copy says "miles" or "estimated miles" — never "$". +- **Leaderboard queries are expensive.** Reuse the existing query where possible; do not add per-user N+1 patterns. +- **Fuul SDK keys stay server-side.** Never instantiate Fuul with a key from a client component. + +## Verification + +- `/verify` +- Toggle the `show_miles_estimate` flag and confirm the gated UI disappears. +- Inspect leaderboard loading in TanStack Query devtools — one query per visible tier, not per row. diff --git a/.claude/skills/leaderboard-miles/feature-flag.md b/.claude/skills/leaderboard-miles/feature-flag.md new file mode 100644 index 00000000..ebc2bc82 --- /dev/null +++ b/.claude/skills/leaderboard-miles/feature-flag.md @@ -0,0 +1,44 @@ +# `show_miles_estimate` feature flag + +The flag that gates miles UI. Introduced to hide miles surfaces when the miles program is paused or when volumes are mis-reported. + +## Source + +`src/lib/feature-flags.ts` — flag definitions. Flags are read at runtime (likely via Vercel Edge Config; check the file for the current mechanism). + +## What it gates (from recent commits) + +- `UserSwapsTable` on the dashboard +- The miles toggle on the leaderboard +- The "Referral Leaders Miles" tab +- The estimated-miles display in the swap flow + +When **off**, none of the above should render. When **on**, they should render normally. + +## How to gate a new miles surface + +```tsx +import { useFeatureFlags } from '@/lib/feature-flags' // or equivalent + +function MilesThing() { + const { show_miles_estimate } = useFeatureFlags() + if (!show_miles_estimate) return null + return
...
+} +``` + +Check `src/lib/feature-flags.ts` for the actual API — pattern may use a direct read, hook, or edge config call. + +## Testing + +When adding a miles-gated feature: + +1. Verify it renders with the flag **on**. +2. Verify it doesn't render with the flag **off** (not hidden via CSS — not in the DOM at all, or at least conditionally rendered). +3. Verify no dependent requests fire when the flag is off (`enabled: show_miles_estimate` on related queries). + +## Anti-patterns + +- Don't use CSS `display: none` — the data still loads, which wastes RPC calls and leaks state via devtools. +- Don't flip the flag default without checking all dependent surfaces. +- Don't inline the flag check in a hook's `enabled` branch and also in the consuming component's render branch — pick one layer (prefer the hook, so data doesn't load at all). diff --git a/.claude/skills/leaderboard-miles/tiers.md b/.claude/skills/leaderboard-miles/tiers.md new file mode 100644 index 00000000..e411e57d --- /dev/null +++ b/.claude/skills/leaderboard-miles/tiers.md @@ -0,0 +1,32 @@ +# Tiers + +Gold / Silver / Bronze — volume-based buckets. + +## Source of truth + +`src/lib/leaderboard-config.ts` defines tier thresholds and display metadata. Never hardcode a threshold in a component. + +## How rank is computed + +See `docs/leaderboard-queries.md` for the authoritative query. Summary: + +- Aggregate user swap volume over the configured window. +- Bucket by tier threshold. +- Rank within tier by volume (descending). + +## Display rules + +- Show tier badge via the tier component in `src/components/dashboard/` (existing patterns — reuse, don't recreate). +- Gold > Silver > Bronze visual hierarchy. +- User's own rank is highlighted; everyone else is neutral. + +## Edge cases + +- New user (no volume) — does not appear on leaderboard; show a "join" CTA if relevant. +- User at tier boundary — ties broken by earliest-to-reach-threshold (check query spec). +- Tier threshold changes — config-driven; do not retroactively rewrite user badges client-side. Server data drives. + +## Don't + +- Don't add a "Platinum" / "Diamond" tier without a config change + UI additions. The three-tier system is a product decision. +- Don't surface raw volume numbers in a context where a tier label is more meaningful. diff --git a/.claude/skills/merging-main/SKILL.md b/.claude/skills/merging-main/SKILL.md new file mode 100644 index 00000000..ab7a3c0f --- /dev/null +++ b/.claude/skills/merging-main/SKILL.md @@ -0,0 +1,260 @@ +--- +name: merging-main +description: Use when merging main into a feature branch, rebasing, or pulling upstream changes that haven't been reviewed against the agentic-repo patterns. The goal is to catch drift — main PRs don't know about this repo's conventions (Zod on all routes, folderized src/lib, strict TS, doc indexes, test seeds) and will quietly reintroduce old patterns. Covers the checklist, the commands, and where to update the agent-visible docs. +--- + +# Merging main without losing agentic alignment + +Main moves independently of the agentic work. Feature PRs that land there +don't know about: + +- The `src/lib/` folderization (swap/ tokens/ settlement/ api/ config/). +- `parseJson` / `parseSearchParams` / `parseParams` on every input-taking route. +- `strict: true` TypeScript (no new `any`, no `@ts-ignore`). +- The `src/app/` and `src/components/` directory maps in + `agent_docs/architecture.md`. +- The route/hook discovery indexes (`src/app/api/README.md`, + `src/hooks/README.md`). +- The ESLint `no-restricted-syntax` rule that forbids `request.json()` and + `request.nextUrl.searchParams` on API routes. + +Every merge is a chance for one of those to silently drift back. This skill +is the playbook that catches it. + +## When to use + +- `git merge origin/main` or `git pull origin main`. +- Opening a PR that says "merge conflicts" on GitHub. +- After a rebase onto a moved main. +- Any time the PR checks say `mergeable: DIRTY`. + +Run `/realign` after you've resolved mechanical conflicts but before +pushing — it's the most efficient time to catch pattern drift. + +## Pre-merge checklist + +Before you merge, capture the baseline: + +```bash +git fetch origin main +git log main..HEAD --oneline | wc -l # commits on your branch +git log $(git merge-base HEAD origin/main)..origin/main --oneline +``` + +Read those main-side commits — the PR titles tell you what categories of +change to watch for (new routes, new components, moved files). + +## Merge mechanics + +Always **merge**, never rebase a branch that's already been pushed and +reviewed: + +```bash +git merge origin/main --no-commit --no-ff +# resolve conflicts +# then the alignment pass below, ideally before `git commit` +``` + +Rebasing a shared branch forces a `git push --force`, which violates the +repo's safety protocol (see CLAUDE.md) and loses review comments on the PR. + +## Alignment pass (the hard part) + +After resolving mechanical conflicts, run these checks against the incoming +main changes. Each is a class of drift to catch. + +### 1. Stale import paths — caught at three points + +The folderization under `src/lib/` (commit `6889c3b`) moved most top-level +files into `config/ · tokens/ · settlement/ · swap/`. Main PRs that opened +before that commit still import the old paths. + +This class is caught automatically by the `no-restricted-imports` rule +in `eslint.config.js`. The rule fires at three points: + +1. **Local, right after merge** — the husky `post-merge` hook + (`.husky/post-merge`) runs `npm run lint` after every `git merge` or + `git pull` that touched `src/` and prints a loud warning on any hit. + Exit code is always 0 (the merge is already committed by the time + the hook fires), but the warning tells you to fix before pushing. +2. **CI on push** — `.github/workflows/verify.yml` has a `lint` job + that greps lint output for `no-restricted-imports` and fails the + PR check if any is found. +3. **Manual** — `npm run lint` or the `/realign` slash command. + +You do NOT need to grep manually — the rule knows the full table. + +If you ever add a new subfolder under `src/lib/`, update the rule in +`eslint.config.js` (and this table) with the old → new mapping. + +Full current rename table (mirrored in the lint rule): + +| Old path (pre-folderization) | New path | +|---|---| +| `@/lib/site-config` | `@/lib/config/site` | +| `@/lib/network-config` | `@/lib/config/network` | +| `@/lib/feature-flags` | `@/lib/config/feature-flags` | +| `@/lib/constants` | `@/lib/config/constants` | +| `@/lib/leaderboard-config` | `@/lib/config/leaderboard` | +| `@/lib/weth-abi` | `@/lib/tokens/weth-abi` | +| `@/lib/erc20-abi` | `@/lib/tokens/erc20-abi` | +| `@/lib/token-list` | `@/lib/tokens/token-list` | +| `@/lib/token-resolver` | `@/lib/tokens/token-resolver` | +| `@/lib/stablecoins` | `@/lib/tokens/stablecoins` | +| `@/lib/stablecoin-list` | `@/lib/tokens/stablecoin-list` | +| `@/lib/weth-utils` | `@/lib/tokens/weth-utils` | +| `@/lib/token-icons` | `@/lib/tokens/token-icons` | +| `@/lib/popular-tokens` | `@/lib/tokens/popular-tokens` | +| `@/lib/barter-supported-tokens` | `@/lib/tokens/barter-supported-tokens` | +| `@/lib/transaction-errors` | `@/lib/settlement/transaction-errors` | +| `@/lib/transaction-receipt-utils` | `@/lib/settlement/transaction-receipt-utils` | +| `@/lib/tx-config` | `@/lib/settlement/tx-config` | +| `@/lib/fast-rpc-status` | `@/lib/settlement/rpc-status` | +| `@/lib/fast-tx-status` | `@/lib/settlement/tx-status` | +| `@/lib/fast-db` | `@/lib/settlement/db` | +| `@/lib/preconfirm-sound` | `@/lib/settlement/preconfirm-sound` | +| `@/lib/slippage` | `@/lib/swap/slippage` | +| `@/lib/quote-guard` | `@/lib/swap/quote-guard` | +| `@/lib/eth-path-tx` | `@/lib/swap/eth-path-tx` | +| `@/lib/permit2-utils` | `@/lib/swap/permit2-utils` | +| `@/lib/barter-api` | `@/lib/swap/barter-api` | +| `@/lib/swap-constants` | `@/lib/swap/constants` | +| `@/lib/swap-events` | `@/lib/swap/events` | +| `@/lib/swap-server` | `@/lib/swap/server` | +| `@/lib/fast-settlement-v2-1` | **Deleted** — use `contracts-abi/` | +| `@/lib/fast-settlement-v3-abi` | **Deleted** — use `contracts-abi/` | + +Do NOT fix by reintroducing the old module — every such re-add is an +anti-pattern regression. Update the call site to the new path. + +### 2. New API routes must use Zod + parseJson + +The ESLint rule at `eslint.config.js:88-112` flags `request.json()` / +`request.nextUrl.searchParams` / `new URL(request.url)` on files under +`src/app/api/**/route.ts`. Run: + +```bash +npx next lint --dir src/app/api 2>&1 | grep -B 2 "no-restricted-syntax" +``` + +Any match is a drift — migrate that route to the pattern in +`.claude/skills/next-app-router/api-routes.md`. Don't silence the rule. + +### 3. TypeScript strictness — no regressions + +```bash +npx tsc --noEmit +``` + +If main introduced a file with `any` or `@ts-ignore`, strict mode will +still compile it (because explicit `any` is allowed). Hunt for those +manually: + +```bash +git diff main..HEAD -- src/ | grep -E "^\+.*: any\b|^\+.*as any\b|^\+.*@ts-ignore" +``` + +Replace with a real type. If the underlying API really is untyped (e.g. +`window.ethereum`), cast through `unknown` with a local interface rather +than leaking `any`. + +### 4. Doc indexes drift + +New routes, hooks, and component folders need to be registered in the +agent-visible docs: + +- **New `src/app//`**: add to the tree in + `agent_docs/architecture.md` under `## src/app/`. +- **New `src/components//`**: add to the tree in + `agent_docs/architecture.md` under `## src/components/`. +- **New API route under `src/app/api/`**: add to the list in + `src/app/api/README.md`. Note which ones accept user input vs. are + trigger-only. +- **New hook under `src/hooks/`**: add to the list in + `src/hooks/README.md`. + +Deleted routes need to be REMOVED from the same indexes. Main's /share +deletion in PR #109 is a prior example. + +### 5. Test seeds on new components / hooks + +Not strictly required, but if main added a new component in a critical +path (not just a marketing landing), copy the SwapToast template +(`tests/components/swap/SwapToast.test.tsx`) and seed at least one test. +Every untested component is a future regression-oracle gap. + +Priority: +- New API route → integration test (`tests/api/.test.ts`). +- New hook with state → happy-dom hook test + (`tests/hooks/.test.ts`). +- New component on a wallet/swap-critical path → functional test. +- New marketing component → skip; a11y sweep is enough. + +### 6. a11y sweep on new user-facing components + +```bash +npx vitest run tests/a11y +``` + +If main added a Dialog or a button with just an icon, the axe sweep will +catch missing accessible names. Close-button patterns in particular +(see `SwapConfirmationModal`'s `aria-label="Close"` fix) are a repeat +offender. + +### 7. Bundle check for dependency additions + +```bash +git diff main..HEAD -- package.json | grep -E "^\+.*\"" +``` + +Any new runtime dependency from main needs at least a sniff check: +- Is it already transitively installed? +- Does it duplicate functionality we have (date-fns vs dayjs, etc.)? +- Does it pin a peer-dep version that conflicts with wagmi/viem/RainbowKit? + +## Post-merge verification + +After the alignment pass, run full verify before pushing: + +```bash +npm run typecheck +npm run lint +npm run test:run +npm run format:check +``` + +(`/verify` runs all four.) + +Then a UI smoke on any route main touched: + +```bash +/verify-ui +``` + +## Writing the merge commit + +A merge-main commit should spell out: + +- Which upstream commits came in (short-hash + one-line). +- What conflicts were resolved, with the semantic choice explained. +- Which drift was caught and fixed. +- What was adopted verbatim from main. + +Example (from commit `b810514`): + +> Merge main: resolve early-access Zod conflict, fix /pro import path +> +> Main added commit 8743544 ("feat: add /pro landing + fix OG image +> pre-warm URL"). Conflict resolved in early-access/route.ts: kept this +> branch's Zod shape but adopted main's "at least one contact method" +> semantics via `.refine`. Also updated src/app/pro/layout.tsx to import +> from @/lib/config/site (new folderized path). + +## Related + +- `.claude/commands/realign.md` — slash command that runs this playbook. +- `agent_docs/audit-followup.md` — the canonical "what's left" list; + update when merging in changes that close an open gap or reopen a + closed one. +- `eslint.config.js` — the API-route Zod rule. Main's PRs bypass it if + they opened before it landed. diff --git a/.claude/skills/next-app-router/SKILL.md b/.claude/skills/next-app-router/SKILL.md new file mode 100644 index 00000000..95d614a0 --- /dev/null +++ b/.claude/skills/next-app-router/SKILL.md @@ -0,0 +1,51 @@ +--- +name: next-app-router +description: Use when editing files under src/app/, adding or changing routes, API endpoints under src/app/api/, server actions, middleware (src/middleware.ts), env vars, or the root layout. Covers Next.js 15 App Router conventions including "use client" boundaries and server components. +--- + +# Next.js 15 App Router + +This repo runs Next 15 with the App Router on React 18. Server Components are the default; `"use client"` must be explicit. + +## When to use + +- Adding or editing a route under `src/app/**` +- Adding or editing an API route under `src/app/api/**` +- Editing `src/middleware.ts` or `src/app/layout.tsx` +- Adding an env variable or touching `src/env/server.ts` +- Using or adding a server action under `src/actions/` + +## Key files + +- `src/app/layout.tsx` — root layout, mounts `Providers` +- `src/components/providers.tsx` — wagmi, RainbowKit, TanStack Query, theme +- `src/middleware.ts` — request middleware (auth/redirect logic) +- `src/env/server.ts` — env schema (t3-oss) +- `next.config.mjs` — image domains, env loader via `jiti` + +## Workflow + +1. Decide Server vs Client Component. Default is Server. Add `"use client"` **only** when you need: hooks, state, browser-only APIs, wagmi, event handlers. +2. For API routes: use Route Handlers in `src/app/api//route.ts` with named exports (`GET`, `POST`, …). +3. For server actions: add under `src/actions/`. Must start with `"use server"`. Validate input with Zod. +4. For env access: `import { env } from '@/env/server'`. Do **not** read `process.env` directly. +5. Run `npm run build` after changes to server boundaries or env — catches mistakes the dev server misses. + +## References + +- Server actions: [`server-actions.md`](./server-actions.md) +- Env validation: [`env-validation.md`](./env-validation.md) +- API routes: [`api-routes.md`](./api-routes.md) + +## Guardrails + +- Never import server-only modules (e.g., `pg`, `googleapis`, env server vars) from a `"use client"` file — build will fail at deploy time. +- Server actions must validate input with Zod **before** any side effect. +- Do not put secrets in `NEXT_PUBLIC_*` vars; those ship to the browser. +- When adding a route, check `src/middleware.ts` for matching patterns — your new route may be unexpectedly gated. +- Keep `layout.tsx` lean — it runs on every render of every child route. + +## See also + +- `agent_docs/architecture.md` (full directory map) +- `agent_docs/env-vars.md` diff --git a/.claude/skills/next-app-router/api-routes.md b/.claude/skills/next-app-router/api-routes.md new file mode 100644 index 00000000..c05d295e --- /dev/null +++ b/.claude/skills/next-app-router/api-routes.md @@ -0,0 +1,81 @@ +# API routes + +## Layout + +Routes live under `src/app/api//route.ts`. Each file exports HTTP-method-named async functions. + +Existing endpoints (see `agent_docs/architecture.md` for the full list): + +`analytics`, `barter`, `config`, `cron`, `early-access`, `fast-tx-status`, +`fastswap`, `fastswap-miles`, `feedback`, `fuul`, `gate`, `hyperliquid`, `og`, +`token-price`, `tokens`, `transaction-status`, `user-community-activity`, +`user-onboarding`, `users`, `waitlist`, `whitelist`. + +## Pattern — always use the Zod helpers in `@/lib/api` + +```ts +// src/app/api/example/[id]/route.ts +import { NextRequest, NextResponse } from "next/server" +import { z } from "zod" +import { env } from "@/env/server" +import { parseJson, parseParams, parseSearchParams } from "@/lib/api/parse" +import { walletAddressSchema, txHashSchema } from "@/lib/api/schemas" + +const paramsSchema = z.object({ id: walletAddressSchema }) +const bodySchema = z.object({ txhash: txHashSchema, status: z.enum(["ok", "err"]) }) + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const p = await parseParams(params, paramsSchema) + if (!p.ok) return p.response + + const body = await parseJson(request, bodySchema) + if (!body.ok) return body.response + + // `p.data.id` and `body.data` are fully typed here — p.data.id is already lower-cased. + return NextResponse.json({ ok: true, id: p.data.id }) +} +``` + +### Return shape — discriminated union + +Parse helpers return `{ ok: true; data } | { ok: false; response }`. Narrow +with `if (!parsed.ok) return parsed.response`, then use `parsed.data`. This +shape requires `strictNullChecks: true` (which this repo has since the +`strictNullChecks` flip). + +## Rules + +1. **Validate every caller input with Zod** — body, search params, dynamic + segments. Shared primitives live in `@/lib/api/schemas`; compose + route-specific shapes next to the handler. +2. Return JSON via `NextResponse.json(...)`; set status explicitly. +3. Use `env` from `@/env/server` for secrets — never `process.env`. +4. For responses that should be cached, set `export const revalidate = ` + or use `NextResponse` cache headers. Set `"Cache-Control": "no-store"` for + near-live data (see `api/fastswap-miles/by-address/route.ts`). +5. For streaming or non-JSON responses, use `Response` / `ReadableStream`. +6. Long-running work → background queue, not an API route. Vercel has timeout limits. + +## Cron / scheduled routes + +`src/app/api/cron/` exists for Vercel Cron. Protect with a bearer token from +env — never leave cron endpoints unauthenticated. + +## Error logging + +Use the project analytics helpers in `src/lib/analytics-server.ts` for +server-side events. Don't `console.error` secrets into production logs. + +## Verification + +- `npm run build` catches type errors across the server boundary. +- The `post-edit-build.sh` hook runs build automatically when you edit + `src/app/api/**`, `src/middleware.ts`, `next.config.mjs`, `src/env/**`, + or `src/actions/**`. +- Test the endpoint with `curl` before claiming complete: + `curl -X POST http://localhost:3000/api/ -H 'Content-Type: application/json' -d '{...}'`. +- If the route has a colocated test under `tests/api/.test.ts`, + the `post-edit-test.sh` hook runs it automatically on save. diff --git a/.claude/skills/next-app-router/env-validation.md b/.claude/skills/next-app-router/env-validation.md new file mode 100644 index 00000000..3ac3e13b --- /dev/null +++ b/.claude/skills/next-app-router/env-validation.md @@ -0,0 +1,31 @@ +# Env validation (t3-oss) + +## Where + +`src/env/server.ts` — single source of truth. Uses `@t3-oss/env-nextjs` + Zod. + +## Pattern + +- `server` block — vars only available on server (no `NEXT_PUBLIC_` prefix) +- `client` block — vars available in the browser (must be `NEXT_PUBLIC_` prefixed) +- `runtimeEnv` — explicit mapping (required by Next for client vars to be inlined) + +## Adding a var + +1. Update `.env.example` with a stub value and a one-line comment. +2. Add to the appropriate block in `src/env/server.ts` with a Zod validator (`z.string().min(1)`, `z.string().url()`, etc.). +3. Add to `runtimeEnv` mapping. +4. Import via `import { env } from '@/env/server'`; reference as `env.MY_VAR`. +5. Document in `agent_docs/env-vars.md`. + +## Build-time failure + +The schema is loaded at build time via `jiti` in `next.config.mjs`. A missing required var **fails the build** — by design. Do not loosen the schema to get past a red build. Add the var to the environment instead. + +## Client-safe vars + +Only `NEXT_PUBLIC_*` vars reach the browser. Never put secrets there. Example of a safe client var in this repo: `NEXT_PUBLIC_ALCHEMY_API_KEY` (public-tier Alchemy key, intentional exposure). + +## Skipping validation (don't) + +`SKIP_ENV_VALIDATION=1 npm run build` exists as an escape hatch — never use it in CI or production. If you're tempted to set it to get past an error, the right fix is to add the var or correct the schema. diff --git a/.claude/skills/next-app-router/server-actions.md b/.claude/skills/next-app-router/server-actions.md new file mode 100644 index 00000000..9b80fe4d --- /dev/null +++ b/.claude/skills/next-app-router/server-actions.md @@ -0,0 +1,37 @@ +# Server actions + +## Pattern + +- Files live in `src/actions/`. +- Each file starts with `"use server"` as the first statement. +- Export typed async functions — they become callable from client components. + +Existing example: `src/actions/capture-email.ts`. + +## Rules + +1. **Validate input with Zod** at the top of every action. Never trust client-supplied values. +2. **Return a typed result** — e.g., `{ ok: true } | { ok: false, error: string }`. Do not throw raw errors across the boundary; convert to serializable shapes. +3. **No secrets in return values.** If you caught an error that includes an API key or internal URL, strip it before returning. +4. **Use `env` from `@/env/server`.** Never `process.env`. +5. **Do not return non-serializable values** (Dates serialize; functions, Maps, Sets do not). + +## Calling from a client component + +```tsx +"use client" +import { captureEmail } from "@/actions/capture-email" +// call as a normal async function; Next handles the transport +``` + +## When NOT to use a server action + +Use a Route Handler (`src/app/api/*/route.ts`) when: + +- The endpoint is called by a non-browser client (cron, webhook, external service). +- You need full control over HTTP status codes and headers. +- The input is not JSON / form-data (e.g., binary upload). + +## Verification + +After editing a server action, run `npm run build` — actions have a server/client boundary that `tsc --noEmit` alone won't catch. diff --git a/.claude/skills/skill-creator/SKILL.md b/.claude/skills/skill-creator/SKILL.md new file mode 100644 index 00000000..4a619d13 --- /dev/null +++ b/.claude/skills/skill-creator/SKILL.md @@ -0,0 +1,46 @@ +--- +name: skill-creator +description: Creating or editing a skill in this repo. Use when the user asks to add a new skill, scaffold a skill directory, or restructure an existing skill. Also use when you notice domain knowledge that would be loaded repeatedly across sessions — that's a candidate for a new skill. +--- + +# Skill creator + +A meta-skill for building other skills in this repo. Follow this shape and skills will compose well with the progressive-disclosure model. + +## When to create a new skill + +Create a skill when **all** of these are true: + +1. The knowledge applies only to a **specific kind of task**, not every session. +2. It's longer than ~10 lines — too much to put in CLAUDE.md without bloat. +3. It has a **clear trigger** you can write as a single `description` sentence starting with "Use when…". + +If only (1) and (2) are true, it might belong in `agent_docs/` as a reference file instead (no frontmatter, loaded by link). + +## Anatomy + +See [`anatomy.md`](./anatomy.md). + +## Checklist + +See [`checklist.md`](./checklist.md). + +## Workflow + +1. Pick a directory name: kebab-case, singular, verb-leading if possible (`defi-swap`, `leaderboard-miles`, `testing-vitest`). +2. Create `.claude/skills//SKILL.md` with the frontmatter template in `anatomy.md`. +3. Keep SKILL.md under ~100 lines. Push anything longer to sibling `.md` reference files. +4. Reference existing code with `src/path/file.ts:42`-style citations — never inline. +5. Run through `checklist.md` before finishing. + +## Guardrails + +- **No inline code snippets** from files in `src/` — they rot. Always cite. +- **No duplication** — if a skill overlaps with another, the trigger descriptions must disambiguate, or merge the skills. +- **Description is the trigger** — treat it like a search query. Include domain keywords (e.g., "permit2", "slippage", "show_miles_estimate") that the user is likely to type. +- **Progressive disclosure** — split long content into `reference.md`, `patterns.md`, etc. SKILL.md should tell Claude _when_ to open them. + +## See also + +- The plan: `/Users/jasonschwarz/.claude/plans/inherited-herding-penguin.md` +- `.claude/commands/new-skill.md` diff --git a/.claude/skills/skill-creator/anatomy.md b/.claude/skills/skill-creator/anatomy.md new file mode 100644 index 00000000..2cd753b6 --- /dev/null +++ b/.claude/skills/skill-creator/anatomy.md @@ -0,0 +1,72 @@ +# Skill anatomy + +## Frontmatter template + +```yaml +--- +name: +description: . Include 3-5 keywords the user is likely to type. +--- +``` + +Optional frontmatter fields (Claude Code): + +- `disable-model-invocation: true` — prevents automatic invocation. Useful for destructive workflows that should only fire when the user explicitly invokes the slash command. + +## Body structure + +Follow this order. Skip sections that don't apply. + +``` +# + +## When to use +<1-3 bullets that make the trigger concrete — concrete file paths, features, symbols> + +## Prerequisites / key files + + +## Workflow + + +## Guardrails + + +## See also + +``` + +## Reference files + +Split anything longer than ~30 lines into a sibling `.md`. Naming convention: + +- `patterns.md` — recurring code patterns in the domain +- `.md` — e.g., `permit2.md`, `server-actions.md` +- `anti-patterns.md` — what not to do +- `checklist.md` — step-by-step verification + +Inside SKILL.md, link via relative path: `[permit2](./permit2.md)`. + +## Scripts + +Scripts live in a `scripts/` subdirectory of the skill. They are **executable**, not read-in. Claude invokes them via Bash; the source isn't loaded into context. Use scripts for: + +- Deterministic calculations +- File scaffolding +- Validation passes + +Keep scripts readable — Claude infers intent from filename and a short comment block. + +## Size budget + +- SKILL.md: aim for 60-100 lines. Hard cap ~150. +- Each reference file: aim for under 200 lines. +- Total skill directory: no hard cap, but if it balloons, split into two skills. + +## Frontmatter description anti-patterns + +| Bad | Why | Better | +|---|---|---| +| "Swap helper" | No trigger, no keywords | "Use when editing swap flow, quotes, slippage, permit2, or WETH wrap/unwrap under src/components/swap or src/hooks/use-swap-*" | +| "Best practices for everything" | Too broad — will match every task | Split into narrow skills | +| "This skill will help you when…" | Narrator voice wastes tokens | Direct imperative: "Use when…" | diff --git a/.claude/skills/skill-creator/checklist.md b/.claude/skills/skill-creator/checklist.md new file mode 100644 index 00000000..c162c2a9 --- /dev/null +++ b/.claude/skills/skill-creator/checklist.md @@ -0,0 +1,16 @@ +# Skill checklist + +Before committing a new or edited skill: + +- [ ] Frontmatter has `name` (kebab-case) and `description` (starts with "Use when…") +- [ ] Description mentions concrete files, symbols, or feature names an agent would pattern-match on +- [ ] No code snippets from `src/` inlined — all citations use `src/path/file.ts:42` form +- [ ] SKILL.md under ~150 lines +- [ ] Anything longer split into sibling reference files, linked from SKILL.md +- [ ] "When to use" section is 1-3 bullets, not a paragraph +- [ ] "Guardrails" section captures the top 3 mistakes that domain invites +- [ ] No overlap with another skill's trigger (if overlap, merge or disambiguate descriptions) +- [ ] Verified that `/prime` does **not** auto-load this skill (it shouldn't — skills are Tier 2) +- [ ] If the skill includes scripts, they're in `scripts/` and are executable (`chmod +x`) +- [ ] Added to the skill table in `CLAUDE.md` under "Skills (load when task matches)" +- [ ] Added to the skill list in `.claude/README.md` diff --git a/.claude/skills/testing-vitest/SKILL.md b/.claude/skills/testing-vitest/SKILL.md new file mode 100644 index 00000000..f650883a --- /dev/null +++ b/.claude/skills/testing-vitest/SKILL.md @@ -0,0 +1,65 @@ +--- +name: testing-vitest +description: Use when writing, running, or debugging Vitest tests in this repo. Covers where tests live (top-level tests/ directory mirroring src/), how to mock wagmi/viem, and the test-runner commands that agents should use (test:run, not watch). The post-edit-test.sh hook auto-runs related tests on save. +--- + +# Testing with Vitest + +## When to use + +- Writing a new test +- Debugging a failing test +- Setting up mocks for wagmi / viem / external SDKs +- Tuning `vitest.config.ts` or `tests/utils/` + +## Key files + +- Config: `vitest.config.ts` +- Helpers: `tests/utils/` (check here before writing your own) +- Existing tests: `tests/` (mirrors `src/`) + +## References + +- Layout + naming: [`test-layout.md`](./test-layout.md) +- Mocking web3: [`mocking-web3.md`](./mocking-web3.md) +- Overview: `agent_docs/testing.md` +- Repo convention: `tests/README.md` + +## Workflow + +1. Check `tests/utils/` for an existing helper (renderer, mock factory) before + writing a new one. +2. Mirror the source path: `src/lib/swap/quote-guard.ts` → + `tests/lib/swap/quote-guard.test.ts`. The auto-run hook relies on this + mapping. +3. Name the file `.test.ts` (or `.tsx` for components). +4. Use the `@/` alias for imports from `src/`; use relative paths for + helpers under `tests/utils/`. +5. Prefer `test:run` (single-pass) over `test` (watch) — always from an agent. +6. When a test fails, read the actual error before re-running — flaky re-runs + waste context. + +## Quick commands + +```bash +npm run test:run # all tests, single pass +npm run test:run -- tests/lib/swap/... # a single file or glob +npm run test:coverage # coverage report (v8) +``` + +## Guardrails + +- **Do not use `npm run test` (watch) from an agent.** It never exits. +- **Do not mock modules you don't own** beyond the surface you need — + over-mocking breaks when deps update. +- **Do not inject real API keys** (even in `.env.test.local`). Use placeholder + strings. +- **Do not test framework internals** (wagmi's `useAccount` return shape) — + test your own logic. + +## Verification + +- `npm run test:run` before declaring the change complete. +- For CI-parity, also run `npm run typecheck` — tests can pass while types break. +- The `post-edit-test.sh` hook runs the matching mirror test automatically on + edit; `post-edit-typecheck.sh` runs `tsc --noEmit`. Silent on success. diff --git a/.claude/skills/testing-vitest/mocking-web3.md b/.claude/skills/testing-vitest/mocking-web3.md new file mode 100644 index 00000000..2f53ee52 --- /dev/null +++ b/.claude/skills/testing-vitest/mocking-web3.md @@ -0,0 +1,64 @@ +# Mocking web3 + +Mock at the module boundary. Don't try to spin up a real node or sign real transactions in tests. + +## Mocking wagmi hooks + +```ts +import { vi } from 'vitest' + +vi.mock('wagmi', async () => { + const actual = await vi.importActual('wagmi') + return { + ...actual, + useAccount: () => ({ address: '0xabc...', isConnected: true }), + useReadContract: () => ({ data: 42n, isLoading: false }), + } +}) +``` + +- Preserve non-mocked exports via `...actual` so type exports still work. +- Mock per-test only if different tests need different return values. + +## Mocking viem clients + +```ts +vi.mock('viem', async () => { + const actual = await vi.importActual('viem') + return { + ...actual, + createPublicClient: () => ({ + readContract: vi.fn().mockResolvedValue(42n), + simulateContract: vi.fn().mockResolvedValue({ result: 'ok' }), + }), + } +}) +``` + +## Mocking the wagmi config + +```ts +vi.mock('@/lib/wagmi', () => ({ + wagmiConfig: { /* minimal fake */ }, + chains: [{ id: 1, name: 'mainnet' }], +})) +``` + +## Mocking SDKs (Fuul, Barter, EmailOctopus, Alchemy) + +Mock at the `src/lib/*` wrapper, not the SDK itself. E.g., `vi.mock('@/lib/fuul', () => ({ trackEvent: vi.fn() }))`. This insulates tests from SDK internals. + +## Testing hooks + +Use `renderHook` from `@testing-library/react` (install if not present — check `package.json` first). Wrap with the QueryClient provider when testing TanStack Query hooks; a helper in `src/test/utils/` may already exist. + +## Anti-patterns + +- **Do not** try to hit a live RPC in tests. +- **Do not** mock `fetch` globally — scope it to a test with `vi.spyOn(global, 'fetch')`. +- **Do not** mock `window.ethereum`; wagmi's connectors abstract it away. +- **Do not** fill test addresses with real user addresses; use `0xabc...` placeholders. + +## Bigint gotcha + +Viem returns `bigint` for uint256. `expect(result).toBe(42)` fails; use `expect(result).toBe(42n)`. diff --git a/.claude/skills/testing-vitest/test-layout.md b/.claude/skills/testing-vitest/test-layout.md new file mode 100644 index 00000000..23f0d42b --- /dev/null +++ b/.claude/skills/testing-vitest/test-layout.md @@ -0,0 +1,80 @@ +# Test layout + +## Where tests live + +All tests live at the repo root under `tests/`, mirroring `src/`: + +``` +tests/ +├── api/ <- src/app/api//route.ts +├── components/ <- src/components/** +├── hooks/ <- src/hooks/** +├── lib/ <- src/lib/** +│ ├── api/ +│ ├── settlement/ +│ ├── swap/ +│ └── tokens/ +└── utils/ <- shared helpers (excluded from runner discovery) +``` + +Colocated `src/**/__tests__/*.test.ts` is **no longer used**. If you see +one, move it into `tests//`. + +The `post-edit-test.sh` hook discovers tests via this mirror — saving +`src/lib/swap/quote-guard.ts` auto-runs `tests/lib/swap/quote-guard.test.ts` +if it exists. + +## Naming + +- `.test.ts` for plain logic +- `.test.tsx` for React components + +## Imports + +- Source under test: use the `@/` alias → `import { foo } from "@/lib/swap/quote-guard"` +- Test helpers: use relative paths → `import { fx } from "../utils/mock-next-request"` +- The `@` alias is reserved for production code; keeping helpers relative + prevents a misnamed helper from accidentally shadowing a source file. + +## Structure of a test file + +```ts +import { describe, it, expect, vi, beforeEach } from "vitest" +import { subject } from "@/lib//" + +describe("subject", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it("does X when Y", () => { + expect(subject(inputs)).toBe(expected) + }) +}) +``` + +- One `describe` per module, unless you need sub-grouping. +- `it("does X when Y")` — descriptive, reads like documentation. +- `beforeEach` clears mocks to avoid per-test bleed. + +## Vitest-specific + +- Use `vi.mock("", factory)` at top-level, not inside test bodies. +- Use `vi.fn()` for ad-hoc mocks. +- Use `vi.hoisted()` when you need a mocked symbol referenced by a hoisted + `vi.mock`. + +## Coverage expectations + +No hard threshold today. Aim for: high coverage on `src/lib/` pure utilities; +reasonable coverage on hooks with non-trivial logic; spot coverage on +components. Coverage config in `vitest.config.ts` excludes `src/env/`, +`src/types/`, and `.d.ts` files — these are contracts, not runtime. + +## Do not + +- Don't skip tests (`.skip`, `.todo`) in committed code without a comment + explaining why. +- Don't commit `.only` tests. +- Don't test third-party library internals. +- Don't run watch mode from an agent — use `npm run test:run`. diff --git a/.claude/skills/ui-shadcn/SKILL.md b/.claude/skills/ui-shadcn/SKILL.md new file mode 100644 index 00000000..4c269f4a --- /dev/null +++ b/.claude/skills/ui-shadcn/SKILL.md @@ -0,0 +1,51 @@ +--- +name: ui-shadcn +description: Use when adding or modifying React components under src/components/**, particularly ui/ (shadcn primitives), shared/, or domain folders. Covers Tailwind conventions, Radix composition, shadcn update workflow, and accessibility basics. +--- + +# UI: shadcn + Radix + Tailwind + +Components are organized by domain. `ui/` holds shadcn primitives (generated from `components.json`); everything else composes them. + +## When to use + +- Creating a new component or modifying an existing one +- Adding a new shadcn primitive +- Working on design tokens (Tailwind config + `globals.css`) +- Composing dialogs, popovers, sheets, drawers (mostly Radix) + +## Key files + +- `src/components/ui/` — shadcn primitives (button, dialog, input, etc.) +- `src/components/shared/` — cross-domain reusable components +- Domain folders: `dashboard/`, `swap/`, `claim/`, `onboarding/`, `landing/`, `referral/`, `network-checker/`, `modals/`, `pwa/`, `learn/` +- `components.json` — shadcn config +- `tailwind.config.ts` — design tokens, plugin config +- `src/app/globals.css` — base styles + CSS variables + +## References + +- Component conventions: [`component-conventions.md`](./component-conventions.md) +- Accessibility: [`accessibility.md`](./accessibility.md) + +## Workflow + +1. Check if a shadcn primitive already covers the need in `src/components/ui/`. +2. If yes, compose it in a domain folder — don't modify `ui/` unless the change is design-system-wide. +3. If you need a new shadcn primitive, use the shadcn CLI so `components.json` stays consistent. +4. Use `cn()` from `src/lib/utils.ts` to merge Tailwind classes; avoid string concatenation. +5. For conditional variants, use `class-variance-authority` (already a dep). +6. Use `@radix-ui/*` primitives directly only when shadcn hasn't wrapped them yet; check `src/components/ui/` first. + +## Guardrails + +- Never edit a file in `src/components/ui/` to add domain-specific logic — push it to `shared/` or the domain folder. +- Never import a design token directly from a component — go through Tailwind classes or CSS variables. +- Never add a new UI library (MUI, Chakra, Mantine) — the design system is shadcn/Radix/Tailwind. +- Prefer Radix primitives for a11y (focus trapping, aria, keyboard) over hand-rolled solutions. +- Animations: `motion` and `framer-motion`-style APIs via `motion` package; keep animations subtle on data-dense surfaces. + +## Verification + +- `/verify` +- `npm run dev` — eyeball in the browser at multiple widths. The app is mobile-first; test narrow viewports. diff --git a/.claude/skills/ui-shadcn/accessibility.md b/.claude/skills/ui-shadcn/accessibility.md new file mode 100644 index 00000000..c3e4140e --- /dev/null +++ b/.claude/skills/ui-shadcn/accessibility.md @@ -0,0 +1,37 @@ +# Accessibility + +Radix primitives handle a lot — use them. Don't roll your own focus trap, aria attributes, or keyboard navigation when Radix covers the case. + +## Defaults that Radix handles for you + +- Focus trapping in dialogs +- Escape-to-close +- aria-labelledby / aria-describedby wiring +- Keyboard navigation (arrow keys in menus, Enter/Space for triggers) +- Portal-mounted overlays with correct z-index + +## What you still need to do + +- **Alt text** on every ``. Decorative → `alt=""`. +- **Label** on every form input. Use `ui/label.tsx` + `htmlFor`/`id`. +- **Focus visibility** — ensure Tailwind's `focus-visible:` utilities are present. shadcn defaults do this; preserve them. +- **Color contrast** — 4.5:1 minimum for text, 3:1 for UI chrome. +- **Keyboard-only test** — can you complete the swap flow without a mouse? If not, that's a bug. +- **Motion-respecting** — wrap big animations in `@media (prefers-reduced-motion: reduce)` or use `motion`'s reduced-motion support. + +## Wallet UX is often inaccessible + +- RainbowKit's default modals are accessible — don't skin them into keyboard-traps. +- Custom network-install flows (`network-checker/`) must announce their state changes. Consider `aria-live="polite"` on status text. + +## Testing + +- Axe DevTools in the browser catches most issues; not automated here but worth a pass on a new page. +- Manual: Tab through the page. Focus must be visible at every stop. + +## Don'ts + +- Don't remove `:focus-visible` styles. +- Don't set `tabIndex="-1"` on interactive elements. +- Don't use `div` + `onClick` for a button. Use `