From 92cec4bfdbb67824e7b1ad7c2dd89c944708a522 Mon Sep 17 00:00:00 2001 From: gastonfoncea Date: Mon, 18 May 2026 17:52:08 -0300 Subject: [PATCH 01/21] docs(claude): require feature branches for all Linear-tracked work Linear issues (spec, plan, code, migrations, everything tied to the issue) must live on the issue's feature branch and reach main only via a merged PR. Never commit issue work directly to main. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 09df858..2952fd8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,3 +49,16 @@ This is a **pnpm workspace**, not npm. Use `pnpm` and `pnpm --filter @ghbounty/< ## Linear issues Issues are tracked in Linear. The repo branch naming convention is `/` (e.g. `gastonfoncea09/ghb-188-mcp-frontend-onboarding`). Commit messages should reference the Linear issue (`— GHB-188`). + +### Linear-tracked work always goes on its own branch + +**Never commit directly to `main`** (or any base branch) when implementing a Linear issue. This applies to **everything** tied to the issue: spec docs, implementation plans, code, migrations, tests, runbooks. All of it lives on the feature branch and reaches `main` only via a merged PR. + +Workflow when starting a Linear issue: + +1. Move the Linear issue to **In Progress** (and re-assign to yourself if needed). +2. Create the feature branch using the Linear-provided `gitBranchName` (Linear shows it on the issue page). `git checkout -b `. +3. All commits for the issue go on that branch — including the spec/plan docs in `docs/superpowers/`. +4. Open a PR when ready. Merge only after review + CI green. + +The only commits that may land on `main` directly are repo-wide chores not tied to a Linear issue (workflow docs, root README typos, etc.) — and even then, prefer a branch + PR when in doubt. From 3d95f9a621aad97a3e8cc7dad972f3ef07fc2f8e Mon Sep 17 00:00:00 2001 From: gastonfoncea Date: Mon, 18 May 2026 17:49:38 -0300 Subject: [PATCH 02/21] =?UTF-8?q?docs(specs):=20MCP=20Sprint=20B=20on-chai?= =?UTF-8?q?n=20tools=20design=20=E2=80=94=20GHB-187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design spec para Sprint B: submit_pr + submissions.list + hard role gating + fix GHB-182. Resuelve decisión central de signing model via Privy delegated server-signing (Opción A). Defense-in-depth para PR ownership (MCP pre-check + relayer post-check). Co-Authored-By: Claude Opus 4.7 (1M context) --- ...05-18-mcp-sprint-b-onchain-tools-design.md | 364 ++++++++++++++++++ 1 file changed, 364 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-18-mcp-sprint-b-onchain-tools-design.md diff --git a/docs/superpowers/specs/2026-05-18-mcp-sprint-b-onchain-tools-design.md b/docs/superpowers/specs/2026-05-18-mcp-sprint-b-onchain-tools-design.md new file mode 100644 index 0000000..e7e3ec9 --- /dev/null +++ b/docs/superpowers/specs/2026-05-18-mcp-sprint-b-onchain-tools-design.md @@ -0,0 +1,364 @@ +# MCP Sprint B — On-chain tools (submit_pr) — design + +**Status:** Spec (approved 2026-05-18, ready for implementation plan) +**Owner:** Gaston +**Created:** 2026-05-18 +**Predecessor:** `2026-05-12-mcp-sprint-b-onchain-tools-outline.md` (outline only; superseded by this spec) +**Linear:** GHB-187 (this sprint), GHB-114 (`submit_pr`), GHB-182 (PR ownership bug, fixed here) + +--- + +## TL;DR + +Sprint B agrega la pieza que faltaba para que el MCP sea write-capable: la tool `submit_pr`. El agente AI puede ahora encontrar un bounty, resolverlo, y submitearlo on-chain en una sola call — sin browser ni intervención humana per-action. También cerramos GHB-182 (PR ownership bug) con defensa en profundidad y endurecemos el role gating de las tools on-chain. + +Scope mínimo (~7 días): +1. `submissions.create` (alias `submit_pr`) — dev-only +2. `submissions.list` — dev-only +3. Hard role gating en todas las tools on-chain +4. Privy delegated server-signing para Solana +5. UX de consent en `/app/credentials` +6. Fix GHB-182: pre-check en MCP + post-check en relayer + +Lo que NO entra: tools del lado company (create_bounty, resolve_bounty, etc.), on-chain ownership check, hash commit del Opus report. + +--- + +## Goals & non-goals + +**Goals** +- Un agente AI con API key + wallet delegated puede submitear PRs autónomamente. +- El user mantiene control: consent explícito al delegar, revoke con un click. +- Defense in depth contra el bug de PR ownership (GHB-182). +- Mantener el modelo de datos actual (`submissions.solver === profile.wallet_pubkey`); no introducir agent wallets dedicadas. + +**Non-goals** +- Tools company-side. Salen en otro sprint. +- Soporte para chains no-Solana. Se sigue asumiendo Solana exclusivo. +- Refactor on-chain del programa Anchor (ownership check on-chain, redeploy con ix de stake). Tracked en GHB-195. +- Commit on-chain del `opus_report_hash` post-scoring. Continúa con el patrón actual de ceros. +- Webhooks (push) para status updates. Se mantiene polling vía `submissions.get`. + +--- + +## Decisiones tomadas durante el brainstorming + +1. **Signing model: Privy delegated server-signing** (Opción A). Elegida sobre: + - B (agent wallet dedicada): cambiaría el data model y rompería authz existente. + - C (two-step client signing): inviable para agentes AI sin wallet nativo (Claude Code, Cursor). + - D (permits on-chain): requiere redeploy del programa, fuera de scope. + - "Browser handoff per submit": derrota el propósito de tener un agente. Confirmado como anti-pattern (ver `project_agent_autonomy_principle` en memory). + +2. **GHB-182 fix: 2 + 3 (MCP pre-check + relayer post-check)**. No tocamos el programa Anchor. + +3. **`opus_report_hash` queda en ceros** (mismo patrón que la web app). + +4. **`submit_pr` idempotente** por `(user_id, bounty_id, pr_url)` — reintento del agente no crea duplicados. + +5. **Consent UI vive en `/app/credentials`**, no en pantalla separada. Mismo flow de onboarding. + +--- + +## Arquitectura + +``` +┌──────────────┐ API key ┌─────────────────┐ Privy server SDK ┌──────────┐ +│ AI agent │────────────────▶│ apps/mcp │─────────────────────▶│ Privy │ +│ (Claude Code,│ submit_pr │ + role gating │ signTransaction │ (Solana) │ +│ Cursor...) │ │ + GHB-182 ckpt │ └──────────┘ +└──────────────┘ └────────┬────────┘ │ + │ │ + │ /api/gas-station/sponsor │ + ▼ ▼ + ┌─────────────────┐ ┌──────────────┐ + │ gas station │───────────────────▶│ Solana RPC │ + │ validator+signer│ signed tx │ (devnet) │ + └─────────────────┘ └──────────────┘ + │ + │ on-chain + ▼ + ┌──────────────┐ + │ relayer │ + │ + GHB-182 │ + │ post-check │ + └──────────────┘ +``` + +**Resumen del flujo:** el agente llama `submit_pr` con su API key. El MCP valida (rol dev + wallet delegated + ownership de PR via GitHub). Arma la tx. Pide a Privy server SDK la firma como solver (en nombre del user). Pasa al gas station para fee payer signing + submit. Devuelve `submission_id` al agente. El relayer la ve on-chain y revalida ownership antes de scorear. + +--- + +## Componentes por paquete + +### `apps/mcp/` + +Archivos nuevos: +- `lib/tools/submissions/list.ts` — tool `submissions.list`. Dev-only. Devuelve submissions del solver (paginación cursor-based, fields: `id, bounty_id, pr_url, state, score, score_source, rank, created_at`). +- `lib/tools/submissions/create.ts` — tool `submissions.create` (a.k.a. `submit_pr`). Dev-only. Orquesta validation → tx build → Privy signing → gas station submit. +- `lib/tools/role-guard.ts` — helper `requireRole(profile, "dev" | "company")` que tira `Forbidden` si no matchea. Aplicado en cada tool on-chain. +- `lib/tools/delegation-guard.ts` — helper `requireWalletDelegated(userId)` que consulta `agent_delegations` y tira `Forbidden` si no. +- `lib/github/verify-pr-ownership.ts` — wrapper de GitHub REST API. Inputs: `pr_url`, expected `github_handle`, expected `repo`. Outputs: `{ ok: true } | { ok: false, reason }`. +- `lib/privy/delegated-signer.ts` — wrapper del Privy server SDK. Método: `signSolanaTransaction(userId, txBytes) → signedTxBytes`. Maneja errores (delegación revocada off-band, Privy down, etc.). +- `lib/solana/build-submit-solution-tx.ts` — replica server-side de `frontend/lib/solana.ts:buildSubmitSolutionIx` (fetch submission_count, derive PDA, build ix, wrap en VersionedTransaction con fee payer = gas station). + +Cambios a archivos existentes: +- `lib/tools/register.ts` — registrar `submissions.list` y `submissions.create`. +- `lib/tools/bounties/list.ts`, `lib/tools/bounties/get.ts` — **sin cambios de rol**. Listar y leer bounties es info pública; el gating duro va en las tools que ejecutan acciones on-chain (`submissions.create`, futuras tools company), no en las read-only. `bounties.get` ya tiene el comportamiento de "agregar `my_submission` solo si role === dev", que se mantiene. +- `lib/tools/submissions/get.ts` — sin cambios funcionales; agregar tests para los nuevos paths. + +### `frontend/` + +> ⚠️ Nota del proyecto: `frontend/AGENTS.md` advierte que esta versión de Next.js tiene breaking changes vs lo que un agente AI puede tener en su training data. Leer la doc relevante en `node_modules/next/dist/docs/` antes de tocar código del frontend. + +- `app/app/credentials/` — agregar sección **"Authorize agent to act on-chain"**. + - Estado UI: `not_authorized` / `authorized` / `revoking`. + - Botón "Authorize" llama `useHeadlessDelegatedActions.delegateWallet({ address, chainType: 'solana' })`. + - Botón "Revoke authorization" llama `revokeWallets()`. + - Persistir el estado local en `agent_delegations` table vía API call al `/api/agent-delegation/upsert` (nuevo endpoint server-side) que reciba la confirmación del frontend y escriba el row. + - Copy del consent screen (ver sección "UX copy" más abajo). +- `app/api/agent-delegation/` — nuevo endpoint Next.js para syncear el estado a la DB. + +### `relayer/` + +- `src/submission-handler.ts` — antes de scorear, ejecutar `verify-pr-ownership` (lib compartida). + - Si falla: `UPDATE submissions SET state='auto_rejected'`, skip scoring, log estructurado con razón. + - Si pasa: continuar con el flow actual (Opus → ranking → evaluations row). +- Compartir el código de verify con MCP via `packages/shared/src/github/verify-pr-ownership.ts`. + +### `packages/db/` + +Nueva tabla `agent_delegations`: + +```sql +CREATE TABLE agent_delegations ( + user_id text PRIMARY KEY REFERENCES profiles(user_id) ON DELETE CASCADE, + wallet_pubkey text NOT NULL, + chain_type text NOT NULL, -- 'solana' por ahora + delegated_at timestamptz NOT NULL DEFAULT now(), + revoked_at timestamptz NULL, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); + +CREATE INDEX idx_agent_delegations_active + ON agent_delegations (user_id) WHERE revoked_at IS NULL; +``` + +Implementación vía Drizzle (editar `packages/db/src/schema.ts` + `pnpm db:generate` + commit del SQL + Gaston aplica con `pnpm db:migrate`). NO sql ad-hoc en Studio. + +RLS: habilitada con policy "user lee su propio row". El MCP server escribe vía `supabaseAdmin()` (service role, bypassea RLS), igual que el resto del schema. La policy importa para clientes con session token (frontend); el MCP no la atraviesa. + +### `packages/shared/` + +- `src/github/verify-pr-ownership.ts` — lib compartida MCP + relayer. Cero deps de Supabase o Privy (puro). + - Input: `{ pr_url: string, expected_github_handle: string, expected_repo_url: string }` + - Output: `{ ok: true } | { ok: false, reason: 'pr_not_found' | 'author_mismatch' | 'repo_mismatch' | 'rate_limited' }` + - Usa `fetch` contra `https://api.github.com/repos/{owner}/{repo}/pulls/{number}` con token de servicio (no del user — esto es read-only para info pública). + +--- + +## Data flow — submit_pr happy path + +``` +1. AI agent → POST https://mcp.ghbounty.com/api/mcp/mcp + header: Authorization: Bearer ghbk_live_xxx + body: { jsonrpc: "2.0", id: 1, method: "tools/call", + params: { name: "submissions.create", + arguments: { bounty_id: "uuid", pr_url: "https://github.com/x/y/pull/123" } } } + +2. apps/mcp/lib/tools/submissions/create.ts + ├─ authenticate(authorization) → profile { user_id, role, wallet_pubkey, github_handle, mcp_status } + ├─ requireRole(profile, "dev") → 403 si role === "company" + ├─ requireMcpStatus(profile, "active") → 403 si suspended/revoked/pending_* + ├─ requireWalletDelegated(user_id) → 403 si no hay row activa en agent_delegations + ├─ idempotency check: + │ SELECT id, state FROM submissions + │ WHERE solver = profile.wallet_pubkey + │ AND issue_pda = (SELECT pda FROM issues WHERE id = $bounty_id) + │ AND pr_url = $pr_url + │ LIMIT 1 + │ ↳ si existe: devolver { submission_id, status: state }. Fin. + ├─ load bounty: SELECT id, pda, github_issue_url, state, chain_id FROM issues WHERE id = $bounty_id + │ ↳ 404 si no existe; 409 si state ≠ 'open' + │ parsear repo del bounty: github_issue_url → { owner, repo, issue_number } + ├─ verify-pr-ownership({ + │ pr_url, + │ expected_github_handle: profile.github_handle, + │ expected_repo_url: `https://github.com/${owner}/${repo}` + │ }) + │ ↳ ok: continue. fail: devolver 403 con la razón. + ├─ fetch on-chain bounty: program.account.bounty.fetch(bounty_pda) → submission_count + ├─ derive submission PDA con [SUBMISSION_SEED, bounty_pda, u32LE(submission_count)] + ├─ build submit_solution ix (pr_url, opus_report_hash = zeros[32]) + ├─ wrap en VersionedTransaction: + │ fee payer: gas station pubkey + │ signers required: gas station (slot 0), user wallet (slot 1) + ├─ serialize → unsigned bytes + ├─ Privy delegated signing: + │ await privyClient.walletApi.solana.signTransaction({ + │ walletId: , + │ transaction: unsignedBytes + │ }) + │ ↳ devuelve tx con slot 1 firmado (el user) + │ ↳ si Privy 403 (delegación revocada off-band): UPDATE agent_delegations SET revoked_at = now(); + │ devolver 403 al agente + ├─ POST /api/gas-station/sponsor con la tx parcial: + │ gas station validator chequea allowlist de discriminators (submit_solution ya está ahí) + │ agrega su firma → submit a Solana RPC + ├─ wait for tx confirmation (timeout 30s): + │ getSignatureStatuses con retry/backoff + │ ↳ si timeout: devolver 202 con { submission_id: null, status: 'pending', tx_signature } + │ el agente puede pollear submissions.get + ├─ insert mirror local en submissions: + │ INSERT INTO submissions (pda, solver, pr_url, opus_report_hash, state, ...) + │ VALUES (...) + │ ON CONFLICT (pda) DO NOTHING + │ ↳ el relayer también lo va a insertar; ON CONFLICT evita la race + └─ devolver { submission_id, status: 'pending', tx_signature, submission_pda } + +3. relayer/src/watcher.ts ve la submission nueva en la cadena + ├─ verify-pr-ownership(pr_url, github_handle_del_solver, bounty.repo) + │ github_handle se obtiene via JOIN: solver_wallet → profiles → github_handle + │ ↳ fail: UPDATE submissions SET state = 'auto_rejected'; log; skip + │ ↳ ok: continue + ├─ scorea con Opus (sin cambios al flow actual) + ├─ INSERT INTO evaluations (...) + └─ UPDATE submissions SET state = 'scored', rank = ..., scored_at = now() + +4. AI agent loopea submissions.get(submission_id) hasta ver state='scored' + ↳ sin cambios — submissions.get ya devuelve score + state. Documentar el polling pattern en el agente skill. +``` + +--- + +## Error handling + edge cases + +| Caso | HTTP/MCP Error | Mensaje al agente | +|---|---|---| +| API key inválida | 401 `Unauthorized` | "Invalid or expired API key" | +| Rol = company | 403 `Forbidden` | "This tool requires `dev` role" | +| mcp_status ≠ active | 403 `Forbidden` | "Account is suspended/revoked. Contact support." | +| User no delegó wallet | 403 `Forbidden` | "Wallet delegation required — visit /app/credentials to authorize." | +| Bounty no existe | 404 `NotFound` | "Bounty not found" | +| Bounty no está open | 409 `Conflict` | "Bounty is `{state}` and not accepting submissions" | +| PR no existe en GitHub (404) | 404 `NotFound` | "PR does not exist on GitHub" | +| PR author ≠ github_handle | 403 `Forbidden` | "PR author does not match your linked GitHub account" | +| PR repo ≠ bounty repo | 403 `Forbidden` | "PR is not against the bounty's target repo" | +| GitHub rate limit | 503 `ServiceUnavailable` | "GitHub rate limit hit, retry in N seconds" | +| Duplicate (idempotent hit) | 200 con `submission_id` existente | (sin error; respuesta normal con el id de la previa) | +| Privy signing fail (delegación revocada) | 403 `Forbidden` | "Wallet delegation revoked — re-authorize at /app/credentials" | +| Privy down | 503 `ServiceUnavailable` | "Signing service unavailable, retry shortly" | +| Gas station rechaza (allowlist miss) | 500 `InternalError` + alert | "Internal validation error" | +| On-chain submission_count race | reintento automático 1 vez | (transparente para el agente) | +| On-chain tx fail (otros) | 500 `InternalError` + log | "On-chain submission failed: " | +| Confirmation timeout | 202 con `tx_signature`, sin `submission_id` | "Tx submitted, confirmation pending — check submissions.get" | +| Relayer detecta mismatch (post-check fail) | (no es error real-time; el agente ve `state=auto_rejected` via submissions.get) | - | + +--- + +## UX copy — consent screen + +En `/app/credentials`, debajo del bloque de API keys: + +> ### Authorize agent to act on-chain +> +> Your AI agent needs permission to sign Solana transactions on your behalf to submit PRs to bounties. Without this, every action would require you to open a browser and confirm — which defeats the point of having an agent. +> +> **What you're authorizing:** +> - GhBounty server can sign `submit_solution` transactions using your wallet (``) +> - This is scoped to the GhBounty escrow program only — we validate every transaction server-side before signing +> +> **What we cannot do:** +> - Transfer your SOL or tokens +> - Withdraw funds from any escrow +> - Sign any transaction outside the `ghbounty_escrow` program +> +> **Revoke any time:** clicking the button below will revoke all server-side signing permissions. Your agent will stop being able to submit PRs until you re-authorize. +> +> `[ Authorize ]` *State: Not authorized* + +Estado post-autorización: + +> ### Agent authorization +> +> ✓ **Authorized** — your agent can submit PRs on your behalf +> +> - Wallet: `` +> - Delegated since: `` +> +> `[ Revoke authorization ]` + +--- + +## Testing + +### Unit (apps/mcp/tests/) +- Un test por handler (`submissions/list.test.ts`, `submissions/create.test.ts`). +- Mocks de: Privy server SDK, Solana RPC, Supabase client, GitHub API, gas station fetch. +- Cubrir cada row de la tabla de error handling. +- Idempotency: misma call dos veces → mismo `submission_id`. + +### Unit (packages/shared/tests/) +- `verify-pr-ownership` — happy path + cada `reason` posible. + +### Unit (relayer/tests/) +- Post-check happy + fail (mismatch debe marcar `auto_rejected`). + +### Integration (apps/mcp/tests/integration/) +- Profile dev de prueba con wallet delegada (Privy test mode si está disponible; si no, mock the Privy server SDK at the integration layer). +- Flow real contra devnet local (validator-test): submit_pr → submission row en DB → simular relayer pickup → evaluations row → submissions.get devuelve scored. + +### Manual smoke test post-deploy +Documentar en `docs/superpowers/runbooks/2026-05-18-sprint-b-smoke.md`: +1. Gaston entra a `/app/credentials`, delega wallet +2. Desde Claude Code conectado a `mcp.ghbounty.com` con la API key de Gaston: + - `bounties.list` → ver bounty test + - `submit_pr({ bounty_id, pr_url })` con un PR real propio contra el repo target + - `submissions.get` polling hasta ver `state='scored'` +3. Validar en Supabase Studio: row en `submissions` con state correcto, row en `evaluations` con score. +4. Validar en Solana Explorer (devnet): cuenta de submission existe en el PDA esperado. + +--- + +## Rollout / migration plan + +Cada paso es revertible. Gaston aplica las migrations manualmente (no CI/Vercel) per CLAUDE.md. + +1. **Mergear migration Drizzle** (`agent_delegations` table). Gaston: `pnpm db:migrate` a devnet (Supabase prod). +2. **Deploy frontend** con UI de delegación. Aún sin tools nuevas — el botón funciona y persiste el row, pero todavía no hay tools que lo lean. +3. **Deploy `packages/shared/src/github/verify-pr-ownership.ts`**. +4. **Deploy relayer** con post-check. Sin efecto real hasta que existan submissions vía MCP. +5. **Deploy MCP** con: `requireRole` aplicado a tools existentes, `submissions.list`, `requireWalletDelegated`, Privy server SDK integrado. Aún sin `submit_pr`. +6. **Smoke test partial**: Gaston delega su wallet, llama `submissions.list` → verifica respuesta. Llama `whoami` → role check. Llama una tool con la API key de un company test → 403. +7. **Deploy MCP con `submit_pr`**. +8. **Smoke test end-to-end** con un PR real (ver sección Testing). +9. **Update** `docsGaso/Engineering/mcp-state.md` + memory `project_mcp_state_2026_05_18.md`. + +--- + +## Open questions / future work + +Cosas que NO entran en Sprint B pero quedan trackeadas: + +- **Privy pricing/SLA**: verificar costo de delegated server-signing en plan actual. Gaston a confirmar con Privy fuera de banda. No bloqueante para implementación pero sí para go-live. +- **On-chain ownership check (GHB-182 v2)**: redeploy del programa con oráculo de GitHub identity. Tracked en GHB-182. Sprint B mitiga off-chain. +- **Webhooks vs polling**: si los agentes terminan poleando muy fuerte, considerar webhook outbound (push) en un sprint futuro. MCP spec no lo cubre nativo aún, requiere convención propia. +- **`opus_report_hash` commit on-chain**: hoy queda en ceros. Si queremos integridad criptográfica del scoring, agregar un ix `set_score` con el hash real. Sprint futuro. +- **Tools company-side**: `bounties.create`, `bounties.resolve`, `bounties.cancel`, `submissions.list` company-flavor. Sprint distinto. +- **Multi-chain**: tracked en GHB-192. + +--- + +## Estimación + +5-7 días de trabajo focused: + +- Día 1: Migration `agent_delegations` + lib `verify-pr-ownership` (compartida). +- Día 2: Frontend `/app/credentials` con consent screen + endpoint sync. +- Día 3: MCP `submissions.list` + role guards + delegation guard + Privy server SDK wrapper. +- Día 4: MCP `submissions.create` (build tx + Privy signing + gas station + idempotency). +- Día 5: Relayer post-check + tests cross-paquete. +- Día 6: Integration tests + smoke test partial. +- Día 7: Smoke test end-to-end + docs + state update. + +Buffer ya incluido. From 1703e52980435bf441179b8042c2072de61f996d Mon Sep 17 00:00:00 2001 From: gastonfoncea Date: Mon, 18 May 2026 18:00:38 -0300 Subject: [PATCH 03/21] =?UTF-8?q?docs(plans):=20MCP=20Sprint=20B=20impleme?= =?UTF-8?q?ntation=20plan=20=E2=80=94=20GHB-187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TDD-style bite-sized tasks for: agent_delegations migration, verifyPrOwnership shared lib, role/delegation guards, Privy delegated-signer, submit_solution tx builder, submissions.list + submissions.create MCP tools, frontend consent UI, relayer post-check, smoke runbook, and PR handoff. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-05-18-mcp-sprint-b-onchain-tools.md | 2296 +++++++++++++++++ 1 file changed, 2296 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-18-mcp-sprint-b-onchain-tools.md diff --git a/docs/superpowers/plans/2026-05-18-mcp-sprint-b-onchain-tools.md b/docs/superpowers/plans/2026-05-18-mcp-sprint-b-onchain-tools.md new file mode 100644 index 0000000..6c473e1 --- /dev/null +++ b/docs/superpowers/plans/2026-05-18-mcp-sprint-b-onchain-tools.md @@ -0,0 +1,2296 @@ +# MCP Sprint B — On-chain tools Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Wire `submissions.create` (a.k.a. `submit_pr`) into the MCP server so AI agents can submit PRs on-chain autonomously via Privy delegated signing, while closing GHB-182 (PR ownership) with defense in depth. + +**Architecture:** New MCP write tool builds a `submit_solution` Solana transaction, asks Privy server SDK to sign as the user (using a delegated session), passes the partially-signed tx to the existing gas-station endpoint for fee-payer signing + submit. Pre-check of PR ownership in MCP + post-check in relayer mitigates GHB-182 without redeploying the Anchor program. Frontend `/app/credentials` gets a consent screen for the user to delegate their Privy wallet. + +**Tech Stack:** Next.js (apps/mcp + frontend), `@modelcontextprotocol/sdk`, Privy `@privy-io/server-auth` (NEW, server SDK) and `@privy-io/react-auth` (existing, browser SDK), Drizzle ORM + Supabase, Solana web3.js + Anchor (`frontend/lib/idl/ghbounty_escrow.json`), Vitest, Zod. + +**Spec reference:** `docs/superpowers/specs/2026-05-18-mcp-sprint-b-onchain-tools-design.md` + +--- + +## File structure + +### New files + +| Path | Responsibility | +|---|---| +| `packages/db/drizzle/0026_agent_delegations.sql` | Migration creating `agent_delegations` table | +| `packages/shared/src/github/verify-pr-ownership.ts` | Pure function: fetch PR from GitHub, compare author + repo | +| `packages/shared/tests/github/verify-pr-ownership.test.ts` | Vitest tests for the verifier | +| `apps/mcp/lib/tools/role-guard.ts` | `requireRole(profile, role)` helper | +| `apps/mcp/lib/tools/delegation-guard.ts` | `requireWalletDelegated(userId)` helper (queries `agent_delegations`) | +| `apps/mcp/lib/privy/delegated-signer.ts` | Thin wrapper over `PrivyClient.walletApi.solana.signTransaction` | +| `apps/mcp/lib/solana/build-submit-solution-tx.ts` | Server-side mirror of `frontend/lib/solana.ts:buildSubmitSolutionIx`, packs into VersionedTransaction | +| `apps/mcp/lib/tools/submissions/list.ts` | `submissions.list` MCP tool (dev-only) | +| `apps/mcp/lib/tools/submissions/create.ts` | `submissions.create` MCP tool (dev-only) | +| `apps/mcp/tests/tools/role-guard.test.ts` | Tests for role guard | +| `apps/mcp/tests/tools/delegation-guard.test.ts` | Tests for delegation guard | +| `apps/mcp/tests/tools/submissions/list.test.ts` | Tests for `submissions.list` | +| `apps/mcp/tests/tools/submissions/create.test.ts` | Tests for `submissions.create` (happy + every error row) | +| `frontend/app/app/credentials/AgentDelegationCard.tsx` | UI component for delegation consent | +| `frontend/app/api/agent-delegation/route.ts` | API route to sync delegation state to DB | +| `docs/superpowers/runbooks/2026-05-18-sprint-b-smoke.md` | Manual smoke test runbook | + +### Modified files + +| Path | Change | +|---|---| +| `packages/db/src/schema.ts` | Add `agentDelegations` table definition | +| `packages/db/drizzle/meta/_journal.json` | Append entry for migration 0026 (auto-generated by Drizzle) | +| `packages/shared/src/index.ts` | Export new `verifyPrOwnership` | +| `apps/mcp/package.json` | Add `@privy-io/server-auth` dependency | +| `apps/mcp/lib/tools/register.ts` | Register `submissions.list` + `submissions.create` | +| `frontend/app/app/credentials/page.tsx` | Render `AgentDelegationCard` | +| `relayer/src/submission-handler.ts` | Pre-scoring ownership check using shared `verifyPrOwnership` | +| `relayer/tests/submission-handler.test.ts` | Cover the new check | + +--- + +## Notes for the executor + +- **Branch:** Work on `gastonfoncea09/ghb-187-sprint-b-mcp-on-chain-tools-submit_pr-check_status`. Never commit Linear-tracked work to `main` (per `CLAUDE.md`). +- **Tests are workspace-wide.** Run `pnpm typecheck && pnpm test` before committing — the pre-commit hook does the same; don't bypass it. +- **Migrations:** Edit `packages/db/src/schema.ts`, then `pnpm db:generate`. **Do not apply migrations yourself.** Add a PR note that the SQL is ready for Gaston to apply via `pnpm db:migrate`. +- **Frontend:** `frontend/AGENTS.md` warns this Next.js version has breaking changes vs training data. Before touching frontend code, read the relevant guide in `node_modules/next/dist/docs/`. +- **TDD throughout.** Test → fail → implement → pass → commit. Don't batch. +- **Commit messages:** `(): — GHB-187`. Example: `feat(mcp): add role-guard helper — GHB-187`. + +--- + +## Task 1: Add `agent_delegations` table to Drizzle schema + +**Files:** +- Modify: `packages/db/src/schema.ts` +- Generated by Drizzle: `packages/db/drizzle/0026_agent_delegations.sql`, `packages/db/drizzle/meta/_journal.json`, `packages/db/drizzle/meta/0026_snapshot.json` + +- [ ] **Step 1: Add table definition to schema.ts** + +Open `packages/db/src/schema.ts`. After the `profiles` table definition (around line 205, before `companies`), add: + +```typescript +/* --- Agent delegations: server-side signing consent ------------ */ +export const agentDelegations = pgTable("agent_delegations", { + userId: text("user_id") + .primaryKey() + .references(() => profiles.userId, { onDelete: "cascade" }), + walletPubkey: text("wallet_pubkey").notNull(), + chainType: text("chain_type").notNull(), + delegatedAt: timestamp("delegated_at", { withTimezone: true }) + .default(sql`now()`) + .notNull(), + revokedAt: timestamp("revoked_at", { withTimezone: true }), + createdAt: timestamp("created_at", { withTimezone: true }) + .default(sql`now()`) + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .default(sql`now()`) + .notNull(), +}); +``` + +- [ ] **Step 2: Run Drizzle to generate the migration** + +```bash +pnpm db:generate +``` + +Expected: a new file `packages/db/drizzle/0026_agent_delegations.sql` appears, `meta/_journal.json` gets an entry for index 26, and `meta/0026_snapshot.json` is written. + +- [ ] **Step 3: Review the generated SQL** + +Open `packages/db/drizzle/0026_agent_delegations.sql`. Verify it contains a `CREATE TABLE "agent_delegations"` with the expected columns and FK to `profiles`. The migration should be wrapped in `BEGIN; ... COMMIT;` (Drizzle does this automatically). + +If the FK doesn't appear, the schema is wrong — fix `schema.ts` and regenerate (delete the bad `.sql`, the snapshot, and the journal entry first). + +- [ ] **Step 4: Add the partial index manually** + +Drizzle Kit doesn't generate partial indexes from schema. Append to `packages/db/drizzle/0026_agent_delegations.sql`, **before the final `COMMIT;`**: + +```sql +CREATE INDEX "idx_agent_delegations_active" + ON "agent_delegations" ("user_id") + WHERE "revoked_at" IS NULL; +``` + +- [ ] **Step 5: Add RLS policy by hand** + +Append (still before `COMMIT;`): + +```sql +ALTER TABLE "agent_delegations" ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "agent_delegations_own_read" + ON "agent_delegations" + FOR SELECT + USING (user_id = (SELECT auth.uid()::text)); +``` + +- [ ] **Step 6: Commit** + +```bash +git add packages/db/src/schema.ts packages/db/drizzle/0026_agent_delegations.sql packages/db/drizzle/meta/_journal.json packages/db/drizzle/meta/0026_snapshot.json +git commit -m "feat(db): add agent_delegations table — GHB-187" +``` + +--- + +## Task 2: Add `verify-pr-ownership` to packages/shared + +**Files:** +- Create: `packages/shared/src/github/verify-pr-ownership.ts` +- Create: `packages/shared/tests/github/verify-pr-ownership.test.ts` +- Modify: `packages/shared/src/index.ts` + +- [ ] **Step 1: Write the failing test** + +Create `packages/shared/tests/github/verify-pr-ownership.test.ts`: + +```typescript +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { verifyPrOwnership } from "../../src/github/verify-pr-ownership"; + +describe("verifyPrOwnership", () => { + let fetchMock: ReturnType; + + beforeEach(() => { + fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock); + }); + + it("returns ok when author and repo match", async () => { + fetchMock.mockResolvedValueOnce( + new Response( + JSON.stringify({ + user: { login: "alice" }, + base: { repo: { html_url: "https://github.com/acme/proj" } }, + }), + { status: 200 } + ) + ); + + const result = await verifyPrOwnership({ + prUrl: "https://github.com/acme/proj/pull/42", + expectedGithubHandle: "alice", + expectedRepoUrl: "https://github.com/acme/proj", + }); + + expect(result).toEqual({ ok: true }); + }); + + it("returns author_mismatch when login differs", async () => { + fetchMock.mockResolvedValueOnce( + new Response( + JSON.stringify({ + user: { login: "mallory" }, + base: { repo: { html_url: "https://github.com/acme/proj" } }, + }), + { status: 200 } + ) + ); + + const result = await verifyPrOwnership({ + prUrl: "https://github.com/acme/proj/pull/42", + expectedGithubHandle: "alice", + expectedRepoUrl: "https://github.com/acme/proj", + }); + + expect(result).toEqual({ ok: false, reason: "author_mismatch" }); + }); + + it("returns repo_mismatch when PR is in a different repo", async () => { + fetchMock.mockResolvedValueOnce( + new Response( + JSON.stringify({ + user: { login: "alice" }, + base: { repo: { html_url: "https://github.com/other/repo" } }, + }), + { status: 200 } + ) + ); + + const result = await verifyPrOwnership({ + prUrl: "https://github.com/acme/proj/pull/42", + expectedGithubHandle: "alice", + expectedRepoUrl: "https://github.com/acme/proj", + }); + + expect(result).toEqual({ ok: false, reason: "repo_mismatch" }); + }); + + it("returns pr_not_found on 404", async () => { + fetchMock.mockResolvedValueOnce(new Response("", { status: 404 })); + + const result = await verifyPrOwnership({ + prUrl: "https://github.com/acme/proj/pull/9999", + expectedGithubHandle: "alice", + expectedRepoUrl: "https://github.com/acme/proj", + }); + + expect(result).toEqual({ ok: false, reason: "pr_not_found" }); + }); + + it("returns rate_limited on 403 with rate-limit header", async () => { + fetchMock.mockResolvedValueOnce( + new Response("", { + status: 403, + headers: { "x-ratelimit-remaining": "0" }, + }) + ); + + const result = await verifyPrOwnership({ + prUrl: "https://github.com/acme/proj/pull/42", + expectedGithubHandle: "alice", + expectedRepoUrl: "https://github.com/acme/proj", + }); + + expect(result).toEqual({ ok: false, reason: "rate_limited" }); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +```bash +pnpm --filter @ghbounty/shared test +``` + +Expected: FAIL with "Cannot find module" or compile error — the source file doesn't exist yet. + +- [ ] **Step 3: Implement `verifyPrOwnership`** + +Create `packages/shared/src/github/verify-pr-ownership.ts`: + +```typescript +/** + * Pure function. Calls GitHub REST API to verify that a PR was opened by + * the expected user against the expected repo. Used by both the MCP server + * (pre-check before submit_pr) and the relayer (post-check before scoring). + * + * Token: pass `GITHUB_TOKEN` (server-side env). Public-repo reads work + * unauthenticated but with lower rate limit; provide a PAT for safety. + */ + +export type VerifyPrOwnershipInput = { + prUrl: string; + expectedGithubHandle: string; + /** Full URL like `https://github.com/owner/repo` (no trailing slash). */ + expectedRepoUrl: string; + /** Optional GitHub token for higher rate limit. */ + token?: string; +}; + +export type VerifyPrOwnershipResult = + | { ok: true } + | { + ok: false; + reason: + | "pr_not_found" + | "author_mismatch" + | "repo_mismatch" + | "rate_limited" + | "invalid_url" + | "upstream_error"; + }; + +const PR_URL_RE = + /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/; + +export async function verifyPrOwnership( + input: VerifyPrOwnershipInput +): Promise { + const match = input.prUrl.match(PR_URL_RE); + if (!match) return { ok: false, reason: "invalid_url" }; + + const [, owner, repo, prNumber] = match; + const apiUrl = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`; + + const headers: Record = { + Accept: "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + }; + if (input.token) headers.Authorization = `Bearer ${input.token}`; + + const res = await fetch(apiUrl, { headers }); + + if (res.status === 404) return { ok: false, reason: "pr_not_found" }; + + if (res.status === 403 && res.headers.get("x-ratelimit-remaining") === "0") { + return { ok: false, reason: "rate_limited" }; + } + + if (!res.ok) return { ok: false, reason: "upstream_error" }; + + const body = (await res.json()) as { + user: { login: string }; + base: { repo: { html_url: string } }; + }; + + if (body.user.login.toLowerCase() !== input.expectedGithubHandle.toLowerCase()) { + return { ok: false, reason: "author_mismatch" }; + } + + const normalize = (u: string) => u.replace(/\/$/, "").toLowerCase(); + if (normalize(body.base.repo.html_url) !== normalize(input.expectedRepoUrl)) { + return { ok: false, reason: "repo_mismatch" }; + } + + return { ok: true }; +} +``` + +- [ ] **Step 4: Export from package barrel** + +Open `packages/shared/src/index.ts` and add: + +```typescript +export { verifyPrOwnership } from "./github/verify-pr-ownership"; +export type { + VerifyPrOwnershipInput, + VerifyPrOwnershipResult, +} from "./github/verify-pr-ownership"; +``` + +- [ ] **Step 5: Run tests, expect pass** + +```bash +pnpm --filter @ghbounty/shared test +``` + +Expected: 5 tests pass. + +- [ ] **Step 6: Commit** + +```bash +git add packages/shared/src/github/verify-pr-ownership.ts packages/shared/tests/github/verify-pr-ownership.test.ts packages/shared/src/index.ts +git commit -m "feat(shared): add verifyPrOwnership helper — GHB-187" +``` + +--- + +## Task 3: Add `@privy-io/server-auth` dependency + +**Files:** +- Modify: `apps/mcp/package.json` + +- [ ] **Step 1: Install the SDK** + +```bash +pnpm --filter @ghbounty/mcp add @privy-io/server-auth +``` + +Expected: `apps/mcp/package.json` gets a new entry in `dependencies` and the lockfile updates. + +- [ ] **Step 2: Verify install** + +```bash +pnpm --filter @ghbounty/mcp typecheck +``` + +Expected: no errors. The package is unused at this point — that's fine. + +- [ ] **Step 3: Commit** + +```bash +git add apps/mcp/package.json pnpm-lock.yaml +git commit -m "chore(mcp): add @privy-io/server-auth dependency — GHB-187" +``` + +--- + +## Task 4: Implement `role-guard` helper + +**Files:** +- Create: `apps/mcp/lib/tools/role-guard.ts` +- Create: `apps/mcp/tests/tools/role-guard.test.ts` + +- [ ] **Step 1: Write the failing test** + +Create `apps/mcp/tests/tools/role-guard.test.ts`: + +```typescript +import { describe, it, expect } from "vitest"; +import { requireRole } from "@/lib/tools/role-guard"; +import type { MCPProfile } from "@/lib/tools/types"; + +const baseProfile: MCPProfile = { + user_id: "did:privy:abc", + role: "dev", + mcp_status: "active", + wallet_pubkey: "Wallet111", + github_handle: "alice", +}; + +describe("requireRole", () => { + it("returns ok when role matches", () => { + expect(requireRole(baseProfile, "dev")).toEqual({ ok: true }); + }); + + it("returns Forbidden when role mismatches", () => { + const result = requireRole({ ...baseProfile, role: "company" }, "dev"); + expect(result).toEqual({ + ok: false, + error: { + code: "Forbidden", + message: "This tool requires `dev` role.", + }, + }); + }); +}); +``` + +- [ ] **Step 2: Run test, expect fail** + +```bash +pnpm --filter @ghbounty/mcp test tests/tools/role-guard.test.ts +``` + +Expected: FAIL (module not found). + +- [ ] **Step 3: Implement** + +Create `apps/mcp/lib/tools/role-guard.ts`: + +```typescript +import type { MCPProfile } from "./types"; + +export type GuardResult = + | { ok: true } + | { ok: false; error: { code: "Forbidden"; message: string } }; + +export function requireRole( + profile: MCPProfile, + expected: "dev" | "company" +): GuardResult { + if (profile.role === expected) return { ok: true }; + return { + ok: false, + error: { + code: "Forbidden", + message: `This tool requires \`${expected}\` role.`, + }, + }; +} +``` + +- [ ] **Step 4: Run test, expect pass** + +```bash +pnpm --filter @ghbounty/mcp test tests/tools/role-guard.test.ts +``` + +Expected: 2 tests pass. + +- [ ] **Step 5: Commit** + +```bash +git add apps/mcp/lib/tools/role-guard.ts apps/mcp/tests/tools/role-guard.test.ts +git commit -m "feat(mcp): add requireRole guard — GHB-187" +``` + +--- + +## Task 5: Implement `delegation-guard` helper + +**Files:** +- Create: `apps/mcp/lib/tools/delegation-guard.ts` +- Create: `apps/mcp/tests/tools/delegation-guard.test.ts` + +- [ ] **Step 1: Write failing test** + +Create `apps/mcp/tests/tools/delegation-guard.test.ts`: + +```typescript +import { describe, it, expect, vi } from "vitest"; +import { requireWalletDelegated } from "@/lib/tools/delegation-guard"; + +const makeSupabase = (row: { revoked_at: string | null } | null) => + ({ + from: () => ({ + select: () => ({ + eq: () => ({ + maybeSingle: () => Promise.resolve({ data: row, error: null }), + }), + }), + }), + }) as any; + +describe("requireWalletDelegated", () => { + it("returns ok when an active delegation exists", async () => { + const supabase = makeSupabase({ revoked_at: null }); + const result = await requireWalletDelegated(supabase, "did:privy:abc"); + expect(result).toEqual({ ok: true }); + }); + + it("returns Forbidden when no row exists", async () => { + const supabase = makeSupabase(null); + const result = await requireWalletDelegated(supabase, "did:privy:abc"); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.code).toBe("Forbidden"); + expect(result.error.message).toMatch(/delegation required/i); + } + }); + + it("returns Forbidden when the delegation was revoked", async () => { + const supabase = makeSupabase({ revoked_at: "2026-05-18T00:00:00Z" }); + const result = await requireWalletDelegated(supabase, "did:privy:abc"); + expect(result.ok).toBe(false); + }); +}); +``` + +- [ ] **Step 2: Run test, expect fail** + +```bash +pnpm --filter @ghbounty/mcp test tests/tools/delegation-guard.test.ts +``` + +Expected: FAIL. + +- [ ] **Step 3: Implement** + +Create `apps/mcp/lib/tools/delegation-guard.ts`: + +```typescript +import type { SupabaseClient } from "@supabase/supabase-js"; +import type { GuardResult } from "./role-guard"; + +export async function requireWalletDelegated( + supabase: SupabaseClient, + userId: string +): Promise { + const { data, error } = await supabase + .from("agent_delegations") + .select("revoked_at") + .eq("user_id", userId) + .maybeSingle(); + + if (error) { + return { + ok: false, + error: { code: "Forbidden", message: "Delegation check failed." }, + }; + } + + if (!data || data.revoked_at !== null) { + return { + ok: false, + error: { + code: "Forbidden", + message: + "Wallet delegation required — visit /app/credentials to authorize.", + }, + }; + } + + return { ok: true }; +} +``` + +- [ ] **Step 4: Run test, expect pass** + +```bash +pnpm --filter @ghbounty/mcp test tests/tools/delegation-guard.test.ts +``` + +Expected: 3 tests pass. + +- [ ] **Step 5: Commit** + +```bash +git add apps/mcp/lib/tools/delegation-guard.ts apps/mcp/tests/tools/delegation-guard.test.ts +git commit -m "feat(mcp): add requireWalletDelegated guard — GHB-187" +``` + +--- + +## Task 6: Implement Privy `delegated-signer` wrapper + +**Files:** +- Create: `apps/mcp/lib/privy/delegated-signer.ts` +- Create: `apps/mcp/tests/privy/delegated-signer.test.ts` + +- [ ] **Step 1: Write failing test** + +Create `apps/mcp/tests/privy/delegated-signer.test.ts`: + +```typescript +import { describe, it, expect, vi } from "vitest"; +import { signSolanaTransaction } from "@/lib/privy/delegated-signer"; + +describe("signSolanaTransaction", () => { + it("returns signed bytes when Privy accepts", async () => { + const fakeClient = { + walletApi: { + solana: { + signTransaction: vi.fn().mockResolvedValue({ + signedTransaction: new Uint8Array([1, 2, 3]), + }), + }, + }, + }; + const result = await signSolanaTransaction(fakeClient as any, { + walletId: "wallet-xyz", + unsignedTx: new Uint8Array([0]), + }); + expect(result).toEqual({ ok: true, signedTx: new Uint8Array([1, 2, 3]) }); + expect(fakeClient.walletApi.solana.signTransaction).toHaveBeenCalledWith({ + walletId: "wallet-xyz", + transaction: new Uint8Array([0]), + }); + }); + + it("returns delegation_revoked on 403 from Privy", async () => { + const err = Object.assign(new Error("forbidden"), { status: 403 }); + const fakeClient = { + walletApi: { + solana: { signTransaction: vi.fn().mockRejectedValue(err) }, + }, + }; + const result = await signSolanaTransaction(fakeClient as any, { + walletId: "wallet-xyz", + unsignedTx: new Uint8Array([0]), + }); + expect(result).toEqual({ ok: false, reason: "delegation_revoked" }); + }); + + it("returns upstream_error on other failures", async () => { + const fakeClient = { + walletApi: { + solana: { + signTransaction: vi.fn().mockRejectedValue(new Error("network")), + }, + }, + }; + const result = await signSolanaTransaction(fakeClient as any, { + walletId: "wallet-xyz", + unsignedTx: new Uint8Array([0]), + }); + expect(result).toEqual({ ok: false, reason: "upstream_error" }); + }); +}); +``` + +- [ ] **Step 2: Run, expect fail** + +```bash +pnpm --filter @ghbounty/mcp test tests/privy/delegated-signer.test.ts +``` + +- [ ] **Step 3: Implement** + +Create `apps/mcp/lib/privy/delegated-signer.ts`: + +```typescript +import type { PrivyClient } from "@privy-io/server-auth"; + +export type SignInput = { + walletId: string; + unsignedTx: Uint8Array; +}; + +export type SignResult = + | { ok: true; signedTx: Uint8Array } + | { ok: false; reason: "delegation_revoked" | "upstream_error" }; + +export async function signSolanaTransaction( + client: PrivyClient, + input: SignInput +): Promise { + try { + const { signedTransaction } = await client.walletApi.solana.signTransaction({ + walletId: input.walletId, + transaction: input.unsignedTx, + }); + return { ok: true, signedTx: signedTransaction }; + } catch (err: any) { + if (err?.status === 403) { + return { ok: false, reason: "delegation_revoked" }; + } + return { ok: false, reason: "upstream_error" }; + } +} + +let cachedClient: PrivyClient | null = null; + +export function getPrivyServerClient(): PrivyClient { + if (cachedClient) return cachedClient; + + // Lazy import keeps the type-only consumers (e.g. tests) clean. + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { PrivyClient } = require("@privy-io/server-auth"); + const appId = process.env.PRIVY_APP_ID; + const appSecret = process.env.PRIVY_APP_SECRET; + if (!appId || !appSecret) { + throw new Error("PRIVY_APP_ID / PRIVY_APP_SECRET must be set"); + } + cachedClient = new PrivyClient(appId, appSecret); + return cachedClient!; +} +``` + +- [ ] **Step 4: Run, expect pass** + +```bash +pnpm --filter @ghbounty/mcp test tests/privy/delegated-signer.test.ts +``` + +Expected: 3 tests pass. + +- [ ] **Step 5: Commit** + +```bash +git add apps/mcp/lib/privy/delegated-signer.ts apps/mcp/tests/privy/delegated-signer.test.ts +git commit -m "feat(mcp): add Privy delegated-signer wrapper — GHB-187" +``` + +--- + +## Task 7: Implement `build-submit-solution-tx` helper + +**Files:** +- Create: `apps/mcp/lib/solana/build-submit-solution-tx.ts` +- Create: `apps/mcp/tests/solana/build-submit-solution-tx.test.ts` + +This mirrors `frontend/lib/solana.ts:buildSubmitSolutionIx` but stays server-side and produces a `VersionedTransaction` ready for Privy signing. + +- [ ] **Step 1: Inspect the frontend helper for reference** + +Read `frontend/lib/solana.ts:253-410` (the `submit_solution helpers` block). Note the PDA derivation, the `fetchBountySubmissionCount` pattern, and the accounts wiring. The server-side version reuses the same IDL (`frontend/lib/idl/ghbounty_escrow.json`). + +- [ ] **Step 2: Write failing test** + +Create `apps/mcp/tests/solana/build-submit-solution-tx.test.ts`: + +```typescript +import { describe, it, expect, vi } from "vitest"; +import { buildSubmitSolutionTx } from "@/lib/solana/build-submit-solution-tx"; + +describe("buildSubmitSolutionTx", () => { + it("rejects pr_url longer than 200 chars", async () => { + await expect( + buildSubmitSolutionTx({ + rpcUrl: "https://example.invalid", + bountyPda: "BountyPda111", + solver: "Solver111", + gasStationPubkey: "Gas111", + prUrl: "x".repeat(201), + submissionCount: 0, + blockhash: "BlockHash111", + }) + ).rejects.toThrow(/pr_url too long/); + }); + + it("packs ix into a v0 VersionedTransaction with two signature slots", async () => { + const result = await buildSubmitSolutionTx({ + rpcUrl: "https://example.invalid", + bountyPda: "Bo1111111111111111111111111111111111111111", + solver: "So1111111111111111111111111111111111111111", + gasStationPubkey: "Ga1111111111111111111111111111111111111111", + prUrl: "https://github.com/x/y/pull/1", + submissionCount: 0, + blockhash: "BlockHash11111111111111111111111111111111", + }); + + expect(result.unsignedTx).toBeInstanceOf(Uint8Array); + expect(result.submissionPda).toBeDefined(); + expect(result.submissionIndex).toBe(0); + }); +}); +``` + +> **Note:** the second test passes synthetic base58 strings that *don't* need to round-trip through a real RPC. Avoid creating a real connection in unit tests — the helper takes pre-fetched `submissionCount` + `blockhash` as inputs precisely so it stays pure-ish. + +- [ ] **Step 3: Implement** + +Create `apps/mcp/lib/solana/build-submit-solution-tx.ts`: + +```typescript +import { + Connection, + PublicKey, + TransactionInstruction, + TransactionMessage, + VersionedTransaction, +} from "@solana/web3.js"; +import { Program, AnchorProvider } from "@coral-xyz/anchor"; +import idl from "@/lib/idl/ghbounty_escrow.json"; + +const SUBMISSION_SEED = Buffer.from("submission"); +const PROGRAM_ID = new PublicKey(idl.address); + +function u32LE(n: number): Buffer { + const buf = Buffer.alloc(4); + buf.writeUInt32LE(n, 0); + return buf; +} + +export type BuildSubmitInput = { + rpcUrl: string; + bountyPda: string; + solver: string; + gasStationPubkey: string; + prUrl: string; + submissionCount: number; + blockhash: string; + /** Optional 32-byte hash; defaults to zeros (matches frontend behavior). */ + opusReportHash?: Uint8Array; +}; + +export type BuildSubmitResult = { + unsignedTx: Uint8Array; + submissionPda: string; + submissionIndex: number; +}; + +export async function buildSubmitSolutionTx( + input: BuildSubmitInput +): Promise { + if (input.prUrl.length > 200) { + throw new Error(`pr_url too long (${input.prUrl.length} chars, max 200)`); + } + const opusReportHash = input.opusReportHash ?? new Uint8Array(32); + if (opusReportHash.length !== 32) { + throw new Error( + `opus_report_hash must be 32 bytes (got ${opusReportHash.length})` + ); + } + + const bountyPda = new PublicKey(input.bountyPda); + const solver = new PublicKey(input.solver); + const gasStation = new PublicKey(input.gasStationPubkey); + + const [submissionPda] = PublicKey.findProgramAddressSync( + [SUBMISSION_SEED, bountyPda.toBuffer(), u32LE(input.submissionCount)], + PROGRAM_ID + ); + + // Use AnchorProvider only for the IDL methods builder. Connection is a stub + // (we don't call .send()); blockhash is supplied by the caller. + const dummyConnection = new Connection(input.rpcUrl, "confirmed"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const program = new Program(idl as any, { + connection: dummyConnection, + } as AnchorProvider); + + const ix: TransactionInstruction = await program.methods + .submitSolution( + input.prUrl, + Array.from(opusReportHash) as unknown as number[] + ) + .accountsStrict({ + solver, + bounty: bountyPda, + submission: submissionPda, + systemProgram: new PublicKey("11111111111111111111111111111111"), + }) + .instruction(); + + const message = new TransactionMessage({ + payerKey: gasStation, + recentBlockhash: input.blockhash, + instructions: [ix], + }).compileToV0Message(); + + const tx = new VersionedTransaction(message); + const unsignedTx = tx.serialize(); + + return { + unsignedTx, + submissionPda: submissionPda.toBase58(), + submissionIndex: input.submissionCount, + }; +} +``` + +> **Path note:** `idl` is imported from `@/lib/idl/ghbounty_escrow.json`. The IDL lives in the frontend today (`frontend/lib/idl/ghbounty_escrow.json`). Copy it to `apps/mcp/lib/idl/ghbounty_escrow.json` as part of this task — keeping a frontend → mcp import would couple packages. + +- [ ] **Step 4: Copy the IDL** + +```bash +cp frontend/lib/idl/ghbounty_escrow.json apps/mcp/lib/idl/ghbounty_escrow.json +``` + +- [ ] **Step 5: Run tests, expect pass** + +```bash +pnpm --filter @ghbounty/mcp test tests/solana/build-submit-solution-tx.test.ts +``` + +Expected: 2 tests pass. (The second test relies on Anchor accepting the synthetic pubkeys; if Anchor barks at the dummy `Connection`, mock `program.methods.submitSolution(...)` chain instead — but try it as-is first.) + +- [ ] **Step 6: Commit** + +```bash +git add apps/mcp/lib/solana/build-submit-solution-tx.ts apps/mcp/lib/idl/ghbounty_escrow.json apps/mcp/tests/solana/build-submit-solution-tx.test.ts +git commit -m "feat(mcp): add server-side submit_solution tx builder — GHB-187" +``` + +--- + +## Task 8: Implement `submissions.list` tool + +**Files:** +- Create: `apps/mcp/lib/tools/submissions/list.ts` +- Create: `apps/mcp/tests/tools/submissions/list.test.ts` +- Modify: `apps/mcp/lib/tools/register.ts` + +- [ ] **Step 1: Write failing test** + +Create `apps/mcp/tests/tools/submissions/list.test.ts`: + +```typescript +import { describe, it, expect, vi } from "vitest"; +import { handleSubmissionsList } from "@/lib/tools/submissions/list"; + +vi.mock("@/lib/auth/middleware", () => ({ + authenticate: vi.fn().mockResolvedValue({ + ok: true, + profile: { + user_id: "did:privy:abc", + role: "dev", + mcp_status: "active", + wallet_pubkey: "Solver111", + github_handle: "alice", + }, + credentialId: "k1", + credentialKind: "api_key", + }), +})); + +vi.mock("@/lib/supabase/admin", () => ({ + supabaseAdmin: () => ({ + from: () => ({ + select: () => ({ + eq: () => ({ + order: () => ({ + limit: () => + Promise.resolve({ + data: [ + { + id: "sub-1", + pr_url: "https://github.com/x/y/pull/1", + state: "scored", + rank: 1, + created_at: "2026-05-18T00:00:00Z", + }, + ], + error: null, + }), + }), + }), + }), + }), + }), +})); + +describe("submissions.list", () => { + it("returns the caller's submissions", async () => { + const result = await handleSubmissionsList({ authorization: "Bearer x" }); + expect(result).toMatchObject({ + items: [ + expect.objectContaining({ + id: "sub-1", + state: "scored", + }), + ], + }); + }); + + it("returns Forbidden when caller is a company", async () => { + const { authenticate } = await import("@/lib/auth/middleware"); + (authenticate as any).mockResolvedValueOnce({ + ok: true, + profile: { + user_id: "did:privy:co", + role: "company", + mcp_status: "active", + wallet_pubkey: null, + github_handle: null, + }, + credentialId: "k1", + credentialKind: "api_key", + }); + + const result = await handleSubmissionsList({ authorization: "Bearer x" }); + expect(result).toEqual({ + error: expect.objectContaining({ code: "Forbidden" }), + }); + }); +}); +``` + +- [ ] **Step 2: Run, expect fail** + +```bash +pnpm --filter @ghbounty/mcp test tests/tools/submissions/list.test.ts +``` + +- [ ] **Step 3: Implement** + +Create `apps/mcp/lib/tools/submissions/list.ts`: + +```typescript +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { authenticate } from "@/lib/auth/middleware"; +import { supabaseAdmin } from "@/lib/supabase/admin"; +import { mcpError } from "@/lib/errors"; +import { requireRole } from "@/lib/tools/role-guard"; + +const ListInput = z.object({ + authorization: z.string().optional(), + limit: z.number().int().min(1).max(50).optional(), +}); + +export async function handleSubmissionsList(raw: unknown) { + const parsed = ListInput.safeParse(raw); + if (!parsed.success) return { error: mcpError("InvalidInput", parsed.error.message) }; + + const auth = await authenticate(parsed.data.authorization); + if (!auth.ok) return { error: auth.error }; + + const roleCheck = requireRole(auth.profile, "dev"); + if (!roleCheck.ok) return { error: mcpError("Forbidden", roleCheck.error.message) }; + + if (!auth.profile.wallet_pubkey) { + return { error: mcpError("Forbidden", "Profile has no wallet pubkey.") }; + } + + const supabase = supabaseAdmin(); + const { data, error } = await supabase + .from("submissions") + .select("id, pr_url, state, rank, created_at") + .eq("solver", auth.profile.wallet_pubkey) + .order("created_at", { ascending: false }) + .limit(parsed.data.limit ?? 50); + + if (error) return { error: mcpError("InternalError", error.message) }; + + return { + items: (data ?? []).map((row: any) => ({ + id: row.id, + pr_url: row.pr_url, + state: row.state, + rank: row.rank, + created_at: row.created_at, + })), + }; +} + +export function registerSubmissionsList(server: McpServer): void { + server.tool( + "submissions.list", + { limit: z.number().int().min(1).max(50).optional() }, + async (input, extra) => { + const authorization = (extra as any)?.requestInfo?.headers?.authorization; + const result = await handleSubmissionsList({ ...input, authorization }); + return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; + } + ); +} +``` + +- [ ] **Step 4: Register the tool** + +Modify `apps/mcp/lib/tools/register.ts`: + +```typescript +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { registerWhoami } from "./whoami"; +import { registerBountiesList } from "./bounties/list"; +import { registerBountiesGet } from "./bounties/get"; +import { registerSubmissionsGet } from "./submissions/get"; +import { registerSubmissionsList } from "./submissions/list"; + +export async function registerAllTools(server: McpServer): Promise { + registerWhoami(server); + registerBountiesList(server); + registerBountiesGet(server); + registerSubmissionsGet(server); + registerSubmissionsList(server); +} +``` + +- [ ] **Step 5: Run all MCP tests** + +```bash +pnpm --filter @ghbounty/mcp test +``` + +Expected: existing tests still pass, new `submissions/list.test.ts` passes. + +- [ ] **Step 6: Commit** + +```bash +git add apps/mcp/lib/tools/submissions/list.ts apps/mcp/lib/tools/register.ts apps/mcp/tests/tools/submissions/list.test.ts +git commit -m "feat(mcp): add submissions.list tool — GHB-187" +``` + +--- + +## Task 9: Implement `submissions.create` (a.k.a. submit_pr) — part 1: happy path + +**Files:** +- Create: `apps/mcp/lib/tools/submissions/create.ts` +- Create: `apps/mcp/tests/tools/submissions/create.test.ts` +- Modify: `apps/mcp/lib/tools/register.ts` + +This task is the heaviest; we split it into two tasks for sanity. Part 1 focuses on the happy path + role/delegation guards. Part 2 (next task) covers GHB-182 pre-check + every error row in the spec. + +- [ ] **Step 1: Write the happy-path test** + +Create `apps/mcp/tests/tools/submissions/create.test.ts`: + +```typescript +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { handleSubmissionsCreate } from "@/lib/tools/submissions/create"; + +const baseProfile = { + user_id: "did:privy:alice", + role: "dev" as const, + mcp_status: "active" as const, + wallet_pubkey: "Solver111", + github_handle: "alice", +}; + +beforeEach(() => { + vi.resetModules(); + vi.doMock("@/lib/auth/middleware", () => ({ + authenticate: vi.fn().mockResolvedValue({ + ok: true, + profile: baseProfile, + credentialId: "k1", + credentialKind: "api_key", + }), + })); + + vi.doMock("@/lib/supabase/admin", () => ({ + supabaseAdmin: () => buildSupabase(), + })); + + vi.doMock("@ghbounty/shared", () => ({ + verifyPrOwnership: vi.fn().mockResolvedValue({ ok: true }), + })); + + vi.doMock("@/lib/privy/delegated-signer", () => ({ + getPrivyServerClient: vi.fn().mockReturnValue({}), + signSolanaTransaction: vi.fn().mockResolvedValue({ + ok: true, + signedTx: new Uint8Array([7, 7, 7]), + }), + })); + + vi.doMock("@/lib/solana/build-submit-solution-tx", () => ({ + buildSubmitSolutionTx: vi.fn().mockResolvedValue({ + unsignedTx: new Uint8Array([1, 2, 3]), + submissionPda: "Sub111", + submissionIndex: 0, + }), + })); + + vi.doMock("@/lib/solana/rpc", () => ({ + solanaRpc: () => ({ + getLatestBlockhash: () => ({ + send: () => ({ value: { blockhash: "Bh1", lastValidBlockHeight: 1 } }), + }), + }), + })); + + vi.doMock("@/lib/gas-station/submit", () => ({ + submitViaGasStation: vi + .fn() + .mockResolvedValue({ ok: true, signature: "tx_sig_123" }), + })); +}); + +function buildSupabase(opts: { existingSubmission?: boolean; bountyState?: string } = {}) { + return { + from: (table: string) => { + if (table === "agent_delegations") { + return { + select: () => ({ + eq: () => ({ + maybeSingle: () => Promise.resolve({ data: { revoked_at: null }, error: null }), + }), + }), + }; + } + if (table === "submissions") { + return { + select: () => ({ + eq: () => ({ + eq: () => ({ + eq: () => ({ + maybeSingle: () => + Promise.resolve({ + data: opts.existingSubmission ? { id: "sub-dup", state: "pending" } : null, + error: null, + }), + }), + }), + }), + }), + insert: () => ({ + select: () => ({ + maybeSingle: () => + Promise.resolve({ data: { id: "sub-new" }, error: null }), + }), + }), + }; + } + if (table === "issues") { + return { + select: () => ({ + eq: () => ({ + maybeSingle: () => + Promise.resolve({ + data: { + id: "bounty-1", + pda: "BountyPda1", + github_issue_url: "https://github.com/acme/proj/issues/42", + state: opts.bountyState ?? "open", + }, + error: null, + }), + }), + }), + }; + } + return { select: () => ({}) }; + }, + } as any; +} + +describe("submissions.create — happy path", () => { + it("creates a submission and returns the id + tx signature", async () => { + const result = await handleSubmissionsCreate({ + authorization: "Bearer x", + bounty_id: "bounty-1", + pr_url: "https://github.com/acme/proj/pull/1", + }); + + expect(result).toMatchObject({ + submission_id: "sub-new", + status: "pending", + tx_signature: "tx_sig_123", + }); + }); +}); +``` + +- [ ] **Step 2: Run, expect fail** + +```bash +pnpm --filter @ghbounty/mcp test tests/tools/submissions/create.test.ts +``` + +- [ ] **Step 3: Implement (happy path only)** + +Create `apps/mcp/lib/tools/submissions/create.ts`: + +```typescript +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { authenticate } from "@/lib/auth/middleware"; +import { supabaseAdmin } from "@/lib/supabase/admin"; +import { mcpError } from "@/lib/errors"; +import { requireRole } from "@/lib/tools/role-guard"; +import { requireWalletDelegated } from "@/lib/tools/delegation-guard"; +import { verifyPrOwnership } from "@ghbounty/shared"; +import { + getPrivyServerClient, + signSolanaTransaction, +} from "@/lib/privy/delegated-signer"; +import { buildSubmitSolutionTx } from "@/lib/solana/build-submit-solution-tx"; +import { solanaRpc } from "@/lib/solana/rpc"; +import { submitViaGasStation } from "@/lib/gas-station/submit"; + +const CreateInput = z.object({ + authorization: z.string().optional(), + bounty_id: z.string().uuid(), + pr_url: z.string().url(), +}); + +function parseRepoUrl(githubIssueUrl: string): string | null { + const m = githubIssueUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\//); + return m ? `https://github.com/${m[1]}/${m[2]}` : null; +} + +export async function handleSubmissionsCreate(raw: unknown) { + const parsed = CreateInput.safeParse(raw); + if (!parsed.success) return { error: mcpError("InvalidInput", parsed.error.message) }; + + const auth = await authenticate(parsed.data.authorization); + if (!auth.ok) return { error: auth.error }; + + const roleCheck = requireRole(auth.profile, "dev"); + if (!roleCheck.ok) return { error: mcpError("Forbidden", roleCheck.error.message) }; + + if (auth.profile.mcp_status !== "active") { + return { error: mcpError("Forbidden", "Account is not active.") }; + } + if (!auth.profile.wallet_pubkey) { + return { error: mcpError("Forbidden", "Profile has no wallet pubkey.") }; + } + if (!auth.profile.github_handle) { + return { error: mcpError("Forbidden", "Profile has no linked GitHub handle.") }; + } + + const supabase = supabaseAdmin(); + + const delegationCheck = await requireWalletDelegated(supabase, auth.profile.user_id); + if (!delegationCheck.ok) { + return { error: mcpError("Forbidden", delegationCheck.error.message) }; + } + + // Load bounty + parse repo + const { data: bounty, error: bountyErr } = await supabase + .from("issues") + .select("id, pda, github_issue_url, state") + .eq("id", parsed.data.bounty_id) + .maybeSingle(); + if (bountyErr) return { error: mcpError("InternalError", bountyErr.message) }; + if (!bounty) return { error: mcpError("NotFound", "Bounty not found") }; + if ((bounty as any).state !== "open") { + return { error: mcpError("Conflict", `Bounty is ${(bounty as any).state}.`) }; + } + const repoUrl = parseRepoUrl((bounty as any).github_issue_url); + if (!repoUrl) { + return { error: mcpError("InternalError", "Could not parse bounty repo URL.") }; + } + + // Idempotency check + const { data: existing } = await supabase + .from("submissions") + .select("id, state") + .eq("solver", auth.profile.wallet_pubkey) + .eq("issue_pda", (bounty as any).pda) + .eq("pr_url", parsed.data.pr_url) + .maybeSingle(); + if (existing) { + return { + submission_id: (existing as any).id, + status: (existing as any).state, + tx_signature: null, + idempotent: true, + }; + } + + // PR ownership pre-check (GHB-182) + const verify = await verifyPrOwnership({ + prUrl: parsed.data.pr_url, + expectedGithubHandle: auth.profile.github_handle, + expectedRepoUrl: repoUrl, + token: process.env.GITHUB_TOKEN, + }); + if (!verify.ok) { + return { error: mcpError("Forbidden", `PR ownership check failed: ${verify.reason}`) }; + } + + // Fetch submission_count + blockhash + const rpc = solanaRpc(); + const blockhashResp = await (rpc as any).getLatestBlockhash().send(); + const blockhash = blockhashResp.value.blockhash; + + // submission_count: read from on-chain bounty account. + // For now, query the local mirror as a fallback if the program client isn't wired here. + // The frontend uses program.account.bounty.fetch; we replicate that in build-submit-solution-tx, + // but we still need the count BEFORE building the tx. Simplest path: pass the bounty PDA into + // build-submit-solution-tx and let it fetch internally — keep this task pragmatic and inline + // a fetch via supabase (issues table) for the current count. + const { data: bountyMeta } = await supabase + .from("issues") + .select("submission_count") + .eq("id", parsed.data.bounty_id) + .maybeSingle(); + const submissionCount = (bountyMeta as any)?.submission_count ?? 0; + + // Build tx + const built = await buildSubmitSolutionTx({ + rpcUrl: process.env.SOLANA_RPC_URL!, + bountyPda: (bounty as any).pda, + solver: auth.profile.wallet_pubkey, + gasStationPubkey: process.env.GAS_STATION_PUBKEY!, + prUrl: parsed.data.pr_url, + submissionCount, + blockhash, + }); + + // Privy delegated signing + const client = getPrivyServerClient(); + const signed = await signSolanaTransaction(client, { + walletId: auth.profile.wallet_pubkey, // walletId == wallet pubkey in Privy server SDK (verify in integration) + unsignedTx: built.unsignedTx, + }); + if (!signed.ok) { + if (signed.reason === "delegation_revoked") { + await supabase + .from("agent_delegations") + .update({ revoked_at: new Date().toISOString() }) + .eq("user_id", auth.profile.user_id); + return { + error: mcpError("Forbidden", "Wallet delegation revoked — re-authorize."), + }; + } + return { error: mcpError("ServiceUnavailable", "Signing service unavailable.") }; + } + + // Gas station submit + const submission = await submitViaGasStation({ + signedTx: signed.signedTx, + chainId: process.env.CHAIN_ID ?? "solana-devnet", + }); + if (!submission.ok) { + return { error: mcpError("InternalError", `On-chain submission failed: ${submission.reason}`) }; + } + + // Mirror row insert + const { data: insertRow, error: insertErr } = await supabase + .from("submissions") + .insert({ + pda: built.submissionPda, + solver: auth.profile.wallet_pubkey, + pr_url: parsed.data.pr_url, + issue_pda: (bounty as any).pda, + submission_index: built.submissionIndex, + opus_report_hash: new Array(32).fill(0), + state: "pending", + }) + .select("id") + .maybeSingle(); + + if (insertErr || !insertRow) { + // The on-chain tx already landed; relayer will reconcile. Return tx_signature so the agent can track it. + return { + submission_id: null, + status: "pending", + tx_signature: submission.signature, + mirror_insert_failed: true, + }; + } + + return { + submission_id: (insertRow as any).id, + status: "pending", + tx_signature: submission.signature, + }; +} + +export function registerSubmissionsCreate(server: McpServer): void { + server.tool( + "submissions.create", + { + bounty_id: z.string().uuid(), + pr_url: z.string().url(), + }, + async (input, extra) => { + const authorization = (extra as any)?.requestInfo?.headers?.authorization; + const result = await handleSubmissionsCreate({ ...input, authorization }); + return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; + } + ); +} +``` + +> **Helpers referenced but possibly missing:** +> - `@/lib/gas-station/submit` — the executor must create this thin wrapper. It should POST to the existing gas-station sponsor route (`frontend/app/api/gas-station/sponsor/route.ts`) using the server-to-server fetch, mirroring how the frontend posts but skipping the Privy access token (use a service token instead). If the existing endpoint requires a Privy access token, add a second auth path scoped to internal callers (env: `MCP_INTERNAL_SECRET`). +> - The exact `walletId` for Privy's `signTransaction` — assumed to be the wallet pubkey. **Verify against Privy server SDK docs once installed.** If it's a different identifier, fetch it from Privy's `users.getById(user_id)`. + +- [ ] **Step 4: Register the tool** + +Modify `apps/mcp/lib/tools/register.ts` — add the import + call: + +```typescript +import { registerSubmissionsCreate } from "./submissions/create"; +// inside registerAllTools: +registerSubmissionsCreate(server); +``` + +- [ ] **Step 5: Run the happy-path test** + +```bash +pnpm --filter @ghbounty/mcp test tests/tools/submissions/create.test.ts +``` + +Expected: 1 test passes. + +- [ ] **Step 6: Commit** + +```bash +git add apps/mcp/lib/tools/submissions/create.ts apps/mcp/lib/tools/register.ts apps/mcp/tests/tools/submissions/create.test.ts +git commit -m "feat(mcp): add submissions.create (submit_pr) happy path — GHB-187" +``` + +--- + +## Task 10: `submissions.create` — error cases + +Add a test per row of the spec's error table to `apps/mcp/tests/tools/submissions/create.test.ts`. Each test reuses the `beforeEach` setup and overrides one mock to trigger the error. + +- [ ] **Step 1: Add tests for every error row** + +Append to the file (inside a new `describe("submissions.create — errors")` block): + +```typescript +describe("submissions.create — errors", () => { + it("returns Forbidden when role is company", async () => { + const { authenticate } = await import("@/lib/auth/middleware"); + (authenticate as any).mockResolvedValueOnce({ + ok: true, + profile: { ...baseProfile, role: "company" }, + credentialId: "k1", + credentialKind: "api_key", + }); + const result = await handleSubmissionsCreate({ + authorization: "Bearer x", + bounty_id: "bounty-1", + pr_url: "https://github.com/acme/proj/pull/1", + }); + expect(result.error?.code).toBe("Forbidden"); + }); + + it("returns Forbidden when mcp_status != active", async () => { + const { authenticate } = await import("@/lib/auth/middleware"); + (authenticate as any).mockResolvedValueOnce({ + ok: true, + profile: { ...baseProfile, mcp_status: "suspended" }, + credentialId: "k1", + credentialKind: "api_key", + }); + const result = await handleSubmissionsCreate({ + authorization: "Bearer x", + bounty_id: "bounty-1", + pr_url: "https://github.com/acme/proj/pull/1", + }); + expect(result.error?.code).toBe("Forbidden"); + }); + + it("returns Forbidden when user has no delegation", async () => { + vi.doMock("@/lib/tools/delegation-guard", () => ({ + requireWalletDelegated: vi.fn().mockResolvedValue({ + ok: false, + error: { code: "Forbidden", message: "no delegation" }, + }), + })); + // re-import after mock + const { handleSubmissionsCreate: h } = await import( + "@/lib/tools/submissions/create" + ); + const result = await h({ + authorization: "Bearer x", + bounty_id: "bounty-1", + pr_url: "https://github.com/acme/proj/pull/1", + }); + expect(result.error?.code).toBe("Forbidden"); + }); + + it("returns NotFound when bounty doesn't exist", async () => { + vi.doMock("@/lib/supabase/admin", () => ({ + supabaseAdmin: () => ({ + from: (table: string) => { + if (table === "agent_delegations") { + return { + select: () => ({ + eq: () => ({ + maybeSingle: () => + Promise.resolve({ data: { revoked_at: null }, error: null }), + }), + }), + }; + } + if (table === "issues") { + return { + select: () => ({ + eq: () => ({ + maybeSingle: () => Promise.resolve({ data: null, error: null }), + }), + }), + }; + } + return { select: () => ({}) }; + }, + }), + })); + const { handleSubmissionsCreate: h } = await import( + "@/lib/tools/submissions/create" + ); + const result = await h({ + authorization: "Bearer x", + bounty_id: "bounty-1", + pr_url: "https://github.com/acme/proj/pull/1", + }); + expect(result.error?.code).toBe("NotFound"); + }); + + it("returns Conflict when bounty is not open", async () => { + vi.doMock("@/lib/supabase/admin", () => ({ + supabaseAdmin: () => buildSupabase({ bountyState: "resolved" }), + })); + const { handleSubmissionsCreate: h } = await import( + "@/lib/tools/submissions/create" + ); + const result = await h({ + authorization: "Bearer x", + bounty_id: "bounty-1", + pr_url: "https://github.com/acme/proj/pull/1", + }); + expect(result.error?.code).toBe("Conflict"); + }); + + it("returns existing submission_id on idempotent hit", async () => { + vi.doMock("@/lib/supabase/admin", () => ({ + supabaseAdmin: () => buildSupabase({ existingSubmission: true }), + })); + const { handleSubmissionsCreate: h } = await import( + "@/lib/tools/submissions/create" + ); + const result = await h({ + authorization: "Bearer x", + bounty_id: "bounty-1", + pr_url: "https://github.com/acme/proj/pull/1", + }); + expect(result.submission_id).toBe("sub-dup"); + expect(result.idempotent).toBe(true); + }); + + it("returns Forbidden when verify-pr-ownership fails", async () => { + vi.doMock("@ghbounty/shared", () => ({ + verifyPrOwnership: vi + .fn() + .mockResolvedValue({ ok: false, reason: "author_mismatch" }), + })); + const { handleSubmissionsCreate: h } = await import( + "@/lib/tools/submissions/create" + ); + const result = await h({ + authorization: "Bearer x", + bounty_id: "bounty-1", + pr_url: "https://github.com/acme/proj/pull/1", + }); + expect(result.error?.code).toBe("Forbidden"); + expect(result.error?.message).toMatch(/author_mismatch/); + }); + + it("marks delegation revoked when Privy returns 403", async () => { + vi.doMock("@/lib/privy/delegated-signer", () => ({ + getPrivyServerClient: vi.fn().mockReturnValue({}), + signSolanaTransaction: vi + .fn() + .mockResolvedValue({ ok: false, reason: "delegation_revoked" }), + })); + const { handleSubmissionsCreate: h } = await import( + "@/lib/tools/submissions/create" + ); + const result = await h({ + authorization: "Bearer x", + bounty_id: "bounty-1", + pr_url: "https://github.com/acme/proj/pull/1", + }); + expect(result.error?.code).toBe("Forbidden"); + expect(result.error?.message).toMatch(/delegation/i); + }); +}); +``` + +- [ ] **Step 2: Run tests** + +```bash +pnpm --filter @ghbounty/mcp test tests/tools/submissions/create.test.ts +``` + +Expected: all tests pass. + +- [ ] **Step 3: Run workspace-wide checks** + +```bash +pnpm typecheck && pnpm test +``` + +Expected: green everywhere. + +- [ ] **Step 4: Commit** + +```bash +git add apps/mcp/tests/tools/submissions/create.test.ts +git commit -m "test(mcp): cover error cases for submissions.create — GHB-187" +``` + +--- + +## Task 11: Frontend `/api/agent-delegation` endpoint + +**Files:** +- Create: `frontend/app/api/agent-delegation/route.ts` + +> Before touching frontend code, read the relevant Next.js routing doc in `node_modules/next/dist/docs/` per `frontend/AGENTS.md`. + +- [ ] **Step 1: Implement the endpoint** + +Create `frontend/app/api/agent-delegation/route.ts`: + +```typescript +import { NextResponse } from "next/server"; +import { supabaseServerClient } from "@/lib/supabase/server"; +import { verifyPrivyAccessToken } from "@/lib/privy/server"; + +export async function POST(req: Request) { + const body = (await req.json()) as { + action: "delegate" | "revoke"; + wallet_pubkey?: string; + chain_type?: string; + }; + + const auth = req.headers.get("authorization"); + if (!auth?.startsWith("Bearer ")) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const claims = await verifyPrivyAccessToken(auth.slice("Bearer ".length)); + if (!claims) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const supabase = supabaseServerClient(); + + if (body.action === "delegate") { + if (!body.wallet_pubkey) { + return NextResponse.json({ error: "wallet_pubkey required" }, { status: 400 }); + } + const { error } = await supabase + .from("agent_delegations") + .upsert( + { + user_id: claims.userId, + wallet_pubkey: body.wallet_pubkey, + chain_type: body.chain_type ?? "solana", + delegated_at: new Date().toISOString(), + revoked_at: null, + updated_at: new Date().toISOString(), + }, + { onConflict: "user_id" } + ); + if (error) return NextResponse.json({ error: error.message }, { status: 500 }); + return NextResponse.json({ ok: true }); + } + + if (body.action === "revoke") { + const { error } = await supabase + .from("agent_delegations") + .update({ revoked_at: new Date().toISOString(), updated_at: new Date().toISOString() }) + .eq("user_id", claims.userId); + if (error) return NextResponse.json({ error: error.message }, { status: 500 }); + return NextResponse.json({ ok: true }); + } + + return NextResponse.json({ error: "Invalid action" }, { status: 400 }); +} + +export async function GET(req: Request) { + const auth = req.headers.get("authorization"); + if (!auth?.startsWith("Bearer ")) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const claims = await verifyPrivyAccessToken(auth.slice("Bearer ".length)); + if (!claims) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const supabase = supabaseServerClient(); + const { data, error } = await supabase + .from("agent_delegations") + .select("wallet_pubkey, chain_type, delegated_at, revoked_at") + .eq("user_id", claims.userId) + .maybeSingle(); + if (error) return NextResponse.json({ error: error.message }, { status: 500 }); + return NextResponse.json({ delegation: data }); +} +``` + +> **Note:** `verifyPrivyAccessToken` and `supabaseServerClient` likely already exist; if not, grep `frontend/lib/` for existing patterns. Reuse whatever the rest of the app uses for the same job. + +- [ ] **Step 2: Smoke-check typecheck** + +```bash +pnpm --filter @ghbounty/frontend typecheck +``` + +Expected: no errors. + +- [ ] **Step 3: Commit** + +```bash +git add frontend/app/api/agent-delegation/route.ts +git commit -m "feat(frontend): add agent-delegation API route — GHB-187" +``` + +--- + +## Task 12: `AgentDelegationCard` component + +**Files:** +- Create: `frontend/app/app/credentials/AgentDelegationCard.tsx` + +- [ ] **Step 1: Implement the component** + +Create `frontend/app/app/credentials/AgentDelegationCard.tsx`: + +```tsx +"use client"; + +import { useEffect, useState } from "react"; +import { usePrivy, useHeadlessDelegatedActions, useSolanaWallets } from "@privy-io/react-auth"; + +type DelegationState = { + wallet_pubkey: string; + chain_type: string; + delegated_at: string; + revoked_at: string | null; +} | null; + +export function AgentDelegationCard() { + const { user, getAccessToken } = usePrivy(); + const { wallets } = useSolanaWallets(); + const { delegateWallet, revokeWallets } = useHeadlessDelegatedActions(); + const [delegation, setDelegation] = useState(null); + const [loading, setLoading] = useState(false); + + const solanaWallet = wallets[0]; + + async function load() { + const token = await getAccessToken(); + const r = await fetch("/api/agent-delegation", { + headers: { Authorization: `Bearer ${token}` }, + }); + if (r.ok) { + const j = await r.json(); + setDelegation(j.delegation); + } + } + + useEffect(() => { + if (user) void load(); + }, [user]); + + async function onAuthorize() { + if (!solanaWallet) return; + setLoading(true); + try { + await delegateWallet({ address: solanaWallet.address, chainType: "solana" }); + const token = await getAccessToken(); + await fetch("/api/agent-delegation", { + method: "POST", + headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, + body: JSON.stringify({ + action: "delegate", + wallet_pubkey: solanaWallet.address, + chain_type: "solana", + }), + }); + await load(); + } finally { + setLoading(false); + } + } + + async function onRevoke() { + setLoading(true); + try { + await revokeWallets(); + const token = await getAccessToken(); + await fetch("/api/agent-delegation", { + method: "POST", + headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, + body: JSON.stringify({ action: "revoke" }), + }); + await load(); + } finally { + setLoading(false); + } + } + + const isActive = delegation && delegation.revoked_at === null; + + return ( +
+

Authorize agent to act on-chain

+ + {!isActive ? ( + <> +

+ Your AI agent needs permission to sign Solana transactions on your behalf to submit PRs + to bounties. Without this, every action would require you to open a browser and confirm — + which defeats the point of having an agent. +

+ +
+

What you're authorizing:

+
    +
  • + GhBounty server can sign submit_solution transactions using your wallet + ({solanaWallet?.address ?? "—"}). +
  • +
  • + Scoped to the GhBounty escrow program only — we validate every transaction + server-side before signing. +
  • +
+

What we cannot do:

+
    +
  • Transfer your SOL or tokens.
  • +
  • Withdraw funds from any escrow.
  • +
  • Sign any transaction outside the ghbounty_escrow program.
  • +
+

+ Revoke any time: revoking stops the agent from submitting PRs until + you re-authorize. +

+
+ + + + ) : ( + <> +

+ ✓ Authorized — your agent can submit PRs on your behalf. +

+
+
+
Wallet:
+
{delegation!.wallet_pubkey}
+
+
+
Delegated since:
+
{new Date(delegation!.delegated_at).toLocaleString()}
+
+
+ + + )} +
+ ); +} +``` + +- [ ] **Step 2: Smoke typecheck** + +```bash +pnpm --filter @ghbounty/frontend typecheck +``` + +- [ ] **Step 3: Commit** + +```bash +git add frontend/app/app/credentials/AgentDelegationCard.tsx +git commit -m "feat(frontend): add AgentDelegationCard consent UI — GHB-187" +``` + +--- + +## Task 13: Wire the card into the credentials page + +**Files:** +- Modify: `frontend/app/app/credentials/page.tsx` + +- [ ] **Step 1: Import + render** + +Open `frontend/app/app/credentials/page.tsx` and: + +1. Add the import near the other component imports: + ```tsx + import { AgentDelegationCard } from "./AgentDelegationCard"; + ``` +2. Render `` somewhere on the page — by convention right after the API keys card (find the existing API-keys component and place the new one below it). + +- [ ] **Step 2: Run the dev server and smoke-test in browser** + +```bash +pnpm --filter @ghbounty/frontend dev +``` + +Open `http://localhost:3000/app/credentials`. Verify the new card renders. Authorize → confirms in Privy → DB row appears (check via Supabase Studio). Revoke → row is updated. + +- [ ] **Step 3: Commit** + +```bash +git add frontend/app/app/credentials/page.tsx +git commit -m "feat(frontend): render AgentDelegationCard in /app/credentials — GHB-187" +``` + +--- + +## Task 14: Relayer pre-scoring ownership check + +**Files:** +- Modify: `relayer/src/submission-handler.ts` +- Modify or create: `relayer/tests/submission-handler.test.ts` + +- [ ] **Step 1: Inspect the current handler** + +Open `relayer/src/submission-handler.ts`. Locate the function that handles a new submission (it should call into `opus.ts` for scoring). The ownership check goes immediately before the Opus call. + +- [ ] **Step 2: Write the failing test** + +In `relayer/tests/submission-handler.test.ts` (create or extend), add: + +```typescript +import { describe, it, expect, vi } from "vitest"; +import { handleSubmission } from "../src/submission-handler"; + +vi.mock("@ghbounty/shared", () => ({ + verifyPrOwnership: vi.fn().mockResolvedValue({ ok: false, reason: "author_mismatch" }), +})); + +describe("handleSubmission — ownership check", () => { + it("marks the submission auto_rejected when ownership mismatches", async () => { + const supabase = makeFakeSupabase(); // re-use existing helper or build inline + await handleSubmission( + { + submissionPda: "Sub1", + solver: "Wallet1", + prUrl: "https://github.com/x/y/pull/1", + bountyRepoUrl: "https://github.com/x/y", + solverGithubHandle: "alice", + }, + { supabase, opusScore: vi.fn() } as any + ); + + expect(supabase.updates).toContainEqual( + expect.objectContaining({ state: "auto_rejected" }) + ); + }); +}); +``` + +> Adapt the test's wiring to whatever shape `handleSubmission` actually has. Read the current file before deciding. + +- [ ] **Step 3: Implement the check** + +Inside the submission handler, before the Opus call: + +```typescript +import { verifyPrOwnership } from "@ghbounty/shared"; + +const ownership = await verifyPrOwnership({ + prUrl: submission.prUrl, + expectedGithubHandle: submission.solverGithubHandle, + expectedRepoUrl: submission.bountyRepoUrl, + token: process.env.GITHUB_TOKEN, +}); + +if (!ownership.ok) { + await supabase + .from("submissions") + .update({ state: "auto_rejected" }) + .eq("pda", submission.submissionPda); + log.warn({ + msg: "ownership_check_failed", + reason: ownership.reason, + pda: submission.submissionPda, + }); + return; +} +``` + +Resolve `solverGithubHandle` and `bountyRepoUrl` by JOINing through `profiles` (solver → wallet → user_id → github_handle) and `issues.github_issue_url` if the handler doesn't already have them. Refactor the input shape if needed. + +- [ ] **Step 4: Run tests** + +```bash +pnpm --filter @ghbounty/relayer test +``` + +Expected: green. + +- [ ] **Step 5: Commit** + +```bash +git add relayer/src/submission-handler.ts relayer/tests/submission-handler.test.ts +git commit -m "feat(relayer): pre-scoring PR ownership check — GHB-187" +``` + +--- + +## Task 15: Smoke test runbook + +**Files:** +- Create: `docs/superpowers/runbooks/2026-05-18-sprint-b-smoke.md` + +- [ ] **Step 1: Write the runbook** + +Create `docs/superpowers/runbooks/2026-05-18-sprint-b-smoke.md`: + +````markdown +# Sprint B smoke test runbook (2026-05-18) + +End-to-end manual verification of `submissions.create` after deploy. + +## Pre-flight + +- Migration 0026 applied to devnet Supabase (`pnpm db:migrate`). +- MCP deploy includes commits up to (and including) the `submissions.create` task. +- Relayer deploy includes the ownership pre-check. +- Frontend deploy includes the `AgentDelegationCard`. +- `PRIVY_APP_ID` / `PRIVY_APP_SECRET` set on the MCP env (Vercel project). + +## Steps + +1. **Delegate the wallet.** Log into `/app/credentials` as Gaston (dev role). Click "Authorize" in the new card. Confirm in Privy. Verify in Supabase Studio that `agent_delegations` has a row with `revoked_at IS NULL`. + +2. **Verify `submissions.list`.** Hit the MCP from Claude Code (or curl): + + ```bash + curl -sS -X POST https://mcp.ghbounty.com/api/mcp/mcp \ + -H "Authorization: Bearer $GHB_API_KEY" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json, text/event-stream" \ + -d '{"jsonrpc":"2.0","id":1,"method":"tools/call", + "params":{"name":"submissions.list","arguments":{}}}' + ``` + + Expected: `{ items: [...] }` (possibly empty pre-test). + +3. **Open a real PR** against the bounty's target repo using Gaston's GitHub account. + +4. **Call `submissions.create`**: + + ```bash + curl -sS -X POST https://mcp.ghbounty.com/api/mcp/mcp \ + -H "Authorization: Bearer $GHB_API_KEY" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json, text/event-stream" \ + -d '{"jsonrpc":"2.0","id":1,"method":"tools/call", + "params":{"name":"submissions.create", + "arguments":{ + "bounty_id":"", + "pr_url":"https://github.com///pull/"}}}' + ``` + + Expected: `{ submission_id, status: "pending", tx_signature }`. + +5. **Check Solana Explorer** (devnet) for the tx signature. Expect a `submit_solution` invocation. + +6. **Wait ~30-60s** for relayer to pick up. Poll `submissions.get`: + + ```bash + curl -sS -X POST https://mcp.ghbounty.com/api/mcp/mcp \ + -H "Authorization: Bearer $GHB_API_KEY" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json, text/event-stream" \ + -d '{"jsonrpc":"2.0","id":1,"method":"tools/call", + "params":{"name":"submissions.get", + "arguments":{"submission_id":""}}}' + ``` + + Expected: `state: "scored"` with a numeric `score`. + +7. **Negative test — wrong author.** Submit a PR URL owned by someone else against another bounty. Expect `Forbidden — PR ownership check failed: author_mismatch`. + +8. **Negative test — revoke + try.** Revoke the delegation from `/app/credentials`. Call `submissions.create` again. Expect `Forbidden — Wallet delegation required`. + +## On failure + +If any step errors: +- Inspect MCP logs (Vercel dashboard) — they include structured tags for `submissions.create`. +- Inspect relayer logs (process supervisor) — look for `ownership_check_failed`. +- Inspect Supabase `submissions` row state for the relevant PDA. +```` + +- [ ] **Step 2: Commit** + +```bash +git add docs/superpowers/runbooks/2026-05-18-sprint-b-smoke.md +git commit -m "docs(runbooks): add Sprint B smoke test runbook — GHB-187" +``` + +--- + +## Task 16: Final workspace check + push for PR + +- [ ] **Step 1: Run all checks** + +```bash +pnpm typecheck && pnpm test +``` + +Expected: green everywhere. + +- [ ] **Step 2: Push the branch** + +```bash +git push -u origin gastonfoncea09/ghb-187-sprint-b-mcp-on-chain-tools-submit_pr-check_status +``` + +- [ ] **Step 3: Open a draft PR** + +```bash +gh pr create --draft --title "feat(mcp): Sprint B — on-chain tools (submit_pr) — GHB-187" --body "$(cat <<'EOF' +## Summary + +Sprint B implementation. Adds `submissions.create` (a.k.a. `submit_pr`) as the first MCP write tool, plus `submissions.list`, hard role gating, Privy delegated server-signing, and a defense-in-depth fix for GHB-182 (PR ownership). + +Spec: `docs/superpowers/specs/2026-05-18-mcp-sprint-b-onchain-tools-design.md` +Plan: `docs/superpowers/plans/2026-05-18-mcp-sprint-b-onchain-tools.md` + +## Test plan + +- [ ] Migration 0026 applied to devnet +- [ ] Delegated wallet via /app/credentials +- [ ] submissions.list returns rows +- [ ] submissions.create creates submission + on-chain tx +- [ ] Relayer post-check rejects ownership mismatch +- [ ] Negative cases per runbook + +Full runbook: `docs/superpowers/runbooks/2026-05-18-sprint-b-smoke.md` + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +EOF +)" +``` + +--- + +## Self-review + +**Spec coverage check (each spec section):** + +| Spec section | Covered by | +|---|---| +| `submissions.create` tool | Tasks 9 + 10 | +| `submissions.list` tool | Task 8 | +| Hard role gating | Task 4 (`requireRole`) applied in Tasks 8 + 9 | +| Privy delegated server-signing | Tasks 3 (install) + 6 (wrapper) + used in 9 | +| UX consent screen at `/app/credentials` | Tasks 11 (API) + 12 (component) + 13 (wire) | +| GHB-182 MCP pre-check | Task 9 (calls `verifyPrOwnership` from Task 2) | +| GHB-182 relayer post-check | Task 14 | +| Migration `agent_delegations` | Task 1 | +| Idempotency by (user, bounty, pr_url) | Task 9 (idempotency check block + Task 10 test) | +| `opus_report_hash = zeros` | Task 7 (default in `buildSubmitSolutionTx`) | +| All error rows in spec table | Task 10 | +| Smoke test runbook | Task 15 | +| Branch + PR flow | Task 16 | + +**Placeholder scan:** Two flagged "verify in integration" notes (Task 9): the exact Privy `walletId` lookup and the gas-station internal auth path. Both are real research items, not placeholders — they require running against actual Privy / gas-station infra to confirm. The executor should treat these as integration tasks and confirm before going to production. + +**Type consistency:** `GuardResult` defined in Task 4 (`role-guard.ts`) and reused in Task 5 (`delegation-guard.ts`) — consistent. `VerifyPrOwnershipResult` in Task 2 reused via the `@ghbounty/shared` barrel in Tasks 9 + 14 — consistent. `SignResult` in Task 6 reused in Task 9 — consistent. + +**Open during execution (flag in PR description):** +- Privy `walletId` shape — does `walletApi.solana.signTransaction` accept the wallet pubkey directly, or does it need an internal Privy wallet ID? Inspect once `@privy-io/server-auth` is installed. +- Gas-station internal auth — `submitViaGasStation` (Task 9) assumes there's a server-to-server path. If the existing `/api/gas-station/sponsor` only accepts Privy access tokens, add an internal-secret path scoped to MCP. From b2845ff4439cbd4acc89493a49f0c1dfca2c91ad Mon Sep 17 00:00:00 2001 From: gastonfoncea Date: Mon, 18 May 2026 18:17:17 -0300 Subject: [PATCH 04/21] =?UTF-8?q?feat(db):=20add=20agent=5Fdelegations=20t?= =?UTF-8?q?able=20=E2=80=94=20GHB-187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the agent_delegations table to track which users have granted server-side Privy wallet signing consent for the MCP submit_pr flow. Includes FK to profiles(user_id) ON DELETE CASCADE, a partial index on active delegations (revoked_at IS NULL), and an RLS policy for end-user session reads. Migration ready for human-applied db:migrate. Co-Authored-By: Claude Sonnet 4.6 --- .../db/drizzle/0026_agent_delegations.sql | 33 + packages/db/drizzle/meta/0025_snapshot.json | 1770 ++++++++++++++++ packages/db/drizzle/meta/0026_snapshot.json | 1842 +++++++++++++++++ packages/db/drizzle/meta/_journal.json | 9 +- packages/db/src/schema.ts | 19 + 5 files changed, 3672 insertions(+), 1 deletion(-) create mode 100644 packages/db/drizzle/0026_agent_delegations.sql create mode 100644 packages/db/drizzle/meta/0025_snapshot.json create mode 100644 packages/db/drizzle/meta/0026_snapshot.json diff --git a/packages/db/drizzle/0026_agent_delegations.sql b/packages/db/drizzle/0026_agent_delegations.sql new file mode 100644 index 0000000..0c0446b --- /dev/null +++ b/packages/db/drizzle/0026_agent_delegations.sql @@ -0,0 +1,33 @@ +-- GHB-187: agent_delegations table — tracks server-side signing consent +-- for MCP submit_pr flow (Privy wallet delegation). + +BEGIN; + +CREATE TABLE "agent_delegations" ( + "user_id" text PRIMARY KEY NOT NULL, + "wallet_pubkey" text NOT NULL, + "chain_type" text NOT NULL, + "delegated_at" timestamp with time zone DEFAULT now() NOT NULL, + "revoked_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "agent_delegations" ADD CONSTRAINT "agent_delegations_user_id_profiles_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."profiles"("user_id") ON DELETE cascade ON UPDATE no action; +--> statement-breakpoint +CREATE INDEX "idx_agent_delegations_active" + ON "agent_delegations" ("user_id") + WHERE "revoked_at" IS NULL; +--> statement-breakpoint +ALTER TABLE "agent_delegations" ENABLE ROW LEVEL SECURITY; +--> statement-breakpoint +CREATE POLICY "agent_delegations_own_read" + ON "agent_delegations" + FOR SELECT + USING (user_id = (auth.jwt() ->> 'sub')); + +-- Reload PostgREST schema cache so the new table is immediately visible +-- (same pattern as 0024_mcp_rls_rebuild.sql / GHB-191). +NOTIFY pgrst, 'reload schema'; + +COMMIT; diff --git a/packages/db/drizzle/meta/0025_snapshot.json b/packages/db/drizzle/meta/0025_snapshot.json new file mode 100644 index 0000000..f602c89 --- /dev/null +++ b/packages/db/drizzle/meta/0025_snapshot.json @@ -0,0 +1,1770 @@ +{ + "id": "aaaaaaaa-0025-0025-0025-aaaaaaaaaaaa", + "prevId": "3867360c-08c0-4d38-8d6a-252b83cc0c42", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_prefix": { + "name": "key_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_profiles_user_id_fk": { + "name": "api_keys_user_id_profiles_user_id_fk", + "tableFrom": "api_keys", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bounty_meta": { + "name": "bounty_meta", + "schema": "", + "columns": { + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_mode": { + "name": "release_mode", + "type": "release_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'assisted'" + }, + "closed_by_user": { + "name": "closed_by_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "max_submissions": { + "name": "max_submissions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "closed_by_cap_at": { + "name": "closed_by_cap_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cap_warning_sent_at": { + "name": "cap_warning_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reject_threshold": { + "name": "reject_threshold", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "evaluation_criteria": { + "name": "evaluation_criteria", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "review_fee_lamports_paid": { + "name": "review_fee_lamports_paid", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "review_fee_lamports_per_review": { + "name": "review_fee_lamports_per_review", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "bounty_meta_issue_id_issues_id_fk": { + "name": "bounty_meta_issue_id_issues_id_fk", + "tableFrom": "bounty_meta", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bounty_meta_created_by_user_id_profiles_user_id_fk": { + "name": "bounty_meta_created_by_user_id_profiles_user_id_fk", + "tableFrom": "bounty_meta", + "tableTo": "profiles", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chain_registry": { + "name": "chain_registry", + "schema": "", + "columns": { + "chain_id": { + "name": "chain_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rpc_url": { + "name": "rpc_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "escrow_address": { + "name": "escrow_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "explorer_url": { + "name": "explorer_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_symbol": { + "name": "token_symbol", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "x402_supported": { + "name": "x402_supported", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.companies": { + "name": "companies", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "industry": { + "name": "industry", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_org": { + "name": "github_org", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "companies_user_id_profiles_user_id_fk": { + "name": "companies_user_id_profiles_user_id_fk", + "tableFrom": "companies", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "companies_slug_unique": { + "name": "companies_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.developers": { + "name": "developers", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_handle": { + "name": "github_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "skills": { + "name": "skills", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "developers_user_id_profiles_user_id_fk": { + "name": "developers_user_id_profiles_user_id_fk", + "tableFrom": "developers", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "developers_username_unique": { + "name": "developers_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.evaluations": { + "name": "evaluations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "submission_pda": { + "name": "submission_pda", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "evaluation_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "reasoning": { + "name": "reasoning", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "report": { + "name": "report", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "report_hash": { + "name": "report_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tx_hash": { + "name": "tx_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "genlayer_score": { + "name": "genlayer_score", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "genlayer_status": { + "name": "genlayer_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "genlayer_dimensions": { + "name": "genlayer_dimensions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "genlayer_tx_hash": { + "name": "genlayer_tx_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issues": { + "name": "issues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chain_id": { + "name": "chain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pda": { + "name": "pda", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bounty_onchain_id": { + "name": "bounty_onchain_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "creator": { + "name": "creator", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scorer": { + "name": "scorer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mint": { + "name": "mint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "issue_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "submission_count": { + "name": "submission_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "review_eligible_count": { + "name": "review_eligible_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "winner": { + "name": "winner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_issue_url": { + "name": "github_issue_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "issues_chain_id_chain_registry_chain_id_fk": { + "name": "issues_chain_id_chain_registry_chain_id_fk", + "tableFrom": "issues", + "tableTo": "chain_registry", + "columnsFrom": [ + "chain_id" + ], + "columnsTo": [ + "chain_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "issues_pda_unique": { + "name": "issues_pda_unique", + "nullsNotDistinct": false, + "columns": [ + "pda" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_clients": { + "name": "oauth_clients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_codes": { + "name": "oauth_codes", + "schema": "", + "columns": { + "code": { + "name": "code", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oauth_codes_expires_idx": { + "name": "oauth_codes_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_codes_user_id_profiles_user_id_fk": { + "name": "oauth_codes_user_id_profiles_user_id_fk", + "tableFrom": "oauth_codes", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_codes_client_id_oauth_clients_id_fk": { + "name": "oauth_codes_client_id_oauth_clients_id_fk", + "tableFrom": "oauth_codes", + "tableTo": "oauth_clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_tokens": { + "name": "oauth_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['full']::text[]" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oauth_tokens_prefix_idx": { + "name": "oauth_tokens_prefix_idx", + "columns": [ + { + "expression": "token_prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_tokens_user_id_profiles_user_id_fk": { + "name": "oauth_tokens_user_id_profiles_user_id_fk", + "tableFrom": "oauth_tokens", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_tokens_client_id_oauth_clients_id_fk": { + "name": "oauth_tokens_client_id_oauth_clients_id_fk", + "tableFrom": "oauth_tokens", + "tableTo": "oauth_clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_txs": { + "name": "pending_txs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message_hash": { + "name": "message_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expected_signer": { + "name": "expected_signer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pending_txs_user_id_profiles_user_id_fk": { + "name": "pending_txs_user_id_profiles_user_id_fk", + "tableFrom": "pending_txs", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profiles": { + "name": "profiles", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "role": { + "name": "role", + "type": "user_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "onboarding_completed": { + "name": "onboarding_completed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "mcp_status": { + "name": "mcp_status", + "type": "agent_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "warnings": { + "name": "warnings", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "github_handle": { + "name": "github_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wallet_pubkey": { + "name": "wallet_pubkey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "profiles_email_unique": { + "name": "profiles_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "profiles_github_handle_unique": { + "name": "profiles_github_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "github_handle" + ] + }, + "profiles_wallet_pubkey_unique": { + "name": "profiles_wallet_pubkey_unique", + "nullsNotDistinct": false, + "columns": [ + "wallet_pubkey" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slashing_events": { + "name": "slashing_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "evidence": { + "name": "evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "slashing_events_user_id_profiles_user_id_fk": { + "name": "slashing_events_user_id_profiles_user_id_fk", + "tableFrom": "slashing_events", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stake_deposits": { + "name": "stake_deposits", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pda": { + "name": "pda", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tx_signature": { + "name": "tx_signature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_lamports": { + "name": "amount_lamports", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "stake_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_until": { + "name": "locked_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "refunded_at": { + "name": "refunded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "slashed_at": { + "name": "slashed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "stake_deposits_user_id_profiles_user_id_fk": { + "name": "stake_deposits_user_id_profiles_user_id_fk", + "tableFrom": "stake_deposits", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "stake_deposits_pda_unique": { + "name": "stake_deposits_pda_unique", + "nullsNotDistinct": false, + "columns": [ + "pda" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.submission_meta": { + "name": "submission_meta", + "schema": "", + "columns": { + "submission_id": { + "name": "submission_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "submitted_by_user_id": { + "name": "submitted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "submission_meta_submission_id_submissions_id_fk": { + "name": "submission_meta_submission_id_submissions_id_fk", + "tableFrom": "submission_meta", + "tableTo": "submissions", + "columnsFrom": [ + "submission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "submission_meta_submitted_by_user_id_profiles_user_id_fk": { + "name": "submission_meta_submitted_by_user_id_profiles_user_id_fk", + "tableFrom": "submission_meta", + "tableTo": "profiles", + "columnsFrom": [ + "submitted_by_user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.submissions": { + "name": "submissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chain_id": { + "name": "chain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_pda": { + "name": "issue_pda", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pda": { + "name": "pda", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "solver": { + "name": "solver", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "submission_index": { + "name": "submission_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opus_report_hash": { + "name": "opus_report_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tx_hash": { + "name": "tx_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "submission_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "rank": { + "name": "rank", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "scored_at": { + "name": "scored_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "submissions_chain_id_chain_registry_chain_id_fk": { + "name": "submissions_chain_id_chain_registry_chain_id_fk", + "tableFrom": "submissions", + "tableTo": "chain_registry", + "columnsFrom": [ + "chain_id" + ], + "columnsTo": [ + "chain_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "submissions_issue_pda_issues_pda_fk": { + "name": "submissions_issue_pda_issues_pda_fk", + "tableFrom": "submissions", + "tableTo": "issues", + "columnsFrom": [ + "issue_pda" + ], + "columnsTo": [ + "pda" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "submissions_pda_unique": { + "name": "submissions_pda_unique", + "nullsNotDistinct": false, + "columns": [ + "pda" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.treasury_refunds": { + "name": "treasury_refunds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bounty_pda": { + "name": "bounty_pda", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lamports": { + "name": "lamports", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "recipient_pubkey": { + "name": "recipient_pubkey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tx_hash": { + "name": "tx_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "treasury_refunds_bounty_pda_kind_unique": { + "name": "treasury_refunds_bounty_pda_kind_unique", + "nullsNotDistinct": false, + "columns": [ + "bounty_pda", + "kind" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.wallets": { + "name": "wallets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chain_id": { + "name": "chain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_treasury": { + "name": "is_treasury", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_payout": { + "name": "is_payout", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "wallets_user_id_profiles_user_id_fk": { + "name": "wallets_user_id_profiles_user_id_fk", + "tableFrom": "wallets", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "wallets_chain_id_chain_registry_chain_id_fk": { + "name": "wallets_chain_id_chain_registry_chain_id_fk", + "tableFrom": "wallets", + "tableTo": "chain_registry", + "columnsFrom": [ + "chain_id" + ], + "columnsTo": [ + "chain_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "wallets_user_id_chain_id_address_unique": { + "name": "wallets_user_id_chain_id_address_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "chain_id", + "address" + ] + }, + "wallets_chain_id_address_unique": { + "name": "wallets_chain_id_address_unique", + "nullsNotDistinct": false, + "columns": [ + "chain_id", + "address" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.agent_status": { + "name": "agent_status", + "schema": "public", + "values": [ + "pending_oauth", + "pending_stake", + "active", + "suspended", + "revoked" + ] + }, + "public.evaluation_source": { + "name": "evaluation_source", + "schema": "public", + "values": [ + "stub", + "opus", + "genlayer" + ] + }, + "public.issue_state": { + "name": "issue_state", + "schema": "public", + "values": [ + "open", + "resolved", + "cancelled" + ] + }, + "public.release_mode": { + "name": "release_mode", + "schema": "public", + "values": [ + "auto", + "assisted" + ] + }, + "public.stake_status": { + "name": "stake_status", + "schema": "public", + "values": [ + "active", + "frozen", + "slashed", + "refunded" + ] + }, + "public.submission_state": { + "name": "submission_state", + "schema": "public", + "values": [ + "pending", + "scored", + "winner", + "auto_rejected" + ] + }, + "public.user_role": { + "name": "user_role", + "schema": "public", + "values": [ + "company", + "dev" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/0026_snapshot.json b/packages/db/drizzle/meta/0026_snapshot.json new file mode 100644 index 0000000..91a0f27 --- /dev/null +++ b/packages/db/drizzle/meta/0026_snapshot.json @@ -0,0 +1,1842 @@ +{ + "id": "63bd039b-9b13-4809-b4e9-afbaeb453bd4", + "prevId": "aaaaaaaa-0025-0025-0025-aaaaaaaaaaaa", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.agent_delegations": { + "name": "agent_delegations", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "wallet_pubkey": { + "name": "wallet_pubkey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chain_type": { + "name": "chain_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delegated_at": { + "name": "delegated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "agent_delegations_user_id_profiles_user_id_fk": { + "name": "agent_delegations_user_id_profiles_user_id_fk", + "tableFrom": "agent_delegations", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_prefix": { + "name": "key_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_profiles_user_id_fk": { + "name": "api_keys_user_id_profiles_user_id_fk", + "tableFrom": "api_keys", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bounty_meta": { + "name": "bounty_meta", + "schema": "", + "columns": { + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_mode": { + "name": "release_mode", + "type": "release_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'assisted'" + }, + "closed_by_user": { + "name": "closed_by_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "max_submissions": { + "name": "max_submissions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "closed_by_cap_at": { + "name": "closed_by_cap_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cap_warning_sent_at": { + "name": "cap_warning_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reject_threshold": { + "name": "reject_threshold", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "evaluation_criteria": { + "name": "evaluation_criteria", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "review_fee_lamports_paid": { + "name": "review_fee_lamports_paid", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "review_fee_lamports_per_review": { + "name": "review_fee_lamports_per_review", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "bounty_meta_issue_id_issues_id_fk": { + "name": "bounty_meta_issue_id_issues_id_fk", + "tableFrom": "bounty_meta", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bounty_meta_created_by_user_id_profiles_user_id_fk": { + "name": "bounty_meta_created_by_user_id_profiles_user_id_fk", + "tableFrom": "bounty_meta", + "tableTo": "profiles", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chain_registry": { + "name": "chain_registry", + "schema": "", + "columns": { + "chain_id": { + "name": "chain_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rpc_url": { + "name": "rpc_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "escrow_address": { + "name": "escrow_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "explorer_url": { + "name": "explorer_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_symbol": { + "name": "token_symbol", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "x402_supported": { + "name": "x402_supported", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.companies": { + "name": "companies", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "industry": { + "name": "industry", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_org": { + "name": "github_org", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "companies_user_id_profiles_user_id_fk": { + "name": "companies_user_id_profiles_user_id_fk", + "tableFrom": "companies", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "companies_slug_unique": { + "name": "companies_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.developers": { + "name": "developers", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_handle": { + "name": "github_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "skills": { + "name": "skills", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "developers_user_id_profiles_user_id_fk": { + "name": "developers_user_id_profiles_user_id_fk", + "tableFrom": "developers", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "developers_username_unique": { + "name": "developers_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.evaluations": { + "name": "evaluations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "submission_pda": { + "name": "submission_pda", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "evaluation_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "reasoning": { + "name": "reasoning", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "report": { + "name": "report", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "report_hash": { + "name": "report_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tx_hash": { + "name": "tx_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "genlayer_score": { + "name": "genlayer_score", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "genlayer_status": { + "name": "genlayer_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "genlayer_dimensions": { + "name": "genlayer_dimensions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "genlayer_tx_hash": { + "name": "genlayer_tx_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issues": { + "name": "issues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chain_id": { + "name": "chain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pda": { + "name": "pda", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bounty_onchain_id": { + "name": "bounty_onchain_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "creator": { + "name": "creator", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scorer": { + "name": "scorer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mint": { + "name": "mint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "issue_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "submission_count": { + "name": "submission_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "review_eligible_count": { + "name": "review_eligible_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "winner": { + "name": "winner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_issue_url": { + "name": "github_issue_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "issues_chain_id_chain_registry_chain_id_fk": { + "name": "issues_chain_id_chain_registry_chain_id_fk", + "tableFrom": "issues", + "tableTo": "chain_registry", + "columnsFrom": [ + "chain_id" + ], + "columnsTo": [ + "chain_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "issues_pda_unique": { + "name": "issues_pda_unique", + "nullsNotDistinct": false, + "columns": [ + "pda" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_clients": { + "name": "oauth_clients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_codes": { + "name": "oauth_codes", + "schema": "", + "columns": { + "code": { + "name": "code", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oauth_codes_expires_idx": { + "name": "oauth_codes_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_codes_user_id_profiles_user_id_fk": { + "name": "oauth_codes_user_id_profiles_user_id_fk", + "tableFrom": "oauth_codes", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_codes_client_id_oauth_clients_id_fk": { + "name": "oauth_codes_client_id_oauth_clients_id_fk", + "tableFrom": "oauth_codes", + "tableTo": "oauth_clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_tokens": { + "name": "oauth_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['full']::text[]" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oauth_tokens_prefix_idx": { + "name": "oauth_tokens_prefix_idx", + "columns": [ + { + "expression": "token_prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_tokens_user_id_profiles_user_id_fk": { + "name": "oauth_tokens_user_id_profiles_user_id_fk", + "tableFrom": "oauth_tokens", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_tokens_client_id_oauth_clients_id_fk": { + "name": "oauth_tokens_client_id_oauth_clients_id_fk", + "tableFrom": "oauth_tokens", + "tableTo": "oauth_clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_txs": { + "name": "pending_txs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message_hash": { + "name": "message_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expected_signer": { + "name": "expected_signer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pending_txs_user_id_profiles_user_id_fk": { + "name": "pending_txs_user_id_profiles_user_id_fk", + "tableFrom": "pending_txs", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profiles": { + "name": "profiles", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "role": { + "name": "role", + "type": "user_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "onboarding_completed": { + "name": "onboarding_completed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "mcp_status": { + "name": "mcp_status", + "type": "agent_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "warnings": { + "name": "warnings", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "github_handle": { + "name": "github_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wallet_pubkey": { + "name": "wallet_pubkey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "profiles_email_unique": { + "name": "profiles_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "profiles_github_handle_unique": { + "name": "profiles_github_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "github_handle" + ] + }, + "profiles_wallet_pubkey_unique": { + "name": "profiles_wallet_pubkey_unique", + "nullsNotDistinct": false, + "columns": [ + "wallet_pubkey" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slashing_events": { + "name": "slashing_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "evidence": { + "name": "evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "slashing_events_user_id_profiles_user_id_fk": { + "name": "slashing_events_user_id_profiles_user_id_fk", + "tableFrom": "slashing_events", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stake_deposits": { + "name": "stake_deposits", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pda": { + "name": "pda", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tx_signature": { + "name": "tx_signature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_lamports": { + "name": "amount_lamports", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "stake_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_until": { + "name": "locked_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "refunded_at": { + "name": "refunded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "slashed_at": { + "name": "slashed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "stake_deposits_user_id_profiles_user_id_fk": { + "name": "stake_deposits_user_id_profiles_user_id_fk", + "tableFrom": "stake_deposits", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "stake_deposits_pda_unique": { + "name": "stake_deposits_pda_unique", + "nullsNotDistinct": false, + "columns": [ + "pda" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.submission_meta": { + "name": "submission_meta", + "schema": "", + "columns": { + "submission_id": { + "name": "submission_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "submitted_by_user_id": { + "name": "submitted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "submission_meta_submission_id_submissions_id_fk": { + "name": "submission_meta_submission_id_submissions_id_fk", + "tableFrom": "submission_meta", + "tableTo": "submissions", + "columnsFrom": [ + "submission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "submission_meta_submitted_by_user_id_profiles_user_id_fk": { + "name": "submission_meta_submitted_by_user_id_profiles_user_id_fk", + "tableFrom": "submission_meta", + "tableTo": "profiles", + "columnsFrom": [ + "submitted_by_user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.submissions": { + "name": "submissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chain_id": { + "name": "chain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_pda": { + "name": "issue_pda", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pda": { + "name": "pda", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "solver": { + "name": "solver", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "submission_index": { + "name": "submission_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opus_report_hash": { + "name": "opus_report_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tx_hash": { + "name": "tx_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "submission_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "rank": { + "name": "rank", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "scored_at": { + "name": "scored_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "submissions_chain_id_chain_registry_chain_id_fk": { + "name": "submissions_chain_id_chain_registry_chain_id_fk", + "tableFrom": "submissions", + "tableTo": "chain_registry", + "columnsFrom": [ + "chain_id" + ], + "columnsTo": [ + "chain_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "submissions_issue_pda_issues_pda_fk": { + "name": "submissions_issue_pda_issues_pda_fk", + "tableFrom": "submissions", + "tableTo": "issues", + "columnsFrom": [ + "issue_pda" + ], + "columnsTo": [ + "pda" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "submissions_pda_unique": { + "name": "submissions_pda_unique", + "nullsNotDistinct": false, + "columns": [ + "pda" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.treasury_refunds": { + "name": "treasury_refunds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bounty_pda": { + "name": "bounty_pda", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lamports": { + "name": "lamports", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "recipient_pubkey": { + "name": "recipient_pubkey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tx_hash": { + "name": "tx_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "treasury_refunds_bounty_pda_kind_unique": { + "name": "treasury_refunds_bounty_pda_kind_unique", + "nullsNotDistinct": false, + "columns": [ + "bounty_pda", + "kind" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.wallets": { + "name": "wallets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chain_id": { + "name": "chain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_treasury": { + "name": "is_treasury", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_payout": { + "name": "is_payout", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "wallets_user_id_profiles_user_id_fk": { + "name": "wallets_user_id_profiles_user_id_fk", + "tableFrom": "wallets", + "tableTo": "profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "wallets_chain_id_chain_registry_chain_id_fk": { + "name": "wallets_chain_id_chain_registry_chain_id_fk", + "tableFrom": "wallets", + "tableTo": "chain_registry", + "columnsFrom": [ + "chain_id" + ], + "columnsTo": [ + "chain_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "wallets_user_id_chain_id_address_unique": { + "name": "wallets_user_id_chain_id_address_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "chain_id", + "address" + ] + }, + "wallets_chain_id_address_unique": { + "name": "wallets_chain_id_address_unique", + "nullsNotDistinct": false, + "columns": [ + "chain_id", + "address" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.agent_status": { + "name": "agent_status", + "schema": "public", + "values": [ + "pending_oauth", + "pending_stake", + "active", + "suspended", + "revoked" + ] + }, + "public.evaluation_source": { + "name": "evaluation_source", + "schema": "public", + "values": [ + "stub", + "opus", + "genlayer" + ] + }, + "public.issue_state": { + "name": "issue_state", + "schema": "public", + "values": [ + "open", + "resolved", + "cancelled" + ] + }, + "public.release_mode": { + "name": "release_mode", + "schema": "public", + "values": [ + "auto", + "assisted" + ] + }, + "public.stake_status": { + "name": "stake_status", + "schema": "public", + "values": [ + "active", + "frozen", + "slashed", + "refunded" + ] + }, + "public.submission_state": { + "name": "submission_state", + "schema": "public", + "values": [ + "pending", + "scored", + "winner", + "auto_rejected" + ] + }, + "public.user_role": { + "name": "user_role", + "schema": "public", + "values": [ + "company", + "dev" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index ea0f15b..4e2b162 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -50,6 +50,13 @@ "when": 1779057697000, "tag": "0025_bypass_stake", "breakpoints": true + }, + { + "idx": 23, + "version": "7", + "when": 1779062400000, + "tag": "0026_agent_delegations", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 390ed20..29083ab 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -204,6 +204,25 @@ export const profiles = pgTable("profiles", { .notNull(), }); +/* --- Agent delegations: server-side signing consent ------------ */ +export const agentDelegations = pgTable("agent_delegations", { + userId: text("user_id") + .primaryKey() + .references(() => profiles.userId, { onDelete: "cascade" }), + walletPubkey: text("wallet_pubkey").notNull(), + chainType: text("chain_type").notNull(), + delegatedAt: timestamp("delegated_at", { withTimezone: true }) + .default(sql`now()`) + .notNull(), + revokedAt: timestamp("revoked_at", { withTimezone: true }), + createdAt: timestamp("created_at", { withTimezone: true }) + .default(sql`now()`) + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .default(sql`now()`) + .notNull(), +}); + /* --- Companies: populated when profiles.role = 'company' -------- */ export const companies = pgTable("companies", { userId: text("user_id") From adb4c043a12c403268bbd2e7cdf11aea8ad3dcdd Mon Sep 17 00:00:00 2001 From: gastonfoncea Date: Mon, 18 May 2026 18:29:52 -0300 Subject: [PATCH 05/21] =?UTF-8?q?feat(shared):=20add=20verifyPrOwnership?= =?UTF-8?q?=20helper=20=E2=80=94=20GHB-187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../shared/src/github/verify-pr-ownership.ts | 88 +++++++++++++ packages/shared/src/index.ts | 5 + .../tests/github/verify-pr-ownership.test.ts | 123 ++++++++++++++++++ 3 files changed, 216 insertions(+) create mode 100644 packages/shared/src/github/verify-pr-ownership.ts create mode 100644 packages/shared/tests/github/verify-pr-ownership.test.ts diff --git a/packages/shared/src/github/verify-pr-ownership.ts b/packages/shared/src/github/verify-pr-ownership.ts new file mode 100644 index 0000000..f61d0ac --- /dev/null +++ b/packages/shared/src/github/verify-pr-ownership.ts @@ -0,0 +1,88 @@ +/** + * Pure function. Calls GitHub REST API to verify that a PR was opened by + * the expected user against the expected repo. Used by both the MCP server + * (pre-check before submit_pr) and the relayer (post-check before scoring). + * + * Token: pass `GITHUB_TOKEN` (server-side env). Public-repo reads work + * unauthenticated but with lower rate limit; provide a PAT for safety. + */ + +export type VerifyPrOwnershipInput = { + prUrl: string; + expectedGithubHandle: string; + /** Full URL like `https://github.com/owner/repo` (no trailing slash). */ + expectedRepoUrl: string; + /** Optional GitHub token for higher rate limit. */ + token?: string; +}; + +export type VerifyPrOwnershipResult = + | { ok: true } + | { + ok: false; + reason: + | "pr_not_found" + | "author_mismatch" + | "repo_mismatch" + | "rate_limited" + | "invalid_url" + | "upstream_error"; + }; + +const PR_URL_RE = + /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/; + +export async function verifyPrOwnership( + input: VerifyPrOwnershipInput +): Promise { + const match = input.prUrl.match(PR_URL_RE); + if (!match) return { ok: false, reason: "invalid_url" }; + + const [, owner, repo, prNumber] = match; + const apiUrl = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`; + + const headers: Record = { + Accept: "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + }; + if (input.token) headers.Authorization = `Bearer ${input.token}`; + + let res: Response; + try { + res = await fetch(apiUrl, { headers }); + } catch { + return { ok: false, reason: "upstream_error" }; + } + + if (res.status === 404) return { ok: false, reason: "pr_not_found" }; + + if (res.status === 403 && res.headers.get("x-ratelimit-remaining") === "0") { + return { ok: false, reason: "rate_limited" }; + } + + if (!res.ok) return { ok: false, reason: "upstream_error" }; + + let body: { user: { login: string } | null; base: { repo: { html_url: string } | null } | null }; + try { + body = (await res.json()) as typeof body; + } catch { + return { ok: false, reason: "upstream_error" }; + } + + const authorLogin = body?.user?.login; + const repoHtmlUrl = body?.base?.repo?.html_url; + if (!authorLogin || !repoHtmlUrl) { + return { ok: false, reason: "upstream_error" }; + } + + if (authorLogin.toLowerCase() !== input.expectedGithubHandle.toLowerCase()) { + return { ok: false, reason: "author_mismatch" }; + } + + const normalize = (u: string) => u.replace(/\/$/, "").toLowerCase(); + if (normalize(repoHtmlUrl) !== normalize(input.expectedRepoUrl)) { + return { ok: false, reason: "repo_mismatch" }; + } + + return { ok: true }; +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 4a190bf..aac58f4 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -2,3 +2,8 @@ export * from "./chains"; export * from "./gas-station/index"; export * from "./api-key"; export * from "./oauth-token"; +export { verifyPrOwnership } from "./github/verify-pr-ownership"; +export type { + VerifyPrOwnershipInput, + VerifyPrOwnershipResult, +} from "./github/verify-pr-ownership"; diff --git a/packages/shared/tests/github/verify-pr-ownership.test.ts b/packages/shared/tests/github/verify-pr-ownership.test.ts new file mode 100644 index 0000000..e36f87a --- /dev/null +++ b/packages/shared/tests/github/verify-pr-ownership.test.ts @@ -0,0 +1,123 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { verifyPrOwnership } from "../../src/github/verify-pr-ownership"; + +describe("verifyPrOwnership", () => { + let fetchMock: ReturnType; + + beforeEach(() => { + fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("returns ok when author and repo match", async () => { + fetchMock.mockResolvedValueOnce( + new Response( + JSON.stringify({ + user: { login: "alice" }, + base: { repo: { html_url: "https://github.com/acme/proj" } }, + }), + { status: 200 } + ) + ); + + const result = await verifyPrOwnership({ + prUrl: "https://github.com/acme/proj/pull/42", + expectedGithubHandle: "alice", + expectedRepoUrl: "https://github.com/acme/proj", + }); + + expect(result).toEqual({ ok: true }); + }); + + it("returns author_mismatch when login differs", async () => { + fetchMock.mockResolvedValueOnce( + new Response( + JSON.stringify({ + user: { login: "mallory" }, + base: { repo: { html_url: "https://github.com/acme/proj" } }, + }), + { status: 200 } + ) + ); + + const result = await verifyPrOwnership({ + prUrl: "https://github.com/acme/proj/pull/42", + expectedGithubHandle: "alice", + expectedRepoUrl: "https://github.com/acme/proj", + }); + + expect(result).toEqual({ ok: false, reason: "author_mismatch" }); + }); + + it("returns repo_mismatch when PR is in a different repo", async () => { + fetchMock.mockResolvedValueOnce( + new Response( + JSON.stringify({ + user: { login: "alice" }, + base: { repo: { html_url: "https://github.com/other/repo" } }, + }), + { status: 200 } + ) + ); + + const result = await verifyPrOwnership({ + prUrl: "https://github.com/acme/proj/pull/42", + expectedGithubHandle: "alice", + expectedRepoUrl: "https://github.com/acme/proj", + }); + + expect(result).toEqual({ ok: false, reason: "repo_mismatch" }); + }); + + it("returns pr_not_found on 404", async () => { + fetchMock.mockResolvedValueOnce(new Response("", { status: 404 })); + + const result = await verifyPrOwnership({ + prUrl: "https://github.com/acme/proj/pull/9999", + expectedGithubHandle: "alice", + expectedRepoUrl: "https://github.com/acme/proj", + }); + + expect(result).toEqual({ ok: false, reason: "pr_not_found" }); + }); + + it("returns rate_limited on 403 with rate-limit header", async () => { + fetchMock.mockResolvedValueOnce( + new Response("", { + status: 403, + headers: { "x-ratelimit-remaining": "0" }, + }) + ); + + const result = await verifyPrOwnership({ + prUrl: "https://github.com/acme/proj/pull/42", + expectedGithubHandle: "alice", + expectedRepoUrl: "https://github.com/acme/proj", + }); + + expect(result).toEqual({ ok: false, reason: "rate_limited" }); + }); + + it("returns invalid_url when the URL doesn't match the PR pattern", async () => { + const result = await verifyPrOwnership({ + prUrl: "https://gitlab.com/owner/repo/pull/1", + expectedGithubHandle: "alice", + expectedRepoUrl: "https://github.com/acme/proj", + }); + expect(result).toEqual({ ok: false, reason: "invalid_url" }); + }); + + it("returns upstream_error when fetch throws (e.g. DNS failure)", async () => { + fetchMock.mockRejectedValueOnce(new Error("ENOTFOUND")); + const result = await verifyPrOwnership({ + prUrl: "https://github.com/acme/proj/pull/42", + expectedGithubHandle: "alice", + expectedRepoUrl: "https://github.com/acme/proj", + }); + expect(result).toEqual({ ok: false, reason: "upstream_error" }); + }); +}); From 0a5948c25e3daf8c433f103bef5efe253f130ba9 Mon Sep 17 00:00:00 2001 From: gastonfoncea Date: Mon, 18 May 2026 18:39:07 -0300 Subject: [PATCH 06/21] =?UTF-8?q?chore(mcp):=20add=20@privy-io/node=20depe?= =?UTF-8?q?ndency=20=E2=80=94=20GHB-187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/mcp/package.json | 3 +- pnpm-lock.yaml | 700 ++++++++++++++++++++++++------------------ 2 files changed, 400 insertions(+), 303 deletions(-) diff --git a/apps/mcp/package.json b/apps/mcp/package.json index 68036e2..24ef99d 100644 --- a/apps/mcp/package.json +++ b/apps/mcp/package.json @@ -15,12 +15,13 @@ "@ghbounty/sdk": "workspace:^", "@ghbounty/shared": "workspace:^", "@modelcontextprotocol/sdk": "^1.0.0", + "@privy-io/node": "^0.18.0", "@solana/kit": "^6.9.0", "@supabase/supabase-js": "^2.104.1", "@upstash/ratelimit": "^2.0.8", "@upstash/redis": "^1.38.0", - "mcp-handler": "^1.1.0", "bcryptjs": "^3.0.3", + "mcp-handler": "^1.1.0", "next": "16.2.4", "react": "19.2.4", "react-dom": "19.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3078069..135d720 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: '@modelcontextprotocol/sdk': specifier: ^1.0.0 version: 1.29.0(zod@3.25.76) + '@privy-io/node': + specifier: ^0.18.0 + version: 0.18.0(@solana/kit@6.9.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) '@solana/kit': specifier: ^6.9.0 version: 6.9.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) @@ -80,28 +83,28 @@ importers: dependencies: '@coral-xyz/anchor': specifier: ^0.30.1 - version: 0.30.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + version: 0.30.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@ghbounty/shared': specifier: workspace:^ version: link:../packages/shared '@privy-io/react-auth': specifier: ^3.22.2 - version: 3.22.2(@solana-program/memo@0.11.0(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)))(@solana-program/system@0.10.0(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)))(@solana-program/token@0.9.0(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)))(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(@solana/sysvars@6.9.0(typescript@5.9.3))(@tanstack/query-core@5.100.6)(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@6.0.6)(zod@3.25.76) + version: 3.22.2(@solana-program/memo@0.11.0(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)))(@solana-program/system@0.10.0(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)))(@solana-program/token@0.9.0(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)))(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@solana/sysvars@6.9.0(typescript@5.9.3))(@tanstack/query-core@5.100.6)(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(zod@3.25.76) '@solana-program/memo': specifier: ^0.11.0 - version: 0.11.0(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)) + version: 0.11.0(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) '@solana/kit': specifier: ^6.8.0 - version: 6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + version: 6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@solana/web3.js': specifier: ^1.95.0 - version: 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + version: 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@supabase/ssr': specifier: ^0.10.2 - version: 0.10.2(@supabase/supabase-js@2.104.1(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + version: 0.10.2(@supabase/supabase-js@2.104.1(bufferutil@4.1.0)(utf-8-validate@5.0.10)) '@supabase/supabase-js': specifier: ^2.104.1 - version: 2.104.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) + version: 2.104.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) bs58: specifier: ^6.0.0 version: 6.0.0 @@ -239,7 +242,7 @@ importers: dependencies: '@solana/web3.js': specifier: ^1.95.0 - version: 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + version: 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) bcryptjs: specifier: ^3.0.3 version: 3.0.3 @@ -258,7 +261,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.1.9 - version: 2.1.9(@types/node@20.19.39)(happy-dom@20.9.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(lightningcss@1.32.0) + version: 2.1.9(@types/node@20.19.39)(happy-dom@20.9.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(lightningcss@1.32.0) relayer: dependencies: @@ -1181,6 +1184,18 @@ packages: peerDependencies: hono: ^4 + '@hpke/chacha20poly1305@1.8.0': + resolution: {integrity: sha512-FcBfAQ+Y99vMNJP2yrZ9wpL8V0GOwp1+zMyzvc6alasrBygfFjFm1yeUtyADJCu/27C3Lm5mJzx6u7pwg+cX5w==} + engines: {node: '>=16.0.0'} + + '@hpke/common@1.10.1': + resolution: {integrity: sha512-moJwhmtLtuxiUzzNp1jpfBfx8yefKoO9D/RCR9dmwrnc7qjJqId1rEtQz+lSlU5cabX8daToMSx/7HayXOiaFw==} + engines: {node: '>=16.0.0'} + + '@hpke/core@1.9.0': + resolution: {integrity: sha512-pFxWl1nNJeQCSUFs7+GAblHvXBCjn9EPN65vdKlYQil2aURaRxfGMO6vBKGqm1YHTKwiAxJQNEI70PbSowMP9Q==} + engines: {node: '>=16.0.0'} + '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -1664,6 +1679,26 @@ packages: viem: optional: true + '@privy-io/node@0.18.0': + resolution: {integrity: sha512-DA+/f+nqJGB3lJ6b2qpan0OWIyAhoSXd/Cfo40Ww9fSXC3KKQ6eVHnOKiEO6KQ1DpVdswkkuBoG9uK9YHUST9w==} + peerDependencies: + '@solana/kit': ^5.1.0 + '@x402/evm': ^2.3.0 + '@x402/fetch': ^2.3.0 + '@x402/svm': ^2.3.0 + viem: ^2.24.1 + peerDependenciesMeta: + '@solana/kit': + optional: true + '@x402/evm': + optional: true + '@x402/fetch': + optional: true + '@x402/svm': + optional: true + viem: + optional: true + '@privy-io/popup@0.0.4': resolution: {integrity: sha512-Lk5BqB//F9naVOR9tkHbrcGs55fM4ILS50tdsgIQ76Kr9i7Og4Ob5IRGIKb75P11f5hXTwAjMezRmWM/7uAsrw==} @@ -3830,6 +3865,9 @@ packages: '@solana/web3.js@1.98.4': resolution: {integrity: sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==} + '@stablelib/base64@1.0.1': + resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} + '@supabase/auth-js@2.104.1': resolution: {integrity: sha512-pqFnDKekq1isqlqnzqzyJ3mzmho+o+FjfVTqhKY3PFlwj2anx3OPznO1kbo1ZEwD8zg1r4EAFf/7pplLyX0ocQ==} engines: {node: '>=20.0.0'} @@ -5402,6 +5440,9 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + fast-stable-stringify@1.0.0: resolution: {integrity: sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==} @@ -6893,6 +6934,9 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + standardwebhooks@1.0.0: + resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -7016,6 +7060,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svix@1.93.0: + resolution: {integrity: sha512-AeCcSs+CrHNejZytBuvD4hw2B14rB7+Sq7ggwYgF22TgXh0uJJ3T4uVJSbSYKFSbO1AA4o470XoGgOYqu2fbSA==} + tabbable@6.4.0: resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} @@ -7765,7 +7812,7 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@base-org/account@1.1.1(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@6.0.6)(zod@3.25.76)': + '@base-org/account@1.1.1(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@noble/hashes': 1.4.0 clsx: 1.2.1 @@ -7773,7 +7820,7 @@ snapshots: idb-keyval: 6.2.1 ox: 0.6.9(typescript@5.9.3)(zod@3.25.76) preact: 10.24.2 - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) zustand: 5.0.3(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.4.0(react@19.2.4)) transitivePeerDependencies: - '@types/react' @@ -7785,16 +7832,16 @@ snapshots: - utf-8-validate - zod - '@base-org/account@2.4.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@6.0.6)(zod@3.25.76)': + '@base-org/account@2.4.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@coinbase/cdp-sdk': 1.48.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@coinbase/cdp-sdk': 1.48.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@noble/hashes': 1.4.0 clsx: 1.2.1 eventemitter3: 5.0.1 idb-keyval: 6.2.1 ox: 0.6.9(typescript@5.9.3)(zod@3.25.76) preact: 10.24.2 - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) zustand: 5.0.3(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.4.0(react@19.2.4)) transitivePeerDependencies: - '@types/react' @@ -7878,18 +7925,18 @@ snapshots: '@codama/nodes': 1.6.0 '@codama/visitors-core': 1.6.0 - '@coinbase/cdp-sdk@1.48.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': + '@coinbase/cdp-sdk@1.48.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': dependencies: - '@solana-program/system': 0.10.0(@solana/kit@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)) - '@solana-program/token': 0.9.0(@solana/kit@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)) - '@solana/kit': 5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana-program/system': 0.10.0(@solana/kit@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) + '@solana-program/token': 0.9.0(@solana/kit@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) + '@solana/kit': 5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) abitype: 1.0.6(typescript@5.9.3)(zod@3.25.76) axios: 1.13.6 axios-retry: 4.5.0(axios@1.13.6) jose: 6.2.3 md5: 2.3.0 uncrypto: 0.1.3 - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) zod: 3.25.76 transitivePeerDependencies: - bufferutil @@ -7919,7 +7966,7 @@ snapshots: eventemitter3: 5.0.4 preact: 10.29.1 - '@coinbase/wallet-sdk@4.3.6(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@6.0.6)(zod@3.25.76)': + '@coinbase/wallet-sdk@4.3.6(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@noble/hashes': 1.4.0 clsx: 1.2.1 @@ -7927,7 +7974,7 @@ snapshots: idb-keyval: 6.2.1 ox: 0.6.9(typescript@5.9.3)(zod@3.25.76) preact: 10.24.2 - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) zustand: 5.0.3(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.4.0(react@19.2.4)) transitivePeerDependencies: - '@types/react' @@ -7964,41 +8011,12 @@ snapshots: - typescript - utf-8-validate - '@coral-xyz/anchor@0.30.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': - dependencies: - '@coral-xyz/anchor-errors': 0.30.1 - '@coral-xyz/borsh': 0.30.1(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)) - '@noble/hashes': 1.8.0 - '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) - bn.js: 5.2.3 - bs58: 4.0.1 - buffer-layout: 1.2.2 - camelcase: 6.3.0 - cross-fetch: 3.2.0 - crypto-hash: 1.3.0 - eventemitter3: 4.0.7 - pako: 2.1.0 - snake-case: 3.0.4 - superstruct: 0.15.5 - toml: 3.0.0 - transitivePeerDependencies: - - bufferutil - - encoding - - typescript - - utf-8-validate - '@coral-xyz/borsh@0.30.1(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))': dependencies: '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) bn.js: 5.2.3 buffer-layout: 1.2.2 - '@coral-xyz/borsh@0.30.1(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))': - dependencies: - '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) - bn.js: 5.2.3 - buffer-layout: 1.2.2 - '@drizzle-team/brocli@0.10.2': {} '@ecies/ciphers@0.2.6(@noble/ciphers@1.3.0)': @@ -8419,11 +8437,11 @@ snapshots: '@floating-ui/utils@0.2.11': {} - '@gemini-wallet/core@0.3.2(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))': + '@gemini-wallet/core@0.3.2(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: '@metamask/rpc-errors': 7.0.2 eventemitter3: 5.0.1 - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - supports-color @@ -8454,6 +8472,16 @@ snapshots: dependencies: hono: 4.12.15 + '@hpke/chacha20poly1305@1.8.0': + dependencies: + '@hpke/common': 1.10.1 + + '@hpke/common@1.10.1': {} + + '@hpke/core@1.9.0': + dependencies: + '@hpke/common': 1.10.1 + '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -8695,7 +8723,7 @@ snapshots: dependencies: openapi-fetch: 0.13.8 - '@metamask/sdk-communication-layer@0.33.1(cross-fetch@4.1.0)(eciesjs@0.4.18)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@metamask/sdk-communication-layer@0.33.1(cross-fetch@4.1.0)(eciesjs@0.4.18)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))': dependencies: '@metamask/sdk-analytics': 0.0.5 bufferutil: 4.1.0 @@ -8705,7 +8733,7 @@ snapshots: eciesjs: 0.4.18 eventemitter2: 6.4.9 readable-stream: 3.6.2 - socket.io-client: 4.8.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) + socket.io-client: 4.8.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) utf-8-validate: 5.0.10 uuid: 8.3.2 transitivePeerDependencies: @@ -8715,13 +8743,13 @@ snapshots: dependencies: '@paulmillr/qr': 0.2.1 - '@metamask/sdk@0.33.1(bufferutil@4.1.0)(utf-8-validate@6.0.6)': + '@metamask/sdk@0.33.1(bufferutil@4.1.0)(utf-8-validate@5.0.10)': dependencies: '@babel/runtime': 7.29.2 '@metamask/onboarding': 1.0.1 '@metamask/providers': 16.1.0 '@metamask/sdk-analytics': 0.0.5 - '@metamask/sdk-communication-layer': 0.33.1(cross-fetch@4.1.0)(eciesjs@0.4.18)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@metamask/sdk-communication-layer': 0.33.1(cross-fetch@4.1.0)(eciesjs@0.4.18)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) '@metamask/sdk-install-modal-web': 0.32.1 '@paulmillr/qr': 0.2.1 bowser: 2.14.1 @@ -8733,7 +8761,7 @@ snapshots: obj-multiplex: 1.0.0 pump: 3.0.4 readable-stream: 3.6.2 - socket.io-client: 4.8.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) + socket.io-client: 4.8.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) tslib: 2.8.1 util: 0.12.5 uuid: 8.3.2 @@ -8955,9 +8983,9 @@ snapshots: '@privy-io/api-types@0.9.0': {} - '@privy-io/are-addresses-equal@0.0.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@privy-io/are-addresses-equal@0.0.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - bufferutil - typescript @@ -8970,17 +8998,17 @@ snapshots: dependencies: '@scure/base': 1.2.6 - '@privy-io/ethereum@0.0.11(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))': + '@privy-io/ethereum@0.0.11(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@privy-io/js-sdk-core@0.61.1(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))': + '@privy-io/js-sdk-core@0.61.1(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: '@privy-io/api-base': 1.9.0 '@privy-io/api-types': 0.9.0 '@privy-io/chains': 0.2.0 '@privy-io/encoding': 0.1.3 - '@privy-io/ethereum': 0.0.11(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + '@privy-io/ethereum': 0.0.11(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) '@privy-io/routes': 0.0.12 canonicalize: 2.1.0 eventemitter3: 5.0.4 @@ -8991,13 +9019,28 @@ snapshots: set-cookie-parser: 2.7.2 uuid: 8.3.2 optionalDependencies: + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + + '@privy-io/node@0.18.0(@solana/kit@6.9.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))': + dependencies: + '@hpke/chacha20poly1305': 1.8.0 + '@hpke/core': 1.9.0 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + canonicalize: 2.1.0 + jose: 6.2.3 + lru-cache: 11.3.5 + svix: 1.93.0 + optionalDependencies: + '@solana/kit': 6.9.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) '@privy-io/popup@0.0.4': {} - '@privy-io/react-auth@3.22.2(@solana-program/memo@0.11.0(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)))(@solana-program/system@0.10.0(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)))(@solana-program/token@0.9.0(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)))(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(@solana/sysvars@6.9.0(typescript@5.9.3))(@tanstack/query-core@5.100.6)(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@6.0.6)(zod@3.25.76)': + '@privy-io/react-auth@3.22.2(@solana-program/memo@0.11.0(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)))(@solana-program/system@0.10.0(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)))(@solana-program/token@0.9.0(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)))(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@solana/sysvars@6.9.0(typescript@5.9.3))(@tanstack/query-core@5.100.6)(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@base-org/account': 1.1.1(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@6.0.6)(zod@3.25.76) + '@base-org/account': 1.1.1(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(zod@3.25.76) '@coinbase/wallet-sdk': 4.3.2 '@floating-ui/react': 0.26.28(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@hcaptcha/react-hcaptcha': 1.17.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -9006,11 +9049,11 @@ snapshots: '@marsidev/react-turnstile': 1.5.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@privy-io/api-base': 1.9.0 '@privy-io/api-types': 0.9.0 - '@privy-io/are-addresses-equal': 0.0.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@privy-io/are-addresses-equal': 0.0.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@privy-io/chains': 0.2.0 '@privy-io/encoding': 0.1.3 - '@privy-io/ethereum': 0.0.11(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) - '@privy-io/js-sdk-core': 0.61.1(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + '@privy-io/ethereum': 0.0.11(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@privy-io/js-sdk-core': 0.61.1(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) '@privy-io/popup': 0.0.4 '@privy-io/routes': 0.0.12 '@privy-io/urls': 0.0.4 @@ -9018,8 +9061,8 @@ snapshots: '@simplewebauthn/browser': 13.3.0 '@tanstack/react-virtual': 3.13.24(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@wallet-standard/app': 1.1.0 - '@walletconnect/ethereum-provider': 2.22.4(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@walletconnect/universal-provider': 2.22.4(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@walletconnect/ethereum-provider': 2.22.4(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/universal-provider': 2.22.4(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) eventemitter3: 5.0.4 fast-password-entropy: 1.1.1 jose: 4.15.9 @@ -9037,14 +9080,14 @@ snapshots: stylis: 4.4.0 tinycolor2: 1.6.0 uuid: 8.3.2 - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - x402: 0.7.3(@solana/sysvars@6.9.0(typescript@5.9.3))(@tanstack/query-core@5.100.6)(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + x402: 0.7.3(@solana/sysvars@6.9.0(typescript@5.9.3))(@tanstack/query-core@5.100.6)(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10) zustand: 5.0.12(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.4.0(react@19.2.4)) optionalDependencies: - '@solana-program/memo': 0.11.0(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)) - '@solana-program/system': 0.10.0(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)) - '@solana-program/token': 0.9.0(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)) - '@solana/kit': 6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana-program/memo': 0.11.0(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) + '@solana-program/system': 0.10.0(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) + '@solana-program/token': 0.9.0(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) + '@solana/kit': 6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -9883,57 +9926,57 @@ snapshots: dependencies: '@redis/client': 1.6.1 - '@reown/appkit-common@1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.22.4)': + '@reown/appkit-common@1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: big.js: 6.2.2 dayjs: 1.11.13 - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.22.4) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.22.4) transitivePeerDependencies: - bufferutil - typescript - utf-8-validate - zod - '@reown/appkit-common@1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@reown/appkit-common@1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: big.js: 6.2.2 dayjs: 1.11.13 - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - bufferutil - typescript - utf-8-validate - zod - '@reown/appkit-common@1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.22.4)': + '@reown/appkit-common@1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: big.js: 6.2.2 dayjs: 1.11.13 - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.22.4) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.22.4) transitivePeerDependencies: - bufferutil - typescript - utf-8-validate - zod - '@reown/appkit-common@1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@reown/appkit-common@1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: big.js: 6.2.2 dayjs: 1.11.13 - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - bufferutil - typescript - utf-8-validate - zod - '@reown/appkit-controllers@1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@reown/appkit-controllers@1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) - '@walletconnect/universal-provider': 2.21.0(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@walletconnect/universal-provider': 2.21.0(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) valtio: 1.13.2(@types/react@19.2.14)(react@19.2.4) - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -9962,13 +10005,13 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-controllers@1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@reown/appkit-controllers@1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-wallet': 1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) - '@walletconnect/universal-provider': 2.21.9(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-common': 1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-wallet': 1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@walletconnect/universal-provider': 2.21.9(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) valtio: 2.1.7(@types/react@19.2.14)(react@19.2.4) - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -9997,12 +10040,12 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-pay@1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@reown/appkit-pay@1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-ui': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-utils': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(valtio@1.13.2(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76) lit: 3.3.0 valtio: 1.13.2(@types/react@19.2.14)(react@19.2.4) transitivePeerDependencies: @@ -10033,12 +10076,12 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-pay@1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@reown/appkit-pay@1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-controllers': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-ui': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-utils': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(valtio@2.1.7(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76) + '@reown/appkit-common': 1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-ui': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76) lit: 3.3.0 valtio: 2.1.7(@types/react@19.2.14)(react@19.2.4) transitivePeerDependencies: @@ -10077,13 +10120,13 @@ snapshots: dependencies: buffer: 6.0.3 - '@reown/appkit-scaffold-ui@1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(valtio@1.13.2(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76)': + '@reown/appkit-scaffold-ui@1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-ui': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-utils': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(valtio@1.13.2(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76) - '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lit: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -10114,13 +10157,13 @@ snapshots: - valtio - zod - '@reown/appkit-scaffold-ui@1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(valtio@2.1.7(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76)': + '@reown/appkit-scaffold-ui@1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-controllers': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-ui': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-utils': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(valtio@2.1.7(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76) - '@reown/appkit-wallet': 1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@reown/appkit-common': 1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-ui': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76) + '@reown/appkit-wallet': 1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lit: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -10151,11 +10194,11 @@ snapshots: - valtio - zod - '@reown/appkit-ui@1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@reown/appkit-ui@1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lit: 3.3.0 qrcode: 1.5.3 transitivePeerDependencies: @@ -10186,12 +10229,12 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-ui@1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@reown/appkit-ui@1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@phosphor-icons/webcomponents': 2.1.5 - '@reown/appkit-common': 1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-controllers': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-wallet': 1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@reown/appkit-common': 1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-wallet': 1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) lit: 3.3.0 qrcode: 1.5.3 transitivePeerDependencies: @@ -10222,16 +10265,16 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-utils@1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(valtio@1.13.2(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76)': + '@reown/appkit-utils@1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-polyfills': 1.7.8 - '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@walletconnect/logger': 2.1.2 - '@walletconnect/universal-provider': 2.21.0(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@walletconnect/universal-provider': 2.21.0(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) valtio: 1.13.2(@types/react@19.2.14)(react@19.2.4) - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -10260,17 +10303,17 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-utils@1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(valtio@2.1.7(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76)': + '@reown/appkit-utils@1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-controllers': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-common': 1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-polyfills': 1.8.9 - '@reown/appkit-wallet': 1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@reown/appkit-wallet': 1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@wallet-standard/wallet': 1.1.0 '@walletconnect/logger': 2.1.2 - '@walletconnect/universal-provider': 2.21.9(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@walletconnect/universal-provider': 2.21.9(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) valtio: 2.1.7(@types/react@19.2.14)(react@19.2.4) - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -10299,9 +10342,9 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-wallet@1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': + '@reown/appkit-wallet@1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.22.4) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.22.4) '@reown/appkit-polyfills': 1.7.8 '@walletconnect/logger': 2.1.2 zod: 3.22.4 @@ -10310,9 +10353,9 @@ snapshots: - typescript - utf-8-validate - '@reown/appkit-wallet@1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': + '@reown/appkit-wallet@1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': dependencies: - '@reown/appkit-common': 1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.22.4) + '@reown/appkit-common': 1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.22.4) '@reown/appkit-polyfills': 1.8.9 '@walletconnect/logger': 2.1.2 zod: 3.22.4 @@ -10321,21 +10364,21 @@ snapshots: - typescript - utf-8-validate - '@reown/appkit@1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@reown/appkit@1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-pay': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-pay': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-polyfills': 1.7.8 - '@reown/appkit-scaffold-ui': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(valtio@1.13.2(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76) - '@reown/appkit-ui': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-utils': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(valtio@1.13.2(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76) - '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@reown/appkit-scaffold-ui': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@walletconnect/types': 2.21.0(@upstash/redis@1.38.0) - '@walletconnect/universal-provider': 2.21.0(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@walletconnect/universal-provider': 2.21.0(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) bs58: 6.0.0 valtio: 1.13.2(@types/react@19.2.14)(react@19.2.4) - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -10364,21 +10407,21 @@ snapshots: - utf-8-validate - zod - '@reown/appkit@1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@reown/appkit@1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-controllers': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-pay': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-common': 1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-pay': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-polyfills': 1.8.9 - '@reown/appkit-scaffold-ui': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(valtio@2.1.7(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76) - '@reown/appkit-ui': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@reown/appkit-utils': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(valtio@2.1.7(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76) - '@reown/appkit-wallet': 1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) - '@walletconnect/universal-provider': 2.21.9(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit-scaffold-ui': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76) + '@reown/appkit-ui': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@2.1.7(@types/react@19.2.14)(react@19.2.4))(zod@3.25.76) + '@reown/appkit-wallet': 1.8.9(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@walletconnect/universal-provider': 2.21.9(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) bs58: 6.0.0 semver: 7.7.2 valtio: 2.1.7(@types/react@19.2.14)(react@19.2.4) - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: '@lit/react': 1.0.8(@types/react@19.2.14) transitivePeerDependencies: @@ -10486,9 +10529,9 @@ snapshots: '@rtsao/scc@1.1.0': {} - '@safe-global/safe-apps-provider@0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@safe-global/safe-apps-provider@0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) events: 3.3.0 transitivePeerDependencies: - bufferutil @@ -10496,10 +10539,10 @@ snapshots: - utf-8-validate - zod - '@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@safe-global/safe-gateway-typescript-sdk': 3.23.1 - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - bufferutil - typescript @@ -10549,35 +10592,35 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} - '@solana-program/compute-budget@0.11.0(@solana/kit@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))': + '@solana-program/compute-budget@0.11.0(@solana/kit@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))': dependencies: - '@solana/kit': 5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana/kit': 5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@solana-program/memo@0.11.0(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))': + '@solana-program/memo@0.11.0(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))': dependencies: - '@solana/kit': 6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana/kit': 6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@solana-program/system@0.10.0(@solana/kit@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))': + '@solana-program/system@0.10.0(@solana/kit@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))': dependencies: - '@solana/kit': 5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana/kit': 5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@solana-program/system@0.10.0(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))': + '@solana-program/system@0.10.0(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))': dependencies: - '@solana/kit': 6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana/kit': 6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) optional: true - '@solana-program/token-2022@0.6.1(@solana/kit@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(@solana/sysvars@6.9.0(typescript@5.9.3))': + '@solana-program/token-2022@0.6.1(@solana/kit@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@solana/sysvars@6.9.0(typescript@5.9.3))': dependencies: - '@solana/kit': 5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana/kit': 5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@solana/sysvars': 6.9.0(typescript@5.9.3) - '@solana-program/token@0.9.0(@solana/kit@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))': + '@solana-program/token@0.9.0(@solana/kit@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))': dependencies: - '@solana/kit': 5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana/kit': 5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@solana-program/token@0.9.0(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))': + '@solana-program/token@0.9.0(@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))': dependencies: - '@solana/kit': 6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana/kit': 6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) optional: true '@solana/accounts@5.5.1(typescript@5.9.3)': @@ -10968,7 +11011,7 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/kit@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': + '@solana/kit@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': dependencies: '@solana/accounts': 5.5.1(typescript@5.9.3) '@solana/addresses': 5.5.1(typescript@5.9.3) @@ -10985,11 +11028,11 @@ snapshots: '@solana/rpc-api': 5.5.1(typescript@5.9.3) '@solana/rpc-parsed-types': 5.5.1(typescript@5.9.3) '@solana/rpc-spec-types': 5.5.1(typescript@5.9.3) - '@solana/rpc-subscriptions': 5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana/rpc-subscriptions': 5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@solana/rpc-types': 5.5.1(typescript@5.9.3) '@solana/signers': 5.5.1(typescript@5.9.3) '@solana/sysvars': 5.5.1(typescript@5.9.3) - '@solana/transaction-confirmation': 5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana/transaction-confirmation': 5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@solana/transaction-messages': 5.5.1(typescript@5.9.3) '@solana/transactions': 5.5.1(typescript@5.9.3) optionalDependencies: @@ -10999,7 +11042,7 @@ snapshots: - fastestsmallesttextencoderdecoder - utf-8-validate - '@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': + '@solana/kit@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': dependencies: '@solana/accounts': 6.8.0(typescript@5.9.3) '@solana/addresses': 6.8.0(typescript@5.9.3) @@ -11018,12 +11061,12 @@ snapshots: '@solana/rpc-api': 6.8.0(typescript@5.9.3) '@solana/rpc-parsed-types': 6.8.0(typescript@5.9.3) '@solana/rpc-spec-types': 6.8.0(typescript@5.9.3) - '@solana/rpc-subscriptions': 6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana/rpc-subscriptions': 6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@solana/rpc-types': 6.8.0(typescript@5.9.3) '@solana/signers': 6.8.0(typescript@5.9.3) '@solana/subscribable': 6.8.0(typescript@5.9.3) '@solana/sysvars': 6.8.0(typescript@5.9.3) - '@solana/transaction-confirmation': 6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana/transaction-confirmation': 6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@solana/transaction-messages': 6.8.0(typescript@5.9.3) '@solana/transactions': 6.8.0(typescript@5.9.3) optionalDependencies: @@ -11412,26 +11455,26 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc-subscriptions-channel-websocket@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': + '@solana/rpc-subscriptions-channel-websocket@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': dependencies: '@solana/errors': 5.5.1(typescript@5.9.3) '@solana/functional': 5.5.1(typescript@5.9.3) '@solana/rpc-subscriptions-spec': 5.5.1(typescript@5.9.3) '@solana/subscribable': 5.5.1(typescript@5.9.3) - ws: 8.20.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 8.20.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - bufferutil - utf-8-validate - '@solana/rpc-subscriptions-channel-websocket@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': + '@solana/rpc-subscriptions-channel-websocket@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': dependencies: '@solana/errors': 6.8.0(typescript@5.9.3) '@solana/functional': 6.8.0(typescript@5.9.3) '@solana/rpc-subscriptions-spec': 6.8.0(typescript@5.9.3) '@solana/subscribable': 6.8.0(typescript@5.9.3) - ws: 8.20.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 8.20.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -11478,7 +11521,7 @@ snapshots: optionalDependencies: typescript: 5.9.3 - '@solana/rpc-subscriptions@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': + '@solana/rpc-subscriptions@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': dependencies: '@solana/errors': 5.5.1(typescript@5.9.3) '@solana/fast-stable-stringify': 5.5.1(typescript@5.9.3) @@ -11486,7 +11529,7 @@ snapshots: '@solana/promises': 5.5.1(typescript@5.9.3) '@solana/rpc-spec-types': 5.5.1(typescript@5.9.3) '@solana/rpc-subscriptions-api': 5.5.1(typescript@5.9.3) - '@solana/rpc-subscriptions-channel-websocket': 5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana/rpc-subscriptions-channel-websocket': 5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@solana/rpc-subscriptions-spec': 5.5.1(typescript@5.9.3) '@solana/rpc-transformers': 5.5.1(typescript@5.9.3) '@solana/rpc-types': 5.5.1(typescript@5.9.3) @@ -11498,7 +11541,7 @@ snapshots: - fastestsmallesttextencoderdecoder - utf-8-validate - '@solana/rpc-subscriptions@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': + '@solana/rpc-subscriptions@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': dependencies: '@solana/errors': 6.8.0(typescript@5.9.3) '@solana/fast-stable-stringify': 6.8.0(typescript@5.9.3) @@ -11506,7 +11549,7 @@ snapshots: '@solana/promises': 6.8.0(typescript@5.9.3) '@solana/rpc-spec-types': 6.8.0(typescript@5.9.3) '@solana/rpc-subscriptions-api': 6.8.0(typescript@5.9.3) - '@solana/rpc-subscriptions-channel-websocket': 6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana/rpc-subscriptions-channel-websocket': 6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@solana/rpc-subscriptions-spec': 6.8.0(typescript@5.9.3) '@solana/rpc-transformers': 6.8.0(typescript@5.9.3) '@solana/rpc-types': 6.8.0(typescript@5.9.3) @@ -11792,7 +11835,7 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/transaction-confirmation@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': + '@solana/transaction-confirmation@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': dependencies: '@solana/addresses': 5.5.1(typescript@5.9.3) '@solana/codecs-strings': 5.5.1(typescript@5.9.3) @@ -11800,7 +11843,7 @@ snapshots: '@solana/keys': 5.5.1(typescript@5.9.3) '@solana/promises': 5.5.1(typescript@5.9.3) '@solana/rpc': 5.5.1(typescript@5.9.3) - '@solana/rpc-subscriptions': 5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana/rpc-subscriptions': 5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@solana/rpc-types': 5.5.1(typescript@5.9.3) '@solana/transaction-messages': 5.5.1(typescript@5.9.3) '@solana/transactions': 5.5.1(typescript@5.9.3) @@ -11811,7 +11854,7 @@ snapshots: - fastestsmallesttextencoderdecoder - utf-8-validate - '@solana/transaction-confirmation@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': + '@solana/transaction-confirmation@6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': dependencies: '@solana/addresses': 6.8.0(typescript@5.9.3) '@solana/codecs-strings': 6.8.0(typescript@5.9.3) @@ -11819,7 +11862,7 @@ snapshots: '@solana/keys': 6.8.0(typescript@5.9.3) '@solana/promises': 6.8.0(typescript@5.9.3) '@solana/rpc': 6.8.0(typescript@5.9.3) - '@solana/rpc-subscriptions': 6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana/rpc-subscriptions': 6.8.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@solana/rpc-types': 6.8.0(typescript@5.9.3) '@solana/transaction-messages': 6.8.0(typescript@5.9.3) '@solana/transactions': 6.8.0(typescript@5.9.3) @@ -12005,6 +12048,8 @@ snapshots: - typescript - utf-8-validate + '@stablelib/base64@1.0.1': {} + '@supabase/auth-js@2.104.1': dependencies: tslib: 2.8.1 @@ -12019,6 +12064,16 @@ snapshots: dependencies: tslib: 2.8.1 + '@supabase/realtime-js@2.104.1(bufferutil@4.1.0)(utf-8-validate@5.0.10)': + dependencies: + '@supabase/phoenix': 0.4.0 + '@types/ws': 8.18.1 + tslib: 2.8.1 + ws: 8.20.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@supabase/realtime-js@2.104.1(bufferutil@4.1.0)(utf-8-validate@6.0.6)': dependencies: '@supabase/phoenix': 0.4.0 @@ -12029,9 +12084,9 @@ snapshots: - bufferutil - utf-8-validate - '@supabase/ssr@0.10.2(@supabase/supabase-js@2.104.1(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + '@supabase/ssr@0.10.2(@supabase/supabase-js@2.104.1(bufferutil@4.1.0)(utf-8-validate@5.0.10))': dependencies: - '@supabase/supabase-js': 2.104.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) + '@supabase/supabase-js': 2.104.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) cookie: 1.1.1 '@supabase/storage-js@2.104.1': @@ -12039,6 +12094,17 @@ snapshots: iceberg-js: 0.8.1 tslib: 2.8.1 + '@supabase/supabase-js@2.104.1(bufferutil@4.1.0)(utf-8-validate@5.0.10)': + dependencies: + '@supabase/auth-js': 2.104.1 + '@supabase/functions-js': 2.104.1 + '@supabase/postgrest-js': 2.104.1 + '@supabase/realtime-js': 2.104.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) + '@supabase/storage-js': 2.104.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@supabase/supabase-js@2.104.1(bufferutil@4.1.0)(utf-8-validate@6.0.6)': dependencies: '@supabase/auth-js': 2.104.1 @@ -12415,19 +12481,19 @@ snapshots: loupe: 3.2.1 tinyrainbow: 1.2.0 - '@wagmi/connectors@6.2.0(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@upstash/redis@1.38.0)(@wagmi/core@2.22.1(@tanstack/query-core@5.100.6)(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)))(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@6.0.6)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.100.6)(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76)': + '@wagmi/connectors@6.2.0(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@upstash/redis@1.38.0)(@wagmi/core@2.22.1(@tanstack/query-core@5.100.6)(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.100.6)(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76)': dependencies: - '@base-org/account': 2.4.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@6.0.6)(zod@3.25.76) - '@coinbase/wallet-sdk': 4.3.6(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@6.0.6)(zod@3.25.76) - '@gemini-wallet/core': 0.3.2(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) - '@metamask/sdk': 0.33.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) - '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.100.6)(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) - '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@base-org/account': 2.4.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(zod@3.25.76) + '@coinbase/wallet-sdk': 4.3.6(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(zod@3.25.76) + '@gemini-wallet/core': 0.3.2(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@metamask/sdk': 0.33.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) + '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.100.6)(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' - porto: 0.2.35(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@wagmi/core@2.22.1(@tanstack/query-core@5.100.6)(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)))(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.100.6)(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(zod@3.25.76)) - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + porto: 0.2.35(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@wagmi/core@2.22.1(@tanstack/query-core@5.100.6)(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.100.6)(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -12468,11 +12534,11 @@ snapshots: - wagmi - zod - '@wagmi/core@2.22.1(@tanstack/query-core@5.100.6)(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))': + '@wagmi/core@2.22.1(@tanstack/query-core@5.100.6)(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: eventemitter3: 5.0.1 mipd: 0.0.7(typescript@5.9.3) - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) zustand: 5.0.0(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.4.0(react@19.2.4)) optionalDependencies: '@tanstack/query-core': 5.100.6 @@ -12497,13 +12563,13 @@ snapshots: dependencies: '@wallet-standard/base': 1.1.0 - '@walletconnect/core@2.21.0(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@walletconnect/core@2.21.0(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.1.0)(utf-8-validate@6.0.6) + '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10) '@walletconnect/keyvaluestorage': 1.1.1(@upstash/redis@1.38.0) '@walletconnect/logger': 2.1.2 '@walletconnect/relay-api': 1.0.11 @@ -12511,7 +12577,7 @@ snapshots: '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.0(@upstash/redis@1.38.0) - '@walletconnect/utils': 2.21.0(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@walletconnect/utils': 2.21.0(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/window-getters': 1.0.1 es-toolkit: 1.33.0 events: 3.3.0 @@ -12541,13 +12607,13 @@ snapshots: - utf-8-validate - zod - '@walletconnect/core@2.21.1(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@walletconnect/core@2.21.1(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.1.0)(utf-8-validate@6.0.6) + '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10) '@walletconnect/keyvaluestorage': 1.1.1(@upstash/redis@1.38.0) '@walletconnect/logger': 2.1.2 '@walletconnect/relay-api': 1.0.11 @@ -12555,7 +12621,7 @@ snapshots: '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.1(@upstash/redis@1.38.0) - '@walletconnect/utils': 2.21.1(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@walletconnect/utils': 2.21.1(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/window-getters': 1.0.1 es-toolkit: 1.33.0 events: 3.3.0 @@ -12585,13 +12651,13 @@ snapshots: - utf-8-validate - zod - '@walletconnect/core@2.21.9(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@walletconnect/core@2.21.9(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.1.0)(utf-8-validate@6.0.6) + '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10) '@walletconnect/keyvaluestorage': 1.1.1(@upstash/redis@1.38.0) '@walletconnect/logger': 2.1.2 '@walletconnect/relay-api': 1.0.11 @@ -12599,7 +12665,7 @@ snapshots: '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.9(@upstash/redis@1.38.0) - '@walletconnect/utils': 2.21.9(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@walletconnect/utils': 2.21.9(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/window-getters': 1.0.1 es-toolkit: 1.39.3 events: 3.3.0 @@ -12629,13 +12695,13 @@ snapshots: - utf-8-validate - zod - '@walletconnect/core@2.22.4(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@walletconnect/core@2.22.4(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.1.0)(utf-8-validate@6.0.6) + '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10) '@walletconnect/keyvaluestorage': 1.1.1(@upstash/redis@1.38.0) '@walletconnect/logger': 3.0.0 '@walletconnect/relay-api': 1.0.11 @@ -12677,18 +12743,18 @@ snapshots: dependencies: tslib: 1.14.1 - '@walletconnect/ethereum-provider@2.21.1(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@walletconnect/ethereum-provider@2.21.1(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit': 1.7.8(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/jsonrpc-http-connection': 1.0.8 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/keyvaluestorage': 1.1.1(@upstash/redis@1.38.0) - '@walletconnect/sign-client': 2.21.1(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@walletconnect/sign-client': 2.21.1(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/types': 2.21.1(@upstash/redis@1.38.0) - '@walletconnect/universal-provider': 2.21.1(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - '@walletconnect/utils': 2.21.1(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@walletconnect/universal-provider': 2.21.1(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/utils': 2.21.1(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -12718,18 +12784,18 @@ snapshots: - utf-8-validate - zod - '@walletconnect/ethereum-provider@2.22.4(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@walletconnect/ethereum-provider@2.22.4(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@reown/appkit': 1.8.9(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/jsonrpc-http-connection': 1.0.8 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/keyvaluestorage': 1.1.1(@upstash/redis@1.38.0) '@walletconnect/logger': 3.0.0 - '@walletconnect/sign-client': 2.22.4(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@walletconnect/sign-client': 2.22.4(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/types': 2.22.4(@upstash/redis@1.38.0) - '@walletconnect/universal-provider': 2.22.4(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@walletconnect/universal-provider': 2.22.4(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/utils': 2.22.4(@upstash/redis@1.38.0)(typescript@5.9.3)(zod@3.25.76) events: 3.3.0 transitivePeerDependencies: @@ -12797,12 +12863,12 @@ snapshots: '@walletconnect/jsonrpc-types': 1.0.4 tslib: 1.14.1 - '@walletconnect/jsonrpc-ws-connection@1.0.16(bufferutil@4.1.0)(utf-8-validate@6.0.6)': + '@walletconnect/jsonrpc-ws-connection@1.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10)': dependencies: '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/safe-json': 1.0.2 events: 3.3.0 - ws: 7.5.10(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 7.5.10(bufferutil@4.1.0)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -12858,16 +12924,16 @@ snapshots: dependencies: tslib: 1.14.1 - '@walletconnect/sign-client@2.21.0(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@walletconnect/sign-client@2.21.0(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@walletconnect/core': 2.21.0(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@walletconnect/core': 2.21.0(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/logger': 2.1.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.0(@upstash/redis@1.38.0) - '@walletconnect/utils': 2.21.0(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@walletconnect/utils': 2.21.0(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -12894,16 +12960,16 @@ snapshots: - utf-8-validate - zod - '@walletconnect/sign-client@2.21.1(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@walletconnect/sign-client@2.21.1(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@walletconnect/core': 2.21.1(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@walletconnect/core': 2.21.1(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/logger': 2.1.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.1(@upstash/redis@1.38.0) - '@walletconnect/utils': 2.21.1(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@walletconnect/utils': 2.21.1(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -12930,16 +12996,16 @@ snapshots: - utf-8-validate - zod - '@walletconnect/sign-client@2.21.9(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@walletconnect/sign-client@2.21.9(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@walletconnect/core': 2.21.9(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@walletconnect/core': 2.21.9(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/logger': 2.1.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.9(@upstash/redis@1.38.0) - '@walletconnect/utils': 2.21.9(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@walletconnect/utils': 2.21.9(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -12966,9 +13032,9 @@ snapshots: - utf-8-validate - zod - '@walletconnect/sign-client@2.22.4(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@walletconnect/sign-client@2.22.4(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@walletconnect/core': 2.22.4(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@walletconnect/core': 2.22.4(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-utils': 1.0.8 @@ -13122,7 +13188,7 @@ snapshots: - ioredis - uploadthing - '@walletconnect/universal-provider@2.21.0(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@walletconnect/universal-provider@2.21.0(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/jsonrpc-http-connection': 1.0.8 @@ -13131,9 +13197,9 @@ snapshots: '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/keyvaluestorage': 1.1.1(@upstash/redis@1.38.0) '@walletconnect/logger': 2.1.2 - '@walletconnect/sign-client': 2.21.0(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@walletconnect/sign-client': 2.21.0(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/types': 2.21.0(@upstash/redis@1.38.0) - '@walletconnect/utils': 2.21.0(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@walletconnect/utils': 2.21.0(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) es-toolkit: 1.33.0 events: 3.3.0 transitivePeerDependencies: @@ -13162,7 +13228,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/universal-provider@2.21.1(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@walletconnect/universal-provider@2.21.1(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/jsonrpc-http-connection': 1.0.8 @@ -13171,9 +13237,9 @@ snapshots: '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/keyvaluestorage': 1.1.1(@upstash/redis@1.38.0) '@walletconnect/logger': 2.1.2 - '@walletconnect/sign-client': 2.21.1(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@walletconnect/sign-client': 2.21.1(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/types': 2.21.1(@upstash/redis@1.38.0) - '@walletconnect/utils': 2.21.1(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@walletconnect/utils': 2.21.1(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) es-toolkit: 1.33.0 events: 3.3.0 transitivePeerDependencies: @@ -13202,7 +13268,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/universal-provider@2.21.9(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@walletconnect/universal-provider@2.21.9(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/jsonrpc-http-connection': 1.0.8 @@ -13211,9 +13277,9 @@ snapshots: '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/keyvaluestorage': 1.1.1(@upstash/redis@1.38.0) '@walletconnect/logger': 2.1.2 - '@walletconnect/sign-client': 2.21.9(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@walletconnect/sign-client': 2.21.9(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/types': 2.21.9(@upstash/redis@1.38.0) - '@walletconnect/utils': 2.21.9(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@walletconnect/utils': 2.21.9(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) es-toolkit: 1.39.3 events: 3.3.0 transitivePeerDependencies: @@ -13242,7 +13308,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/universal-provider@2.22.4(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@walletconnect/universal-provider@2.22.4(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/jsonrpc-http-connection': 1.0.8 @@ -13251,7 +13317,7 @@ snapshots: '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/keyvaluestorage': 1.1.1(@upstash/redis@1.38.0) '@walletconnect/logger': 3.0.0 - '@walletconnect/sign-client': 2.22.4(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@walletconnect/sign-client': 2.22.4(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/types': 2.22.4(@upstash/redis@1.38.0) '@walletconnect/utils': 2.22.4(@upstash/redis@1.38.0)(typescript@5.9.3)(zod@3.25.76) es-toolkit: 1.39.3 @@ -13282,7 +13348,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/utils@2.21.0(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@walletconnect/utils@2.21.0(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@noble/ciphers': 1.2.1 '@noble/curves': 1.8.1 @@ -13300,7 +13366,7 @@ snapshots: detect-browser: 5.3.0 query-string: 7.1.3 uint8arrays: 3.1.0 - viem: 2.23.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + viem: 2.23.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -13326,7 +13392,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/utils@2.21.1(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@walletconnect/utils@2.21.1(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@noble/ciphers': 1.2.1 '@noble/curves': 1.8.1 @@ -13344,7 +13410,7 @@ snapshots: detect-browser: 5.3.0 query-string: 7.1.3 uint8arrays: 3.1.0 - viem: 2.23.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + viem: 2.23.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -13370,7 +13436,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/utils@2.21.9(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)': + '@walletconnect/utils@2.21.9(@upstash/redis@1.38.0)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@msgpack/msgpack': 3.1.2 '@noble/ciphers': 1.3.0 @@ -13390,7 +13456,7 @@ snapshots: bs58: 6.0.0 detect-browser: 5.3.0 uint8arrays: 3.1.1 - viem: 2.36.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + viem: 2.36.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -14045,12 +14111,12 @@ snapshots: dependencies: once: 1.4.0 - engine.io-client@6.6.4(bufferutil@4.1.0)(utf-8-validate@6.0.6): + engine.io-client@6.6.4(bufferutil@4.1.0)(utf-8-validate@5.0.10): dependencies: '@socket.io/component-emitter': 3.1.2 debug: 4.4.3 engine.io-parser: 5.2.3 - ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) xmlhttprequest-ssl: 2.1.2 transitivePeerDependencies: - bufferutil @@ -14656,6 +14722,8 @@ snapshots: fast-safe-stringify@2.1.1: {} + fast-sha256@1.3.0: {} + fast-stable-stringify@1.0.0: {} fast-uri@3.1.2: {} @@ -15085,9 +15153,9 @@ snapshots: dependencies: ws: 7.5.10(bufferutil@4.1.0)(utf-8-validate@6.0.6) - isows@1.0.6(ws@8.18.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)): + isows@1.0.6(ws@8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)): dependencies: - ws: 8.18.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) isows@1.0.7(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)): dependencies: @@ -15096,6 +15164,7 @@ snapshots: isows@1.0.7(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)): dependencies: ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) + optional: true iterator.prototype@1.1.5: dependencies: @@ -15783,21 +15852,21 @@ snapshots: pony-cause@2.1.11: {} - porto@0.2.35(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@wagmi/core@2.22.1(@tanstack/query-core@5.100.6)(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)))(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.100.6)(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(zod@3.25.76)): + porto@0.2.35(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@wagmi/core@2.22.1(@tanstack/query-core@5.100.6)(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.100.6)(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)): dependencies: - '@wagmi/core': 2.22.1(@tanstack/query-core@5.100.6)(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.100.6)(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) hono: 4.12.15 idb-keyval: 6.2.2 mipd: 0.0.7(typescript@5.9.3) ox: 0.9.17(typescript@5.9.3)(zod@4.3.6) - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) zod: 4.3.6 zustand: 5.0.12(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.4.0(react@19.2.4)) optionalDependencies: '@tanstack/react-query': 5.100.6(react@19.2.4) react: 19.2.4 typescript: 5.9.3 - wagmi: 2.19.5(@tanstack/query-core@5.100.6)(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(zod@3.25.76) + wagmi: 2.19.5(@tanstack/query-core@5.100.6)(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) transitivePeerDependencies: - '@types/react' - immer @@ -16352,11 +16421,11 @@ snapshots: dot-case: 3.0.4 tslib: 2.8.1 - socket.io-client@4.8.3(bufferutil@4.1.0)(utf-8-validate@6.0.6): + socket.io-client@4.8.3(bufferutil@4.1.0)(utf-8-validate@5.0.10): dependencies: '@socket.io/component-emitter': 3.1.2 debug: 4.4.3 - engine.io-client: 6.6.4(bufferutil@4.1.0)(utf-8-validate@6.0.6) + engine.io-client: 6.6.4(bufferutil@4.1.0)(utf-8-validate@5.0.10) socket.io-parser: 4.2.6 transitivePeerDependencies: - bufferutil @@ -16399,6 +16468,11 @@ snapshots: stackback@0.0.2: {} + standardwebhooks@1.0.0: + dependencies: + '@stablelib/base64': 1.0.1 + fast-sha256: 1.3.0 + statuses@2.0.2: {} std-env@3.10.0: {} @@ -16522,6 +16596,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svix@1.93.0: + dependencies: + standardwebhooks: 1.0.0 + tabbable@6.4.0: {} tailwind-merge@3.6.0: {} @@ -16804,16 +16882,16 @@ snapshots: vary@1.1.2: {} - viem@2.23.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76): + viem@2.23.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76): dependencies: '@noble/curves': 1.8.1 '@noble/hashes': 1.7.1 '@scure/bip32': 1.6.2 '@scure/bip39': 1.5.4 abitype: 1.0.8(typescript@5.9.3)(zod@3.25.76) - isows: 1.0.6(ws@8.18.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + isows: 1.0.6(ws@8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) ox: 0.6.7(typescript@5.9.3)(zod@3.25.76) - ws: 8.18.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -16821,16 +16899,16 @@ snapshots: - utf-8-validate - zod - viem@2.36.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76): + viem@2.36.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76): dependencies: '@noble/curves': 1.9.6 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 abitype: 1.0.8(typescript@5.9.3)(zod@3.25.76) - isows: 1.0.7(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + isows: 1.0.7(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) ox: 0.9.1(typescript@5.9.3)(zod@3.25.76) - ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -16838,15 +16916,15 @@ snapshots: - utf-8-validate - zod - viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6): + viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.22.4): dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.2.3(typescript@5.9.3)(zod@4.3.6) + abitype: 1.2.3(typescript@5.9.3)(zod@3.22.4) isows: 1.0.7(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) - ox: 0.14.13(typescript@5.9.3)(zod@4.3.6) + ox: 0.14.13(typescript@5.9.3)(zod@3.22.4) ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) optionalDependencies: typescript: 5.9.3 @@ -16855,16 +16933,33 @@ snapshots: - utf-8-validate - zod - viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.22.4): + viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76): dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.2.3(typescript@5.9.3)(zod@3.22.4) - isows: 1.0.7(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)) - ox: 0.14.13(typescript@5.9.3)(zod@3.22.4) - ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) + abitype: 1.2.3(typescript@5.9.3)(zod@3.25.76) + isows: 1.0.7(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + ox: 0.14.13(typescript@5.9.3)(zod@3.25.76) + ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + + viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6): + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.2.3(typescript@5.9.3)(zod@4.3.6) + isows: 1.0.7(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + ox: 0.14.13(typescript@5.9.3)(zod@4.3.6) + ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -16888,6 +16983,7 @@ snapshots: - bufferutil - utf-8-validate - zod + optional: true vite-node@2.1.9(@types/node@20.19.39)(lightningcss@1.32.0): dependencies: @@ -16981,7 +17077,7 @@ snapshots: - supports-color - terser - vitest@2.1.9(@types/node@20.19.39)(happy-dom@20.9.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(lightningcss@1.32.0): + vitest@2.1.9(@types/node@20.19.39)(happy-dom@20.9.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(lightningcss@1.32.0): dependencies: '@vitest/expect': 2.1.9 '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@20.19.39)(lightningcss@1.32.0)) @@ -17005,7 +17101,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.19.39 - happy-dom: 20.9.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + happy-dom: 20.9.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) transitivePeerDependencies: - less - lightningcss @@ -17089,14 +17185,14 @@ snapshots: - supports-color - terser - wagmi@2.19.5(@tanstack/query-core@5.100.6)(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(zod@3.25.76): + wagmi@2.19.5(@tanstack/query-core@5.100.6)(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76): dependencies: '@tanstack/react-query': 5.100.6(react@19.2.4) - '@wagmi/connectors': 6.2.0(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@upstash/redis@1.38.0)(@wagmi/core@2.22.1(@tanstack/query-core@5.100.6)(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)))(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@6.0.6)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.100.6)(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.100.6)(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + '@wagmi/connectors': 6.2.0(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@upstash/redis@1.38.0)(@wagmi/core@2.22.1(@tanstack/query-core@5.100.6)(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.100.6)(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.100.6)(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) react: 19.2.4 use-sync-external-store: 1.4.0(react@19.2.4) - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -17219,10 +17315,10 @@ snapshots: bufferutil: 4.1.0 utf-8-validate: 6.0.6 - ws@8.18.0(bufferutil@4.1.0)(utf-8-validate@6.0.6): + ws@8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10): optionalDependencies: bufferutil: 4.1.0 - utf-8-validate: 6.0.6 + utf-8-validate: 5.0.10 ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10): optionalDependencies: @@ -17233,32 +17329,32 @@ snapshots: optionalDependencies: bufferutil: 4.1.0 utf-8-validate: 6.0.6 + optional: true ws@8.20.0(bufferutil@4.1.0)(utf-8-validate@5.0.10): optionalDependencies: bufferutil: 4.1.0 utf-8-validate: 5.0.10 - optional: true ws@8.20.0(bufferutil@4.1.0)(utf-8-validate@6.0.6): optionalDependencies: bufferutil: 4.1.0 utf-8-validate: 6.0.6 - x402@0.7.3(@solana/sysvars@6.9.0(typescript@5.9.3))(@tanstack/query-core@5.100.6)(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6): + x402@0.7.3(@solana/sysvars@6.9.0(typescript@5.9.3))(@tanstack/query-core@5.100.6)(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10): dependencies: '@scure/base': 1.2.6 - '@solana-program/compute-budget': 0.11.0(@solana/kit@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)) - '@solana-program/token': 0.9.0(@solana/kit@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)) - '@solana-program/token-2022': 0.6.1(@solana/kit@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(@solana/sysvars@6.9.0(typescript@5.9.3)) - '@solana/kit': 5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) - '@solana/transaction-confirmation': 5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@solana-program/compute-budget': 0.11.0(@solana/kit@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) + '@solana-program/token': 0.9.0(@solana/kit@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) + '@solana-program/token-2022': 0.6.1(@solana/kit@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@solana/sysvars@6.9.0(typescript@5.9.3)) + '@solana/kit': 5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/transaction-confirmation': 5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@solana/wallet-standard-features': 1.3.0 '@wallet-standard/app': 1.1.0 '@wallet-standard/base': 1.1.0 '@wallet-standard/features': 1.1.0 - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) - wagmi: 2.19.5(@tanstack/query-core@5.100.6)(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@6.0.6)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))(zod@3.25.76) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + wagmi: 2.19.5(@tanstack/query-core@5.100.6)(@tanstack/react-query@5.100.6(react@19.2.4))(@types/react@19.2.14)(@upstash/redis@1.38.0)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) zod: 3.25.76 transitivePeerDependencies: - '@azure/app-configuration' From 88593c964f41fe1c0f554659c58bb2f64fff56cf Mon Sep 17 00:00:00 2001 From: gastonfoncea Date: Mon, 18 May 2026 18:53:36 -0300 Subject: [PATCH 07/21] =?UTF-8?q?docs(plans):=20flag=20db:migrate=20as=20e?= =?UTF-8?q?xplicit=20Task=2016=20step=20=E2=80=94=20GHB-187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gaston must run pnpm db:migrate against devnet before opening the PR. Without it, agent_delegations doesn't exist and Sprint B fails at runtime. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-05-18-mcp-sprint-b-onchain-tools.md | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/superpowers/plans/2026-05-18-mcp-sprint-b-onchain-tools.md b/docs/superpowers/plans/2026-05-18-mcp-sprint-b-onchain-tools.md index 6c473e1..233b518 100644 --- a/docs/superpowers/plans/2026-05-18-mcp-sprint-b-onchain-tools.md +++ b/docs/superpowers/plans/2026-05-18-mcp-sprint-b-onchain-tools.md @@ -2222,7 +2222,25 @@ git commit -m "docs(runbooks): add Sprint B smoke test runbook — GHB-187" --- -## Task 16: Final workspace check + push for PR +## Task 16: DB migrate (manual gate) + final workspace check + push for PR + +> **🚨 Critical:** the migration `0026_agent_delegations.sql` was committed in Task 1 but **not applied**. Before opening the PR, Gaston must run `pnpm db:migrate` against devnet Supabase. Without this, none of Sprint B's code works at runtime — `submissions.create` and the delegation guard will both fail with "relation agent_delegations does not exist". This step is intentional human-gated per `CLAUDE.md`. + +- [ ] **Step 0: Gaston applies the migration to devnet** + +```bash +# Gaston runs locally with DATABASE_URL pointing at devnet Supabase +pnpm db:migrate +``` + +Verify in Supabase Studio: +- Table `agent_delegations` exists with the expected columns +- RLS is enabled +- Policy `agent_delegations_own_read` is present + +If the migration fails, do NOT proceed to deploy. Fix the migration on the feature branch and re-apply. + + - [ ] **Step 1: Run all checks** From 3bfeb04fde2b6d33674badfdf805f69b26181cae Mon Sep 17 00:00:00 2001 From: gastonfoncea Date: Mon, 18 May 2026 18:55:01 -0300 Subject: [PATCH 08/21] =?UTF-8?q?feat(mcp):=20add=20requireRole=20guard=20?= =?UTF-8?q?=E2=80=94=20GHB-187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- apps/mcp/lib/tools/role-guard.ts | 19 +++++++++++++++++ apps/mcp/tests/tools/role-guard.test.ts | 28 +++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 apps/mcp/lib/tools/role-guard.ts create mode 100644 apps/mcp/tests/tools/role-guard.test.ts diff --git a/apps/mcp/lib/tools/role-guard.ts b/apps/mcp/lib/tools/role-guard.ts new file mode 100644 index 0000000..f6c9c36 --- /dev/null +++ b/apps/mcp/lib/tools/role-guard.ts @@ -0,0 +1,19 @@ +import type { MCPProfile } from "./types"; + +export type GuardResult = + | { ok: true } + | { ok: false; error: { code: "Forbidden"; message: string } }; + +export function requireRole( + profile: MCPProfile, + expected: "dev" | "company" +): GuardResult { + if (profile.role === expected) return { ok: true }; + return { + ok: false, + error: { + code: "Forbidden", + message: `This tool requires \`${expected}\` role.`, + }, + }; +} diff --git a/apps/mcp/tests/tools/role-guard.test.ts b/apps/mcp/tests/tools/role-guard.test.ts new file mode 100644 index 0000000..58b07a0 --- /dev/null +++ b/apps/mcp/tests/tools/role-guard.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from "vitest"; +import { requireRole } from "@/lib/tools/role-guard"; +import type { MCPProfile } from "@/lib/tools/types"; + +const baseProfile: MCPProfile = { + user_id: "did:privy:abc", + role: "dev", + mcp_status: "active", + wallet_pubkey: "Wallet111", + github_handle: "alice", +}; + +describe("requireRole", () => { + it("returns ok when role matches", () => { + expect(requireRole(baseProfile, "dev")).toEqual({ ok: true }); + }); + + it("returns Forbidden when role mismatches", () => { + const result = requireRole({ ...baseProfile, role: "company" }, "dev"); + expect(result).toEqual({ + ok: false, + error: { + code: "Forbidden", + message: "This tool requires `dev` role.", + }, + }); + }); +}); From 332f72d307fdda5b518eaf3cdfe6325fa0e37e52 Mon Sep 17 00:00:00 2001 From: gastonfoncea Date: Mon, 18 May 2026 19:09:44 -0300 Subject: [PATCH 09/21] =?UTF-8?q?feat(mcp):=20add=20requireWalletDelegated?= =?UTF-8?q?=20guard=20=E2=80=94=20GHB-187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- apps/mcp/lib/tools/delegation-guard.ts | 33 +++++++++++++++++ apps/mcp/tests/tools/delegation-guard.test.ts | 37 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 apps/mcp/lib/tools/delegation-guard.ts create mode 100644 apps/mcp/tests/tools/delegation-guard.test.ts diff --git a/apps/mcp/lib/tools/delegation-guard.ts b/apps/mcp/lib/tools/delegation-guard.ts new file mode 100644 index 0000000..6e18897 --- /dev/null +++ b/apps/mcp/lib/tools/delegation-guard.ts @@ -0,0 +1,33 @@ +import type { SupabaseClient } from "@supabase/supabase-js"; +import type { GuardResult } from "./role-guard"; + +export async function requireWalletDelegated( + supabase: SupabaseClient, + userId: string +): Promise { + const { data, error } = await supabase + .from("agent_delegations") + .select("revoked_at") + .eq("user_id", userId) + .maybeSingle(); + + if (error) { + return { + ok: false, + error: { code: "Forbidden", message: "Delegation check failed." }, + }; + } + + if (!data || data.revoked_at !== null) { + return { + ok: false, + error: { + code: "Forbidden", + message: + "Wallet delegation required — visit /app/credentials to authorize.", + }, + }; + } + + return { ok: true }; +} diff --git a/apps/mcp/tests/tools/delegation-guard.test.ts b/apps/mcp/tests/tools/delegation-guard.test.ts new file mode 100644 index 0000000..f7559be --- /dev/null +++ b/apps/mcp/tests/tools/delegation-guard.test.ts @@ -0,0 +1,37 @@ +import { describe, it, expect, vi } from "vitest"; +import { requireWalletDelegated } from "@/lib/tools/delegation-guard"; + +const makeSupabase = (row: { revoked_at: string | null } | null) => + ({ + from: () => ({ + select: () => ({ + eq: () => ({ + maybeSingle: () => Promise.resolve({ data: row, error: null }), + }), + }), + }), + }) as any; + +describe("requireWalletDelegated", () => { + it("returns ok when an active delegation exists", async () => { + const supabase = makeSupabase({ revoked_at: null }); + const result = await requireWalletDelegated(supabase, "did:privy:abc"); + expect(result).toEqual({ ok: true }); + }); + + it("returns Forbidden when no row exists", async () => { + const supabase = makeSupabase(null); + const result = await requireWalletDelegated(supabase, "did:privy:abc"); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.code).toBe("Forbidden"); + expect(result.error.message).toMatch(/delegation required/i); + } + }); + + it("returns Forbidden when the delegation was revoked", async () => { + const supabase = makeSupabase({ revoked_at: "2026-05-18T00:00:00Z" }); + const result = await requireWalletDelegated(supabase, "did:privy:abc"); + expect(result.ok).toBe(false); + }); +}); From d6839a0a1d5c4de1d8cfb1579e369b7c480163e3 Mon Sep 17 00:00:00 2001 From: gastonfoncea Date: Mon, 18 May 2026 19:14:14 -0300 Subject: [PATCH 10/21] =?UTF-8?q?feat(mcp):=20add=20Privy=20delegated-sign?= =?UTF-8?q?er=20wrapper=20=E2=80=94=20GHB-187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- apps/mcp/lib/privy/delegated-signer.ts | 59 ++++++++++++++++++ apps/mcp/tests/privy/delegated-signer.test.ts | 60 +++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 apps/mcp/lib/privy/delegated-signer.ts create mode 100644 apps/mcp/tests/privy/delegated-signer.test.ts diff --git a/apps/mcp/lib/privy/delegated-signer.ts b/apps/mcp/lib/privy/delegated-signer.ts new file mode 100644 index 0000000..34ad7fb --- /dev/null +++ b/apps/mcp/lib/privy/delegated-signer.ts @@ -0,0 +1,59 @@ +import type { PrivyClient } from "@privy-io/node"; + +export type SignInput = { + walletId: string; + unsignedTx: Uint8Array; +}; + +export type SignResult = + | { ok: true; signedTx: Uint8Array } + | { ok: false; reason: "delegation_revoked" | "upstream_error" }; + +/** + * Ask Privy to sign a Solana transaction on behalf of a user who has + * delegated their wallet to our server. Returns a partially-signed + * transaction (only the user's signature slot filled) — the caller + * still needs to get a fee-payer signature via the gas station. + */ +export async function signSolanaTransaction( + client: PrivyClient, + input: SignInput +): Promise { + try { + const response = await client + .wallets() + .solana() + .signTransaction(input.walletId, { + transaction: input.unsignedTx, + }); + + // Privy returns base64 in snake_case. Decode to bytes for callers. + const signedTx = Buffer.from(response.signed_transaction, "base64"); + return { ok: true, signedTx: new Uint8Array(signedTx) }; + } catch (err: any) { + if (err?.status === 403) { + return { ok: false, reason: "delegation_revoked" }; + } + return { ok: false, reason: "upstream_error" }; + } +} + +let cachedClient: PrivyClient | null = null; + +/** + * Lazily construct a singleton Privy client. Throws if env vars are missing. + * Tests inject their own client and never call this. + */ +export function getPrivyServerClient(): PrivyClient { + if (cachedClient) return cachedClient; + + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { PrivyClient } = require("@privy-io/node"); + const appId = process.env.PRIVY_APP_ID; + const appSecret = process.env.PRIVY_APP_SECRET; + if (!appId || !appSecret) { + throw new Error("PRIVY_APP_ID / PRIVY_APP_SECRET must be set"); + } + cachedClient = new PrivyClient({ appId, appSecret }); + return cachedClient!; +} diff --git a/apps/mcp/tests/privy/delegated-signer.test.ts b/apps/mcp/tests/privy/delegated-signer.test.ts new file mode 100644 index 0000000..11f009d --- /dev/null +++ b/apps/mcp/tests/privy/delegated-signer.test.ts @@ -0,0 +1,60 @@ +import { describe, it, expect, vi } from "vitest"; +import { signSolanaTransaction } from "@/lib/privy/delegated-signer"; + +function makeFakeClient(impl: (...args: any[]) => any) { + // Mirrors client.wallets().solana().signTransaction(...) + return { + wallets: () => ({ + solana: () => ({ + signTransaction: vi.fn(impl), + }), + }), + } as any; +} + +describe("signSolanaTransaction", () => { + it("returns signed bytes when Privy accepts", async () => { + // base64 of [1, 2, 3] is "AQID" + const fakeClient = makeFakeClient(async () => ({ + encoding: "base64", + signed_transaction: "AQID", + })); + + const result = await signSolanaTransaction(fakeClient, { + walletId: "wallet-xyz", + unsignedTx: new Uint8Array([0]), + }); + + expect(result).toEqual({ + ok: true, + signedTx: new Uint8Array([1, 2, 3]), + }); + }); + + it("returns delegation_revoked on 403 from Privy", async () => { + const err = Object.assign(new Error("forbidden"), { status: 403 }); + const fakeClient = makeFakeClient(async () => { + throw err; + }); + + const result = await signSolanaTransaction(fakeClient, { + walletId: "wallet-xyz", + unsignedTx: new Uint8Array([0]), + }); + + expect(result).toEqual({ ok: false, reason: "delegation_revoked" }); + }); + + it("returns upstream_error on other failures", async () => { + const fakeClient = makeFakeClient(async () => { + throw new Error("network"); + }); + + const result = await signSolanaTransaction(fakeClient, { + walletId: "wallet-xyz", + unsignedTx: new Uint8Array([0]), + }); + + expect(result).toEqual({ ok: false, reason: "upstream_error" }); + }); +}); From c1f5ffa3a787ce5c70fd79e2156e3fa3fcb5aaee Mon Sep 17 00:00:00 2001 From: gastonfoncea Date: Mon, 18 May 2026 19:20:35 -0300 Subject: [PATCH 11/21] =?UTF-8?q?feat(mcp):=20add=20server-side=20submit?= =?UTF-8?q?=5Fsolution=20tx=20builder=20=E2=80=94=20GHB-187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements buildSubmitSolutionTx helper that builds the submit_solution Anchor instruction and packs it into an unsigned v0 VersionedTransaction with gas station as fee payer, ready for Privy signing. Co-Authored-By: Claude Sonnet 4.6 --- apps/mcp/lib/idl/ghbounty_escrow.json | 621 ++++++++++++++++++ .../lib/solana/build-submit-solution-tx.ts | 141 ++++ apps/mcp/package.json | 2 + .../solana/build-submit-solution-tx.test.ts | 51 ++ pnpm-lock.yaml | 156 +++-- 5 files changed, 914 insertions(+), 57 deletions(-) create mode 100644 apps/mcp/lib/idl/ghbounty_escrow.json create mode 100644 apps/mcp/lib/solana/build-submit-solution-tx.ts create mode 100644 apps/mcp/tests/solana/build-submit-solution-tx.test.ts diff --git a/apps/mcp/lib/idl/ghbounty_escrow.json b/apps/mcp/lib/idl/ghbounty_escrow.json new file mode 100644 index 0000000..d7bab3e --- /dev/null +++ b/apps/mcp/lib/idl/ghbounty_escrow.json @@ -0,0 +1,621 @@ +{ + "address": "CPZx26QXs3HjwGobr8cVAZEtF1qGzqnNbBdt7h1EwbBg", + "metadata": { + "name": "ghbounty_escrow", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, + "instructions": [ + { + "name": "cancel_bounty", + "discriminator": [ + 79, + 65, + 107, + 143, + 128, + 165, + 135, + 46 + ], + "accounts": [ + { + "name": "creator", + "writable": true, + "signer": true + }, + { + "name": "bounty", + "writable": true + } + ], + "args": [] + }, + { + "name": "create_bounty", + "discriminator": [ + 122, + 90, + 14, + 143, + 8, + 125, + 200, + 2 + ], + "accounts": [ + { + "name": "creator", + "writable": true, + "signer": true + }, + { + "name": "bounty", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 98, + 111, + 117, + 110, + 116, + 121 + ] + }, + { + "kind": "account", + "path": "creator" + }, + { + "kind": "arg", + "path": "bounty_id" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "bounty_id", + "type": "u64" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "scorer", + "type": "pubkey" + }, + { + "name": "github_issue_url", + "type": "string" + } + ] + }, + { + "name": "resolve_bounty", + "discriminator": [ + 207, + 43, + 93, + 238, + 222, + 184, + 79, + 219 + ], + "accounts": [ + { + "name": "creator", + "signer": true + }, + { + "name": "bounty", + "writable": true + }, + { + "name": "winning_submission", + "writable": true + }, + { + "name": "winner", + "writable": true + } + ], + "args": [] + }, + { + "name": "set_score", + "discriminator": [ + 218, + 167, + 25, + 121, + 208, + 190, + 8, + 87 + ], + "accounts": [ + { + "name": "scorer", + "signer": true + }, + { + "name": "bounty" + }, + { + "name": "submission", + "writable": true + } + ], + "args": [ + { + "name": "score", + "type": "u8" + } + ] + }, + { + "name": "init_stake_deposit", + "discriminator": [ + 213, + 203, + 209, + 208, + 0, + 230, + 132, + 106 + ], + "accounts": [ + { + "name": "owner", + "writable": true, + "signer": true + }, + { + "name": "stake", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 116, + 97, + 107, + 101, + 95, + 100, + 101, + 112, + 111, + 115, + 105, + 116 + ] + }, + { + "kind": "account", + "path": "owner" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "submit_solution", + "discriminator": [ + 203, + 233, + 157, + 191, + 70, + 37, + 205, + 0 + ], + "accounts": [ + { + "name": "solver", + "writable": true, + "signer": true + }, + { + "name": "bounty", + "writable": true + }, + { + "name": "submission", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 117, + 98, + 109, + 105, + 115, + 115, + 105, + 111, + 110 + ] + }, + { + "kind": "account", + "path": "bounty" + }, + { + "kind": "account", + "path": "bounty.submission_count", + "account": "Bounty" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "pr_url", + "type": "string" + }, + { + "name": "opus_report_hash", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + ], + "accounts": [ + { + "name": "Bounty", + "discriminator": [ + 237, + 16, + 105, + 198, + 19, + 69, + 242, + 234 + ] + }, + { + "name": "StakeDeposit", + "discriminator": [ + 174, + 92, + 136, + 117, + 54, + 202, + 43, + 233 + ] + }, + { + "name": "Submission", + "discriminator": [ + 58, + 194, + 159, + 158, + 75, + 102, + 178, + 197 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "ZeroAmount", + "msg": "Bounty amount must be greater than zero" + }, + { + "code": 6001, + "name": "UrlTooLong", + "msg": "URL exceeds maximum length" + }, + { + "code": 6002, + "name": "BountyNotOpen", + "msg": "Bounty is not in the Open state" + }, + { + "code": 6003, + "name": "UnauthorizedCreator", + "msg": "Only the bounty creator can perform this action" + }, + { + "code": 6004, + "name": "SubmissionMismatch", + "msg": "Submission does not belong to this bounty" + }, + { + "code": 6005, + "name": "ScoreOutOfRange", + "msg": "Score must be between 1 and 10" + }, + { + "code": 6006, + "name": "ScoreAlreadySet", + "msg": "Score has already been set on this submission" + }, + { + "code": 6007, + "name": "UnauthorizedScorer", + "msg": "Only the designated scorer can set scores on this bounty" + }, + { + "code": 6008, + "name": "LamportOverflow", + "msg": "Lamport arithmetic overflow" + } + ], + "types": [ + { + "name": "Bounty", + "type": { + "kind": "struct", + "fields": [ + { + "name": "creator", + "type": "pubkey" + }, + { + "name": "scorer", + "type": "pubkey" + }, + { + "name": "bounty_id", + "type": "u64" + }, + { + "name": "mint", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "state", + "type": { + "defined": { + "name": "BountyState" + } + } + }, + { + "name": "submission_count", + "type": "u32" + }, + { + "name": "winner", + "type": { + "option": "pubkey" + } + }, + { + "name": "github_issue_url", + "type": "string" + }, + { + "name": "created_at", + "type": "i64" + }, + { + "name": "bump", + "type": "u8" + } + ] + } + }, + { + "name": "BountyState", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Open" + }, + { + "name": "Resolved" + }, + { + "name": "Cancelled" + } + ] + } + }, + { + "name": "StakeDeposit", + "type": { + "kind": "struct", + "fields": [ + { + "name": "owner", + "docs": [ + "Owner / depositor wallet — refund destination." + ], + "type": "pubkey" + }, + { + "name": "amount", + "docs": [ + "Lamports currently held in the PDA (decremented on partial slash)." + ], + "type": "u64" + }, + { + "name": "status", + "type": { + "defined": { + "name": "StakeStatus" + } + } + }, + { + "name": "locked_until", + "docs": [ + "Unix seconds at which `refund_stake_deposit` becomes valid." + ], + "type": "i64" + }, + { + "name": "created_at", + "docs": [ + "Unix seconds when the deposit was created." + ], + "type": "i64" + }, + { + "name": "bump", + "type": "u8" + } + ] + } + }, + { + "name": "StakeStatus", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Active" + }, + { + "name": "Frozen" + }, + { + "name": "Slashed" + }, + { + "name": "Refunded" + } + ] + } + }, + { + "name": "Submission", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bounty", + "type": "pubkey" + }, + { + "name": "solver", + "type": "pubkey" + }, + { + "name": "submission_index", + "type": "u32" + }, + { + "name": "pr_url", + "type": "string" + }, + { + "name": "opus_report_hash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "score", + "type": { + "option": "u8" + } + }, + { + "name": "state", + "type": { + "defined": { + "name": "SubmissionState" + } + } + }, + { + "name": "created_at", + "type": "i64" + }, + { + "name": "bump", + "type": "u8" + } + ] + } + }, + { + "name": "SubmissionState", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Pending" + }, + { + "name": "Scored" + }, + { + "name": "Winner" + } + ] + } + } + ], + "constants": [ + { + "name": "BOUNTY_SEED", + "type": "bytes", + "value": "[98, 111, 117, 110, 116, 121]" + }, + { + "name": "SUBMISSION_SEED", + "type": "bytes", + "value": "[115, 117, 98, 109, 105, 115, 115, 105, 111, 110]" + } + ] +} \ No newline at end of file diff --git a/apps/mcp/lib/solana/build-submit-solution-tx.ts b/apps/mcp/lib/solana/build-submit-solution-tx.ts new file mode 100644 index 0000000..d39e8af --- /dev/null +++ b/apps/mcp/lib/solana/build-submit-solution-tx.ts @@ -0,0 +1,141 @@ +/** + * Server-side helper to build a `submit_solution` Solana instruction + * and wrap it in a v0 VersionedTransaction ready for Privy signing. + * + * Mirrors `frontend/lib/solana.ts:buildSubmitSolutionIx` but: + * - Runs on the server (MCP app). + * - Accepts a pre-fetched `submissionCount` and `blockhash` so the + * caller controls RPC usage (no implicit network calls here). + * - Returns a serialized `VersionedTransaction` with two signer slots: + * [0] solver — filled by Privy + * [1] gasStation — filled by the gas station service + */ +import { + Connection, + PublicKey, + TransactionMessage, + VersionedTransaction, +} from "@solana/web3.js"; +import { AnchorProvider, Program, type Wallet } from "@coral-xyz/anchor"; +import idl from "@/lib/idl/ghbounty_escrow.json"; + +const SUBMISSION_SEED = Buffer.from("submission"); +const PROGRAM_ID = new PublicKey(idl.address); + +/** Encode a u32 as 4-byte little-endian buffer (matches on-chain u32 seed). */ +function u32LE(n: number): Buffer { + const buf = Buffer.alloc(4); + buf.writeUInt32LE(n >>> 0, 0); + return buf; +} + +/** + * Read-only wallet stub — identical to the frontend pattern. + * Only used to satisfy AnchorProvider; signing always goes through Privy. + */ +function readonlyWallet(): Wallet { + return { + publicKey: PublicKey.default, + signTransaction: async () => { + throw new Error("readonly wallet — sign through Privy"); + }, + signAllTransactions: async () => { + throw new Error("readonly wallet — sign through Privy"); + }, + } as unknown as Wallet; +} + +export type BuildSubmitInput = { + /** Solana JSON-RPC endpoint (only used to instantiate the AnchorProvider). */ + rpcUrl: string; + /** Base58 address of the bounty PDA. */ + bountyPda: string; + /** Base58 address of the solver's Solana wallet (will sign via Privy). */ + solver: string; + /** Base58 address of the gas station wallet (fee payer). */ + gasStationPubkey: string; + /** GitHub PR URL (max 200 chars, enforced on-chain). */ + prUrl: string; + /** + * Current value of `bounty.submission_count` fetched from the chain. + * Used as the seed index to derive the submission PDA. + */ + submissionCount: number; + /** Recent blockhash for the transaction. */ + blockhash: string; + /** + * 32-byte hash of the off-chain Opus report. Defaults to 32 zero bytes + * (matches the frontend: for manual submissions the relayer fills it later). + */ + opusReportHash?: Uint8Array; +}; + +export type BuildSubmitResult = { + /** Serialized v0 VersionedTransaction (unsigned). */ + unsignedTx: Uint8Array; + /** Base58 address of the submission PDA that will be initialized. */ + submissionPda: string; + /** The submission index used (equals `submissionCount` from input). */ + submissionIndex: number; +}; + +export async function buildSubmitSolutionTx( + input: BuildSubmitInput +): Promise { + if (input.prUrl.length > 200) { + throw new Error(`pr_url too long (${input.prUrl.length} chars, max 200)`); + } + + const opusReportHash = input.opusReportHash ?? new Uint8Array(32); + if (opusReportHash.length !== 32) { + throw new Error( + `opus_report_hash must be 32 bytes (got ${opusReportHash.length})` + ); + } + + const bountyPda = new PublicKey(input.bountyPda); + const solver = new PublicKey(input.solver); + const gasStation = new PublicKey(input.gasStationPubkey); + + const [submissionPda] = PublicKey.findProgramAddressSync( + [SUBMISSION_SEED, bountyPda.toBuffer(), u32LE(input.submissionCount)], + PROGRAM_ID + ); + + const connection = new Connection(input.rpcUrl, "confirmed"); + const provider = new AnchorProvider( + connection, + readonlyWallet(), + AnchorProvider.defaultOptions() + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const program = new Program(idl as any, provider); + + const ix = await program.methods + .submitSolution( + input.prUrl, + Array.from(opusReportHash) as unknown as number[] + ) + .accountsStrict({ + solver, + bounty: bountyPda, + submission: submissionPda, + systemProgram: new PublicKey("11111111111111111111111111111111"), + }) + .instruction(); + + const message = new TransactionMessage({ + payerKey: gasStation, + recentBlockhash: input.blockhash, + instructions: [ix], + }).compileToV0Message(); + + const tx = new VersionedTransaction(message); + const unsignedTx = tx.serialize(); + + return { + unsignedTx, + submissionPda: submissionPda.toBase58(), + submissionIndex: input.submissionCount, + }; +} diff --git a/apps/mcp/package.json b/apps/mcp/package.json index 24ef99d..9a146ba 100644 --- a/apps/mcp/package.json +++ b/apps/mcp/package.json @@ -11,12 +11,14 @@ "test": "vitest run" }, "dependencies": { + "@coral-xyz/anchor": "^0.30.1", "@ghbounty/db": "workspace:^", "@ghbounty/sdk": "workspace:^", "@ghbounty/shared": "workspace:^", "@modelcontextprotocol/sdk": "^1.0.0", "@privy-io/node": "^0.18.0", "@solana/kit": "^6.9.0", + "@solana/web3.js": "^1.95.0", "@supabase/supabase-js": "^2.104.1", "@upstash/ratelimit": "^2.0.8", "@upstash/redis": "^1.38.0", diff --git a/apps/mcp/tests/solana/build-submit-solution-tx.test.ts b/apps/mcp/tests/solana/build-submit-solution-tx.test.ts new file mode 100644 index 0000000..bf71793 --- /dev/null +++ b/apps/mcp/tests/solana/build-submit-solution-tx.test.ts @@ -0,0 +1,51 @@ +/** + * Tests for the server-side submit_solution tx builder. + * + * Note on synthetic pubkeys: Solana web3.js v1 `new PublicKey(string)` requires + * a valid 32-byte base58 string. The task plan's synthetic strings + * ("Bo111...") are too short to decode to exactly 32 bytes and throw + * "Invalid public key input". We therefore generate real keypairs via + * `Keypair.generate()` in the test setup. + */ +import { describe, it, expect } from "vitest"; +import { Keypair, VersionedTransaction } from "@solana/web3.js"; +import { buildSubmitSolutionTx } from "@/lib/solana/build-submit-solution-tx"; + +// Valid base58 pubkeys generated fresh for each test run. +const bountyKp = Keypair.generate(); +const solverKp = Keypair.generate(); +const gasStationKp = Keypair.generate(); + +const VALID_INPUT = { + rpcUrl: "https://example.invalid", + bountyPda: bountyKp.publicKey.toBase58(), + solver: solverKp.publicKey.toBase58(), + gasStationPubkey: gasStationKp.publicKey.toBase58(), + prUrl: "https://github.com/x/y/pull/1", + submissionCount: 0, + blockhash: "11111111111111111111111111111111", +} as const; + +describe("buildSubmitSolutionTx", () => { + it("rejects pr_url longer than 200 chars", async () => { + await expect( + buildSubmitSolutionTx({ + ...VALID_INPUT, + prUrl: "x".repeat(201), + }) + ).rejects.toThrow(/pr_url too long/); + }); + + it("packs ix into a v0 VersionedTransaction with two signature slots", async () => { + const result = await buildSubmitSolutionTx(VALID_INPUT); + + expect(result.unsignedTx).toBeInstanceOf(Uint8Array); + expect(result.submissionPda).toBeDefined(); + expect(result.submissionIndex).toBe(0); + + // Verify the wrapper compiled as v0 with two signature slots (gas station + solver) + const tx = VersionedTransaction.deserialize(result.unsignedTx); + expect(tx.version).toBe(0); + expect(tx.message.header.numRequiredSignatures).toBe(2); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 135d720..558f40c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: apps/mcp: dependencies: + '@coral-xyz/anchor': + specifier: ^0.30.1 + version: 0.30.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@ghbounty/db': specifier: workspace:^ version: link:../../packages/db @@ -28,13 +31,16 @@ importers: version: 1.29.0(zod@3.25.76) '@privy-io/node': specifier: ^0.18.0 - version: 0.18.0(@solana/kit@6.9.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + version: 0.18.0(@solana/kit@6.9.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) '@solana/kit': specifier: ^6.9.0 - version: 6.9.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + version: 6.9.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/web3.js': + specifier: ^1.95.0 + version: 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) '@supabase/supabase-js': specifier: ^2.104.1 - version: 2.104.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) + version: 2.104.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) '@upstash/ratelimit': specifier: ^2.0.8 version: 2.0.8(@upstash/redis@1.38.0) @@ -77,7 +83,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.1.9 - version: 2.1.9(@types/node@22.19.17)(happy-dom@20.9.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(lightningcss@1.32.0) + version: 2.1.9(@types/node@22.19.17)(happy-dom@20.9.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(lightningcss@1.32.0) frontend: dependencies: @@ -9021,7 +9027,7 @@ snapshots: optionalDependencies: viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@privy-io/node@0.18.0(@solana/kit@6.9.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))': + '@privy-io/node@0.18.0(@solana/kit@6.9.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: '@hpke/chacha20poly1305': 1.8.0 '@hpke/core': 1.9.0 @@ -9033,8 +9039,8 @@ snapshots: lru-cache: 11.3.5 svix: 1.93.0 optionalDependencies: - '@solana/kit': 6.9.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + '@solana/kit': 6.9.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@privy-io/popup@0.0.4': {} @@ -11076,6 +11082,40 @@ snapshots: - fastestsmallesttextencoderdecoder - utf-8-validate + '@solana/kit@6.9.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@solana/accounts': 6.9.0(typescript@5.9.3) + '@solana/addresses': 6.9.0(typescript@5.9.3) + '@solana/codecs': 6.9.0(typescript@5.9.3) + '@solana/errors': 6.9.0(typescript@5.9.3) + '@solana/functional': 6.9.0(typescript@5.9.3) + '@solana/instruction-plans': 6.9.0(typescript@5.9.3) + '@solana/instructions': 6.9.0(typescript@5.9.3) + '@solana/keys': 6.9.0(typescript@5.9.3) + '@solana/offchain-messages': 6.9.0(typescript@5.9.3) + '@solana/plugin-core': 6.9.0(typescript@5.9.3) + '@solana/plugin-interfaces': 6.9.0(typescript@5.9.3) + '@solana/program-client-core': 6.9.0(typescript@5.9.3) + '@solana/programs': 6.9.0(typescript@5.9.3) + '@solana/rpc': 6.9.0(typescript@5.9.3) + '@solana/rpc-api': 6.9.0(typescript@5.9.3) + '@solana/rpc-parsed-types': 6.9.0(typescript@5.9.3) + '@solana/rpc-spec-types': 6.9.0(typescript@5.9.3) + '@solana/rpc-subscriptions': 6.9.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/rpc-types': 6.9.0(typescript@5.9.3) + '@solana/signers': 6.9.0(typescript@5.9.3) + '@solana/subscribable': 6.9.0(typescript@5.9.3) + '@solana/sysvars': 6.9.0(typescript@5.9.3) + '@solana/transaction-confirmation': 6.9.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/transaction-messages': 6.9.0(typescript@5.9.3) + '@solana/transactions': 6.9.0(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - fastestsmallesttextencoderdecoder + - utf-8-validate + '@solana/kit@6.9.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': dependencies: '@solana/accounts': 6.9.0(typescript@5.9.3) @@ -11481,6 +11521,19 @@ snapshots: - bufferutil - utf-8-validate + '@solana/rpc-subscriptions-channel-websocket@6.9.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@solana/errors': 6.9.0(typescript@5.9.3) + '@solana/functional': 6.9.0(typescript@5.9.3) + '@solana/rpc-subscriptions-spec': 6.9.0(typescript@5.9.3) + '@solana/subscribable': 6.9.0(typescript@5.9.3) + ws: 8.20.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@solana/rpc-subscriptions-channel-websocket@6.9.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': dependencies: '@solana/errors': 6.9.0(typescript@5.9.3) @@ -11561,6 +11614,26 @@ snapshots: - fastestsmallesttextencoderdecoder - utf-8-validate + '@solana/rpc-subscriptions@6.9.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@solana/errors': 6.9.0(typescript@5.9.3) + '@solana/fast-stable-stringify': 6.9.0(typescript@5.9.3) + '@solana/functional': 6.9.0(typescript@5.9.3) + '@solana/promises': 6.9.0(typescript@5.9.3) + '@solana/rpc-spec-types': 6.9.0(typescript@5.9.3) + '@solana/rpc-subscriptions-api': 6.9.0(typescript@5.9.3) + '@solana/rpc-subscriptions-channel-websocket': 6.9.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/rpc-subscriptions-spec': 6.9.0(typescript@5.9.3) + '@solana/rpc-transformers': 6.9.0(typescript@5.9.3) + '@solana/rpc-types': 6.9.0(typescript@5.9.3) + '@solana/subscribable': 6.9.0(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - fastestsmallesttextencoderdecoder + - utf-8-validate + '@solana/rpc-subscriptions@6.9.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': dependencies: '@solana/errors': 6.9.0(typescript@5.9.3) @@ -11873,6 +11946,25 @@ snapshots: - fastestsmallesttextencoderdecoder - utf-8-validate + '@solana/transaction-confirmation@6.9.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@solana/addresses': 6.9.0(typescript@5.9.3) + '@solana/codecs-strings': 6.9.0(typescript@5.9.3) + '@solana/errors': 6.9.0(typescript@5.9.3) + '@solana/keys': 6.9.0(typescript@5.9.3) + '@solana/promises': 6.9.0(typescript@5.9.3) + '@solana/rpc': 6.9.0(typescript@5.9.3) + '@solana/rpc-subscriptions': 6.9.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/rpc-types': 6.9.0(typescript@5.9.3) + '@solana/transaction-messages': 6.9.0(typescript@5.9.3) + '@solana/transactions': 6.9.0(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - fastestsmallesttextencoderdecoder + - utf-8-validate + '@solana/transaction-confirmation@6.9.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': dependencies: '@solana/addresses': 6.9.0(typescript@5.9.3) @@ -12074,16 +12166,6 @@ snapshots: - bufferutil - utf-8-validate - '@supabase/realtime-js@2.104.1(bufferutil@4.1.0)(utf-8-validate@6.0.6)': - dependencies: - '@supabase/phoenix': 0.4.0 - '@types/ws': 8.18.1 - tslib: 2.8.1 - ws: 8.20.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) - transitivePeerDependencies: - - bufferutil - - utf-8-validate - '@supabase/ssr@0.10.2(@supabase/supabase-js@2.104.1(bufferutil@4.1.0)(utf-8-validate@5.0.10))': dependencies: '@supabase/supabase-js': 2.104.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) @@ -12105,17 +12187,6 @@ snapshots: - bufferutil - utf-8-validate - '@supabase/supabase-js@2.104.1(bufferutil@4.1.0)(utf-8-validate@6.0.6)': - dependencies: - '@supabase/auth-js': 2.104.1 - '@supabase/functions-js': 2.104.1 - '@supabase/postgrest-js': 2.104.1 - '@supabase/realtime-js': 2.104.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) - '@supabase/storage-js': 2.104.1 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -15161,11 +15232,6 @@ snapshots: dependencies: ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) - isows@1.0.7(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)): - dependencies: - ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) - optional: true - iterator.prototype@1.1.5: dependencies: define-data-property: 1.1.4 @@ -16967,24 +17033,6 @@ snapshots: - utf-8-validate - zod - viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76): - dependencies: - '@noble/curves': 1.9.1 - '@noble/hashes': 1.8.0 - '@scure/bip32': 1.7.0 - '@scure/bip39': 1.6.0 - abitype: 1.2.3(typescript@5.9.3)(zod@3.25.76) - isows: 1.0.7(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)) - ox: 0.14.13(typescript@5.9.3)(zod@3.25.76) - ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - zod - optional: true - vite-node@2.1.9(@types/node@20.19.39)(lightningcss@1.32.0): dependencies: cac: 6.7.14 @@ -17325,12 +17373,6 @@ snapshots: bufferutil: 4.1.0 utf-8-validate: 5.0.10 - ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6): - optionalDependencies: - bufferutil: 4.1.0 - utf-8-validate: 6.0.6 - optional: true - ws@8.20.0(bufferutil@4.1.0)(utf-8-validate@5.0.10): optionalDependencies: bufferutil: 4.1.0 From ff02667308204677db9a33a59bb31fc64f84950f Mon Sep 17 00:00:00 2001 From: gastonfoncea Date: Mon, 18 May 2026 19:33:01 -0300 Subject: [PATCH 12/21] =?UTF-8?q?feat(mcp):=20add=20submissions.list=20too?= =?UTF-8?q?l=20=E2=80=94=20GHB-187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- apps/mcp/lib/tools/register.ts | 2 + apps/mcp/lib/tools/submissions/list.ts | 58 ++++++++++++++ apps/mcp/tests/tools/submissions/list.test.ts | 78 +++++++++++++++++++ 3 files changed, 138 insertions(+) create mode 100644 apps/mcp/lib/tools/submissions/list.ts create mode 100644 apps/mcp/tests/tools/submissions/list.test.ts diff --git a/apps/mcp/lib/tools/register.ts b/apps/mcp/lib/tools/register.ts index 725cff6..3c56539 100644 --- a/apps/mcp/lib/tools/register.ts +++ b/apps/mcp/lib/tools/register.ts @@ -3,10 +3,12 @@ import { registerWhoami } from "./whoami"; import { registerBountiesList } from "./bounties/list"; import { registerBountiesGet } from "./bounties/get"; import { registerSubmissionsGet } from "./submissions/get"; +import { registerSubmissionsList } from "./submissions/list"; export async function registerAllTools(server: McpServer): Promise { registerWhoami(server); registerBountiesList(server); registerBountiesGet(server); registerSubmissionsGet(server); + registerSubmissionsList(server); } diff --git a/apps/mcp/lib/tools/submissions/list.ts b/apps/mcp/lib/tools/submissions/list.ts new file mode 100644 index 0000000..f309a0e --- /dev/null +++ b/apps/mcp/lib/tools/submissions/list.ts @@ -0,0 +1,58 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { authenticate } from "@/lib/auth/middleware"; +import { supabaseAdmin } from "@/lib/supabase/admin"; +import { mcpError } from "@/lib/errors"; +import { requireRole } from "@/lib/tools/role-guard"; + +const ListInput = z.object({ + authorization: z.string().optional(), + limit: z.number().int().min(1).max(50).optional(), +}); + +export async function handleSubmissionsList(raw: unknown) { + const parsed = ListInput.safeParse(raw); + if (!parsed.success) return { error: mcpError("InvalidInput", parsed.error.message) }; + + const auth = await authenticate(parsed.data.authorization); + if (!auth.ok) return { error: auth.error }; + + const roleCheck = requireRole(auth.profile, "dev"); + if (!roleCheck.ok) return { error: mcpError("Forbidden", roleCheck.error.message) }; + + if (!auth.profile.wallet_pubkey) { + return { error: mcpError("Forbidden", "Profile has no wallet pubkey.") }; + } + + const supabase = supabaseAdmin(); + const { data, error } = await supabase + .from("submissions") + .select("id, pr_url, state, rank, created_at") + .eq("solver", auth.profile.wallet_pubkey) + .order("created_at", { ascending: false }) + .limit(parsed.data.limit ?? 50); + + if (error) return { error: mcpError("InternalError", error.message) }; + + return { + items: (data ?? []).map((row: any) => ({ + id: row.id, + pr_url: row.pr_url, + state: row.state, + rank: row.rank, + created_at: row.created_at, + })), + }; +} + +export function registerSubmissionsList(server: McpServer): void { + server.tool( + "submissions.list", + { limit: z.number().int().min(1).max(50).optional() }, + async (input, extra) => { + const authorization = (extra as any)?.requestInfo?.headers?.authorization; + const result = await handleSubmissionsList({ ...input, authorization }); + return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; + } + ); +} diff --git a/apps/mcp/tests/tools/submissions/list.test.ts b/apps/mcp/tests/tools/submissions/list.test.ts new file mode 100644 index 0000000..f585f0c --- /dev/null +++ b/apps/mcp/tests/tools/submissions/list.test.ts @@ -0,0 +1,78 @@ +import { describe, it, expect, vi } from "vitest"; +import { handleSubmissionsList } from "@/lib/tools/submissions/list"; + +vi.mock("@/lib/auth/middleware", () => ({ + authenticate: vi.fn().mockResolvedValue({ + ok: true, + profile: { + user_id: "did:privy:abc", + role: "dev", + mcp_status: "active", + wallet_pubkey: "Solver111", + github_handle: "alice", + }, + credentialId: "k1", + credentialKind: "api_key", + }), +})); + +vi.mock("@/lib/supabase/admin", () => ({ + supabaseAdmin: () => ({ + from: () => ({ + select: () => ({ + eq: () => ({ + order: () => ({ + limit: () => + Promise.resolve({ + data: [ + { + id: "sub-1", + pr_url: "https://github.com/x/y/pull/1", + state: "scored", + rank: 1, + created_at: "2026-05-18T00:00:00Z", + }, + ], + error: null, + }), + }), + }), + }), + }), + }), +})); + +describe("submissions.list", () => { + it("returns the caller's submissions", async () => { + const result = await handleSubmissionsList({ authorization: "Bearer x" }); + expect(result).toMatchObject({ + items: [ + expect.objectContaining({ + id: "sub-1", + state: "scored", + }), + ], + }); + }); + + it("returns Forbidden when caller is a company", async () => { + const { authenticate } = await import("@/lib/auth/middleware"); + (authenticate as any).mockResolvedValueOnce({ + ok: true, + profile: { + user_id: "did:privy:co", + role: "company", + mcp_status: "active", + wallet_pubkey: null, + github_handle: null, + }, + credentialId: "k1", + credentialKind: "api_key", + }); + + const result = await handleSubmissionsList({ authorization: "Bearer x" }); + expect(result).toEqual({ + error: expect.objectContaining({ code: "Forbidden" }), + }); + }); +}); From 88484550b1030333dde7215e724725c193347a23 Mon Sep 17 00:00:00 2001 From: gastonfoncea Date: Mon, 18 May 2026 19:40:19 -0300 Subject: [PATCH 13/21] =?UTF-8?q?feat(mcp):=20add=20submissions.create=20(?= =?UTF-8?q?submit=5Fpr)=20happy=20path=20=E2=80=94=20GHB-187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full auth → role guard → delegation guard → idempotency → PR ownership → build tx → Privy sign → gas station submit → DB mirror insert flow. Adds SolanaGasStation singleton wrapper, registers the tool, and covers happy path + 4 guard cases with vitest. Co-Authored-By: Claude Sonnet 4.6 --- apps/mcp/lib/errors.ts | 1 + apps/mcp/lib/gas-station/server.ts | 62 ++++ apps/mcp/lib/tools/register.ts | 2 + apps/mcp/lib/tools/submissions/create.ts | 311 ++++++++++++++++ .../tests/tools/submissions/create.test.ts | 336 ++++++++++++++++++ 5 files changed, 712 insertions(+) create mode 100644 apps/mcp/lib/gas-station/server.ts create mode 100644 apps/mcp/lib/tools/submissions/create.ts create mode 100644 apps/mcp/tests/tools/submissions/create.test.ts diff --git a/apps/mcp/lib/errors.ts b/apps/mcp/lib/errors.ts index 52b4bff..dc724d1 100644 --- a/apps/mcp/lib/errors.ts +++ b/apps/mcp/lib/errors.ts @@ -14,6 +14,7 @@ export type McpErrorCode = | "NotFound" | "Conflict" | "RpcError" + | "ServiceUnavailable" | "InternalError" | "InvalidInput"; diff --git a/apps/mcp/lib/gas-station/server.ts b/apps/mcp/lib/gas-station/server.ts new file mode 100644 index 0000000..ca45088 --- /dev/null +++ b/apps/mcp/lib/gas-station/server.ts @@ -0,0 +1,62 @@ +/** + * Server-side SolanaGasStation singleton for the MCP app. + * + * Rather than going through the frontend's HTTP endpoint at + * /api/gas-station/sponsor, the MCP app uses the SolanaGasStation class + * directly — no extra network hop, no service-to-service auth token needed, + * and full control over error handling. + * + * Wired in GHB-187 (submissions.create / submit_pr). + */ + +import { Connection } from "@solana/web3.js"; +import { + SolanaGasStation, + GasStationError, + loadGasStationKeypair, + makeConnectionRpcSubmitter, +} from "@ghbounty/shared"; +import type { ChainId } from "@ghbounty/shared"; + +let cached: SolanaGasStation | null = null; + +function get(): SolanaGasStation { + if (cached) return cached; + + const rpcUrl = process.env.SOLANA_RPC_URL; + if (!rpcUrl) throw new Error("SOLANA_RPC_URL must be set"); + + const chainId = (process.env.CHAIN_ID ?? "solana-devnet") as ChainId; + + cached = new SolanaGasStation({ + chainId, + keypair: loadGasStationKeypair(), + rpc: makeConnectionRpcSubmitter(new Connection(rpcUrl, "confirmed")), + }); + return cached; +} + +export async function submitSponsoredTx( + signedTxBytes: Uint8Array +): Promise<{ ok: true; signature: string } | { ok: false; reason: string }> { + const gasStation = get(); + const chainId = (process.env.CHAIN_ID ?? "solana-devnet") as ChainId; + try { + const result = await gasStation.sponsor({ + chainId, + payload: { + kind: "solana", + partiallySignedTxB64: Buffer.from(signedTxBytes).toString("base64"), + }, + }); + return { ok: true, signature: result.txHash }; + } catch (err: unknown) { + // Return only safe, structured codes — never raw error messages that may + // contain internal details (pubkeys, discriminator codes, RPC responses). + const reason = + err instanceof GasStationError + ? err.code // "validator_rejected" | "rpc_error" | "unsupported_chain" — safe + : "unexpected_error"; + return { ok: false, reason }; + } +} diff --git a/apps/mcp/lib/tools/register.ts b/apps/mcp/lib/tools/register.ts index 3c56539..6ac4387 100644 --- a/apps/mcp/lib/tools/register.ts +++ b/apps/mcp/lib/tools/register.ts @@ -4,6 +4,7 @@ import { registerBountiesList } from "./bounties/list"; import { registerBountiesGet } from "./bounties/get"; import { registerSubmissionsGet } from "./submissions/get"; import { registerSubmissionsList } from "./submissions/list"; +import { registerSubmissionsCreate } from "./submissions/create"; export async function registerAllTools(server: McpServer): Promise { registerWhoami(server); @@ -11,4 +12,5 @@ export async function registerAllTools(server: McpServer): Promise { registerBountiesGet(server); registerSubmissionsGet(server); registerSubmissionsList(server); + registerSubmissionsCreate(server); } diff --git a/apps/mcp/lib/tools/submissions/create.ts b/apps/mcp/lib/tools/submissions/create.ts new file mode 100644 index 0000000..837c427 --- /dev/null +++ b/apps/mcp/lib/tools/submissions/create.ts @@ -0,0 +1,311 @@ +/** + * submissions.create (a.k.a. submit_pr) — GHB-187 + * + * Lets an AI agent submit a PR on-chain as a bounty solution. Full flow: + * auth → role guard (dev) → delegation guard → load bounty + * → idempotency check → verifyPrOwnership (GHB-182 pre-check) + * → fetch blockhash → build submit_solution tx (Task 7) + * → Privy signs as solver (Task 6) → SolanaGasStation signs + submits + * → insert mirror row in DB → return result + */ + +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { authenticate } from "@/lib/auth/middleware"; +import { supabaseAdmin } from "@/lib/supabase/admin"; +import { mcpError } from "@/lib/errors"; +import { getChainId } from "@/lib/config"; +import { requireRole } from "@/lib/tools/role-guard"; +import { requireWalletDelegated } from "@/lib/tools/delegation-guard"; +import { verifyPrOwnership } from "@ghbounty/shared"; +import { + getPrivyServerClient, + signSolanaTransaction, +} from "@/lib/privy/delegated-signer"; +import { buildSubmitSolutionTx } from "@/lib/solana/build-submit-solution-tx"; +import { solanaRpc } from "@/lib/solana/rpc"; +import { submitSponsoredTx } from "@/lib/gas-station/server"; + +const CreateInput = z.object({ + authorization: z.string().optional(), + bounty_id: z.string().uuid(), + pr_url: z.string().url().max(200), +}); + +/** + * Extract the repo URL (https://github.com/owner/repo) from a GitHub + * issue URL. Returns null if the URL doesn't match the expected pattern. + */ +function parseRepoUrl(githubIssueUrl: string): string | null { + const m = githubIssueUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\//); + return m ? `https://github.com/${m[1]}/${m[2]}` : null; +} + +/** 32 zero bytes encoded as a 64-char hex string — matches the `text` column type. */ +const ZERO_OPUS_HASH = "0".repeat(64); + +export async function handleSubmissionsCreate(raw: unknown) { + const parsed = CreateInput.safeParse(raw); + if (!parsed.success) { + return { error: mcpError("InvalidInput", parsed.error.message) }; + } + + // --- Auth --- + const auth = await authenticate(parsed.data.authorization); + if (!auth.ok) return { error: auth.error }; + + // --- Role guard (dev only) --- + const roleCheck = requireRole(auth.profile, "dev"); + if (!roleCheck.ok) { + return { error: mcpError("Forbidden", roleCheck.error.message) }; + } + + // --- Profile completeness checks --- + if (auth.profile.mcp_status !== "active") { + return { + error: mcpError("Forbidden", "Account is not active."), + }; + } + if (!auth.profile.wallet_pubkey) { + return { + error: mcpError("Forbidden", "Profile has no wallet pubkey."), + }; + } + if (!auth.profile.github_handle) { + return { + error: mcpError("Forbidden", "Profile has no linked GitHub handle."), + }; + } + + const supabase = supabaseAdmin(); + + // --- Delegation guard --- + const delegationCheck = await requireWalletDelegated( + supabase, + auth.profile.user_id + ); + if (!delegationCheck.ok) { + return { error: mcpError("Forbidden", delegationCheck.error.message) }; + } + + // --- Load bounty --- + const { data: bounty, error: bountyErr } = await supabase + .from("issues") + .select("id, pda, github_issue_url, state, submission_count, chain_id") + .eq("id", parsed.data.bounty_id) + .maybeSingle(); + + if (bountyErr) return { error: mcpError("InternalError", bountyErr.message) }; + if (!bounty) return { error: mcpError("NotFound", "Bounty not found") }; + + const b = bounty as any; + + // --- Idempotency check: (solver, issue_pda, pr_url) — BEFORE state check --- + // A retry after the bounty closes must get idempotent success, not 409 Conflict. + const { data: existing } = await supabase + .from("submissions") + .select("id, state") + .eq("solver", auth.profile.wallet_pubkey) + .eq("issue_pda", b.pda) + .eq("pr_url", parsed.data.pr_url) + .maybeSingle(); + + if (existing) { + const ex = existing as any; + return { + submission_id: ex.id, + status: ex.state, + tx_signature: null, + idempotent: true, + }; + } + + // --- State check AFTER idempotency --- + if (b.state !== "open") { + return { + error: mcpError("Conflict", `Bounty is ${b.state}.`), + }; + } + + const repoUrl = parseRepoUrl(b.github_issue_url); + if (!repoUrl) { + return { + error: mcpError( + "InternalError", + "Could not parse bounty repo URL." + ), + }; + } + + // --- PR ownership pre-check (GHB-182) --- + const verify = await verifyPrOwnership({ + prUrl: parsed.data.pr_url, + expectedGithubHandle: auth.profile.github_handle, + expectedRepoUrl: repoUrl, + token: process.env.GITHUB_TOKEN, + }); + if (!verify.ok) { + // Transient failures (rate_limited, upstream_error) → ServiceUnavailable so + // the agent retries. Permanent failures (author_mismatch, wrong_repo, etc.) + // → Forbidden so the agent gives up. + const code = + verify.reason === "rate_limited" || verify.reason === "upstream_error" + ? "ServiceUnavailable" + : "Forbidden"; + return { + error: mcpError(code, `PR ownership check failed: ${verify.reason}`), + }; + } + + // --- Fetch latest blockhash --- + const rpc = solanaRpc(); + const blockhashResp = await (rpc as any).getLatestBlockhash().send(); + const blockhash: string = blockhashResp.value.blockhash; + + // --- Build submit_solution transaction (Task 7) --- + const gasStationPubkey = process.env.GAS_STATION_PUBKEY; + if (!gasStationPubkey) { + return { + error: mcpError("InternalError", "GAS_STATION_PUBKEY not configured."), + }; + } + + const built = await buildSubmitSolutionTx({ + rpcUrl: process.env.SOLANA_RPC_URL ?? "", + bountyPda: b.pda, + solver: auth.profile.wallet_pubkey, + gasStationPubkey, + prUrl: parsed.data.pr_url, + submissionCount: b.submission_count ?? 0, + blockhash, + }); + + // --- Privy delegated signing (Task 6) --- + // + // ASSUMPTION: `walletId === wallet_pubkey` (Solana base58 address). + // If Privy's internal walletId differs from the on-chain address, the + // consent flow (Task 12 / GHB-187) must persist Privy's returned + // walletId at delegation time and we read it here. Document this in the + // PR so the Task 12 implementor is aware. + const privyClient = getPrivyServerClient(); + const signed = await signSolanaTransaction(privyClient, { + walletId: auth.profile.wallet_pubkey, + unsignedTx: built.unsignedTx, + }); + + if (!signed.ok) { + if (signed.reason === "delegation_revoked") { + // Mark delegation revoked so future calls fast-fail cleanly. + await supabase + .from("agent_delegations") + .update({ revoked_at: new Date().toISOString() }) + .eq("user_id", auth.profile.user_id); + return { + error: mcpError( + "Forbidden", + "Wallet delegation revoked — re-authorize at /app/credentials." + ), + }; + } + return { + error: mcpError( + "ServiceUnavailable", + "Signing service temporarily unavailable." + ), + }; + } + + // --- Gas station signs as fee payer + submits on-chain --- + const submission = await submitSponsoredTx(signed.signedTx); + if (!submission.ok) { + return { + error: mcpError( + "InternalError", + `On-chain submission failed: ${submission.reason}` + ), + }; + } + + // --- Insert mirror row in DB --- + // opus_report_hash is a text column — store as 64-char hex of 32 zero bytes. + // The relayer back-fills the real hash after scoring. + // + // We use upsert with ignoreDuplicates to handle the race where the relayer's + // on-chain watcher inserts the same row before we do. Without this, the insert + // would error and we'd fall into the mirror_insert_failed branch even though + // the row exists. + // NOTE: integration test for the upsert race is not included — the conflict + // branch is hard to mock cleanly; rely on the relayer smoke test instead. + const chainId = b.chain_id ?? getChainId(); + const { data: insertRow, error: insertErr } = await supabase + .from("submissions") + .upsert( + { + pda: built.submissionPda, + chain_id: chainId, + solver: auth.profile.wallet_pubkey, + pr_url: parsed.data.pr_url, + issue_pda: b.pda, + submission_index: built.submissionIndex, + opus_report_hash: ZERO_OPUS_HASH, + tx_hash: submission.signature, + state: "pending", + }, + { onConflict: "pda", ignoreDuplicates: true } + ) + .select("id") + .maybeSingle(); + + if (insertErr) { + // Genuine error (not a conflict) — on-chain tx succeeded so return the + // signature anyway; the relayer's watcher will reconcile the DB row later. + return { + submission_id: null, + status: "pending", + tx_signature: submission.signature, + submission_pda: built.submissionPda, + mirror_insert_failed: true, + }; + } + + if (!insertRow) { + // Upsert ignored the duplicate — the relayer raced us and inserted first. + // Fetch the existing row by pda + solver so we can return its id. + const { data: existingRow } = await supabase + .from("submissions") + .select("id") + .eq("pda", built.submissionPda) + .eq("solver", auth.profile.wallet_pubkey) + .maybeSingle(); + return { + submission_id: (existingRow as any)?.id ?? null, + status: "pending", + tx_signature: submission.signature, + submission_pda: built.submissionPda, + }; + } + + return { + submission_id: (insertRow as any).id, + status: "pending", + tx_signature: submission.signature, + submission_pda: built.submissionPda, + }; +} + +export function registerSubmissionsCreate(server: McpServer): void { + server.tool( + "submissions.create", + { + bounty_id: z.string().uuid(), + pr_url: z.string().url().max(200), + }, + async (input, extra) => { + const authorization = (extra as any)?.requestInfo?.headers?.authorization; + const result = await handleSubmissionsCreate({ ...input, authorization }); + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + }; + } + ); +} diff --git a/apps/mcp/tests/tools/submissions/create.test.ts b/apps/mcp/tests/tools/submissions/create.test.ts new file mode 100644 index 0000000..b7e92e8 --- /dev/null +++ b/apps/mcp/tests/tools/submissions/create.test.ts @@ -0,0 +1,336 @@ +/** + * submissions.create — happy-path test (GHB-187). + * Error-case tests live in Task 10. + */ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +// --- Module mocks (hoisted before imports) --- + +vi.mock("@/lib/auth/middleware", () => ({ + authenticate: vi.fn(), +})); + +vi.mock("@/lib/supabase/admin", () => ({ + supabaseAdmin: vi.fn(), +})); + +vi.mock("@ghbounty/shared", () => ({ + verifyPrOwnership: vi.fn(), +})); + +vi.mock("@/lib/privy/delegated-signer", () => ({ + getPrivyServerClient: vi.fn(), + signSolanaTransaction: vi.fn(), +})); + +vi.mock("@/lib/solana/build-submit-solution-tx", () => ({ + buildSubmitSolutionTx: vi.fn(), +})); + +vi.mock("@/lib/solana/rpc", () => ({ + solanaRpc: vi.fn(), +})); + +vi.mock("@/lib/gas-station/server", () => ({ + submitSponsoredTx: vi.fn(), +})); + +vi.mock("@/lib/config", () => ({ + getChainId: () => "solana-devnet", + getProgramAddress: () => "test_program_addr", +})); + +// --- Imports after mocks --- +import { handleSubmissionsCreate } from "@/lib/tools/submissions/create"; +import { authenticate } from "@/lib/auth/middleware"; +import { supabaseAdmin } from "@/lib/supabase/admin"; +import { verifyPrOwnership } from "@ghbounty/shared"; +import { + getPrivyServerClient, + signSolanaTransaction, +} from "@/lib/privy/delegated-signer"; +import { buildSubmitSolutionTx } from "@/lib/solana/build-submit-solution-tx"; +import { solanaRpc } from "@/lib/solana/rpc"; +import { submitSponsoredTx } from "@/lib/gas-station/server"; + +// --- Fixtures --- + +const baseProfile = { + user_id: "did:privy:alice", + role: "dev" as const, + mcp_status: "active" as const, + wallet_pubkey: "Solver111", + github_handle: "alice", +}; + +function buildSupabase( + opts: { existingSubmission?: boolean; bountyState?: string } = {} +) { + return { + from: (table: string) => { + if (table === "agent_delegations") { + return { + select: () => ({ + eq: () => ({ + maybeSingle: () => + Promise.resolve({ data: { revoked_at: null }, error: null }), + }), + }), + update: () => ({ + eq: () => Promise.resolve({ error: null }), + }), + }; + } + if (table === "issues") { + return { + select: () => ({ + eq: () => ({ + maybeSingle: () => + Promise.resolve({ + data: { + id: "bounty-1", + pda: "BountyPda1", + chain_id: "solana-devnet", + github_issue_url: + "https://github.com/acme/proj/issues/42", + state: opts.bountyState ?? "open", + submission_count: 0, + }, + error: null, + }), + }), + }), + }; + } + if (table === "submissions") { + // First call: idempotency check (no existing submission by default) + // Second call: insert + return { + select: () => ({ + eq: () => ({ + eq: () => ({ + eq: () => ({ + maybeSingle: () => + Promise.resolve({ + data: opts.existingSubmission + ? { id: "sub-dup", state: "pending" } + : null, + error: null, + }), + }), + }), + }), + }), + upsert: () => ({ + select: () => ({ + maybeSingle: () => + Promise.resolve({ data: { id: "sub-new" }, error: null }), + }), + }), + }; + } + return { select: () => ({}) }; + }, + } as any; +} + +// --- Tests --- + +describe("submissions.create — happy path", () => { + beforeEach(() => { + vi.clearAllMocks(); + + (authenticate as any).mockResolvedValue({ + ok: true, + profile: baseProfile, + credentialId: "k1", + credentialKind: "api_key", + }); + + (supabaseAdmin as any).mockReturnValue(buildSupabase()); + + (verifyPrOwnership as any).mockResolvedValue({ ok: true }); + + (getPrivyServerClient as any).mockReturnValue({}); + (signSolanaTransaction as any).mockResolvedValue({ + ok: true, + signedTx: new Uint8Array([7, 7, 7]), + }); + + (buildSubmitSolutionTx as any).mockResolvedValue({ + unsignedTx: new Uint8Array([1, 2, 3]), + submissionPda: "Sub111", + submissionIndex: 0, + }); + + (solanaRpc as any).mockReturnValue({ + getLatestBlockhash: () => ({ + send: async () => ({ + value: { blockhash: "Bh1", lastValidBlockHeight: 1 }, + }), + }), + }); + + (submitSponsoredTx as any).mockResolvedValue({ + ok: true, + signature: "tx_sig_123", + }); + + process.env.GAS_STATION_PUBKEY = "Ga1111"; + process.env.SOLANA_RPC_URL = "https://example.invalid"; + process.env.CHAIN_ID = "solana-devnet"; + }); + + it("creates a submission and returns the id + tx signature", async () => { + const result = await handleSubmissionsCreate({ + authorization: "Bearer x", + bounty_id: "00000000-0000-0000-0000-000000000001", + pr_url: "https://github.com/acme/proj/pull/1", + }); + + expect(result).toMatchObject({ + submission_id: "sub-new", + status: "pending", + tx_signature: "tx_sig_123", + submission_pda: "Sub111", + }); + expect((result as any).idempotent).toBeUndefined(); + }); + + it("returns idempotent result when submission already exists", async () => { + (supabaseAdmin as any).mockReturnValue( + buildSupabase({ existingSubmission: true }) + ); + + const result = await handleSubmissionsCreate({ + authorization: "Bearer x", + bounty_id: "00000000-0000-0000-0000-000000000001", + pr_url: "https://github.com/acme/proj/pull/1", + }); + + expect(result).toMatchObject({ + submission_id: "sub-dup", + status: "pending", + tx_signature: null, + idempotent: true, + }); + // Gas station should NOT have been called + expect(submitSponsoredTx).not.toHaveBeenCalled(); + }); + + it("returns Forbidden when role is company", async () => { + (authenticate as any).mockResolvedValue({ + ok: true, + profile: { ...baseProfile, role: "company" }, + credentialId: "k1", + credentialKind: "api_key", + }); + + const result = await handleSubmissionsCreate({ + authorization: "Bearer x", + bounty_id: "00000000-0000-0000-0000-000000000001", + pr_url: "https://github.com/acme/proj/pull/1", + }); + + expect((result as any).error?.code).toBe("Forbidden"); + }); + + it("returns Forbidden when PR ownership check fails", async () => { + (verifyPrOwnership as any).mockResolvedValue({ + ok: false, + reason: "author_mismatch", + }); + + const result = await handleSubmissionsCreate({ + authorization: "Bearer x", + bounty_id: "00000000-0000-0000-0000-000000000001", + pr_url: "https://github.com/acme/proj/pull/1", + }); + + expect((result as any).error?.code).toBe("Forbidden"); + expect(submitSponsoredTx).not.toHaveBeenCalled(); + }); + + it("returns Conflict when bounty is not open", async () => { + (supabaseAdmin as any).mockReturnValue( + buildSupabase({ bountyState: "resolved" }) + ); + + const result = await handleSubmissionsCreate({ + authorization: "Bearer x", + bounty_id: "00000000-0000-0000-0000-000000000001", + pr_url: "https://github.com/acme/proj/pull/1", + }); + + expect((result as any).error?.code).toBe("Conflict"); + }); + + it("returns idempotent even if the bounty was closed after the original submission", async () => { + // Scenario: the agent submitted successfully, bounty was then resolved, and + // the agent retries the same call. Idempotency check runs BEFORE the state + // check, so the existing submission is found and returned — NOT 409 Conflict. + (supabaseAdmin as any).mockReturnValue( + buildSupabase({ existingSubmission: true, bountyState: "resolved" }) + ); + + const result = await handleSubmissionsCreate({ + authorization: "Bearer x", + bounty_id: "00000000-0000-0000-0000-000000000001", + pr_url: "https://github.com/acme/proj/pull/1", + }); + + expect(result).toMatchObject({ + submission_id: "sub-dup", + status: "pending", + tx_signature: null, + idempotent: true, + }); + expect((result as any).error).toBeUndefined(); + // Gas station must NOT have been called + expect(submitSponsoredTx).not.toHaveBeenCalled(); + }); + + it("returns ServiceUnavailable when PR ownership check fails with rate_limited", async () => { + (verifyPrOwnership as any).mockResolvedValue({ + ok: false, + reason: "rate_limited", + }); + + const result = await handleSubmissionsCreate({ + authorization: "Bearer x", + bounty_id: "00000000-0000-0000-0000-000000000001", + pr_url: "https://github.com/acme/proj/pull/1", + }); + + expect((result as any).error?.code).toBe("ServiceUnavailable"); + expect(submitSponsoredTx).not.toHaveBeenCalled(); + }); + + it("returns ServiceUnavailable when PR ownership check fails with upstream_error", async () => { + (verifyPrOwnership as any).mockResolvedValue({ + ok: false, + reason: "upstream_error", + }); + + const result = await handleSubmissionsCreate({ + authorization: "Bearer x", + bounty_id: "00000000-0000-0000-0000-000000000001", + pr_url: "https://github.com/acme/proj/pull/1", + }); + + expect((result as any).error?.code).toBe("ServiceUnavailable"); + expect(submitSponsoredTx).not.toHaveBeenCalled(); + }); + + it("returns InvalidInput when pr_url exceeds 200 characters", async () => { + const longUrl = "https://github.com/acme/proj/pull/" + "x".repeat(200); + + const result = await handleSubmissionsCreate({ + authorization: "Bearer x", + bounty_id: "00000000-0000-0000-0000-000000000001", + pr_url: longUrl, + }); + + expect((result as any).error?.code).toBe("InvalidInput"); + }); +}); From e557c47d601586b0cffa7ec29453945deec63108 Mon Sep 17 00:00:00 2001 From: gastonfoncea Date: Mon, 18 May 2026 20:05:08 -0300 Subject: [PATCH 14/21] =?UTF-8?q?test(mcp):=20cover=20remaining=20error=20?= =?UTF-8?q?cases=20for=20submissions.create=20=E2=80=94=20GHB-187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 4 missing error-case tests: mcp_status suspended → Forbidden, no active delegation → Forbidden, bounty not found → NotFound, and Privy delegation_revoked → Forbidden with DB update verification. Co-Authored-By: Claude Sonnet 4.6 --- .../tests/tools/submissions/create.test.ts | 129 +++++++++++++++--- 1 file changed, 112 insertions(+), 17 deletions(-) diff --git a/apps/mcp/tests/tools/submissions/create.test.ts b/apps/mcp/tests/tools/submissions/create.test.ts index b7e92e8..8efdcee 100644 --- a/apps/mcp/tests/tools/submissions/create.test.ts +++ b/apps/mcp/tests/tools/submissions/create.test.ts @@ -64,21 +64,38 @@ const baseProfile = { }; function buildSupabase( - opts: { existingSubmission?: boolean; bountyState?: string } = {} + opts: { + existingSubmission?: boolean; + bountyState?: string; + noDelegation?: boolean; + noBounty?: boolean; + onAgentDelegationUpdate?: (...args: unknown[]) => void; + } = {} ) { return { from: (table: string) => { if (table === "agent_delegations") { + const updateFn = vi.fn(() => ({ + eq: () => Promise.resolve({ error: null }), + })); + if (opts.onAgentDelegationUpdate) { + updateFn.mockImplementation((...args: unknown[]) => { + opts.onAgentDelegationUpdate!(...args); + return { eq: () => Promise.resolve({ error: null }) }; + }); + } return { select: () => ({ eq: () => ({ maybeSingle: () => - Promise.resolve({ data: { revoked_at: null }, error: null }), + Promise.resolve( + opts.noDelegation + ? { data: null, error: null } + : { data: { revoked_at: null }, error: null } + ), }), }), - update: () => ({ - eq: () => Promise.resolve({ error: null }), - }), + update: updateFn, }; } if (table === "issues") { @@ -86,18 +103,22 @@ function buildSupabase( select: () => ({ eq: () => ({ maybeSingle: () => - Promise.resolve({ - data: { - id: "bounty-1", - pda: "BountyPda1", - chain_id: "solana-devnet", - github_issue_url: - "https://github.com/acme/proj/issues/42", - state: opts.bountyState ?? "open", - submission_count: 0, - }, - error: null, - }), + Promise.resolve( + opts.noBounty + ? { data: null, error: null } + : { + data: { + id: "bounty-1", + pda: "BountyPda1", + chain_id: "solana-devnet", + github_issue_url: + "https://github.com/acme/proj/issues/42", + state: opts.bountyState ?? "open", + submission_count: 0, + }, + error: null, + } + ), }), }), }; @@ -333,4 +354,78 @@ describe("submissions.create — happy path", () => { expect((result as any).error?.code).toBe("InvalidInput"); }); + + it("returns Forbidden when mcp_status is not active", async () => { + (authenticate as any).mockResolvedValue({ + ok: true, + profile: { ...baseProfile, mcp_status: "suspended" }, + credentialId: "k1", + credentialKind: "api_key", + }); + + const result = await handleSubmissionsCreate({ + authorization: "Bearer x", + bounty_id: "00000000-0000-0000-0000-000000000001", + pr_url: "https://github.com/acme/proj/pull/1", + }); + + expect((result as any).error?.code).toBe("Forbidden"); + expect(submitSponsoredTx).not.toHaveBeenCalled(); + }); + + it("returns Forbidden when user has no active delegation", async () => { + (supabaseAdmin as any).mockReturnValue(buildSupabase({ noDelegation: true })); + + const result = await handleSubmissionsCreate({ + authorization: "Bearer x", + bounty_id: "00000000-0000-0000-0000-000000000001", + pr_url: "https://github.com/acme/proj/pull/1", + }); + + expect((result as any).error?.code).toBe("Forbidden"); + expect(submitSponsoredTx).not.toHaveBeenCalled(); + }); + + it("returns NotFound when bounty does not exist", async () => { + (supabaseAdmin as any).mockReturnValue(buildSupabase({ noBounty: true })); + + const result = await handleSubmissionsCreate({ + authorization: "Bearer x", + bounty_id: "00000000-0000-0000-0000-000000000001", + pr_url: "https://github.com/acme/proj/pull/1", + }); + + expect((result as any).error?.code).toBe("NotFound"); + expect(submitSponsoredTx).not.toHaveBeenCalled(); + }); + + it("returns Forbidden and updates agent_delegations when Privy returns delegation_revoked", async () => { + const updateCalls: unknown[][] = []; + (supabaseAdmin as any).mockReturnValue( + buildSupabase({ + onAgentDelegationUpdate: (...args: unknown[]) => { + updateCalls.push(args); + }, + }) + ); + + (signSolanaTransaction as any).mockResolvedValue({ + ok: false, + reason: "delegation_revoked", + }); + + const result = await handleSubmissionsCreate({ + authorization: "Bearer x", + bounty_id: "00000000-0000-0000-0000-000000000001", + pr_url: "https://github.com/acme/proj/pull/1", + }); + + expect((result as any).error?.code).toBe("Forbidden"); + expect(submitSponsoredTx).not.toHaveBeenCalled(); + // Verify the DB update on agent_delegations was called with revoked_at + expect(updateCalls.length).toBe(1); + expect((updateCalls[0][0] as any)).toMatchObject({ + revoked_at: expect.any(String), + }); + }); }); From d865d74b7da068d15535081f9c043af5a9667ebc Mon Sep 17 00:00:00 2001 From: gastonfoncea Date: Mon, 18 May 2026 21:59:20 -0300 Subject: [PATCH 15/21] =?UTF-8?q?feat(frontend):=20add=20agent-delegation?= =?UTF-8?q?=20API=20route=20=E2=80=94=20GHB-187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit POST /api/agent-delegation handles delegate (upsert) and revoke (set revoked_at) actions; GET returns the caller's current delegation row. Auth via Privy JWT (same pattern as stake/api-keys routes). Core logic extracted to lib/agent-delegation-route-core.ts with 10 vitest tests. Also adds agent_delegations table types to db.types.ts. Co-Authored-By: Claude Sonnet 4.6 --- frontend/app/api/agent-delegation/route.ts | 108 ++++++++ frontend/lib/agent-delegation-route-core.ts | 100 ++++++++ frontend/lib/db.types.ts | 23 ++ .../tests/agent-delegation-route-core.test.ts | 241 ++++++++++++++++++ 4 files changed, 472 insertions(+) create mode 100644 frontend/app/api/agent-delegation/route.ts create mode 100644 frontend/lib/agent-delegation-route-core.ts create mode 100644 frontend/tests/agent-delegation-route-core.test.ts diff --git a/frontend/app/api/agent-delegation/route.ts b/frontend/app/api/agent-delegation/route.ts new file mode 100644 index 0000000..b2f74f3 --- /dev/null +++ b/frontend/app/api/agent-delegation/route.ts @@ -0,0 +1,108 @@ +/** + * GHB-187 — `GET /api/agent-delegation` and `POST /api/agent-delegation`. + * + * Thin wrapper over `lib/agent-delegation-route-core.ts`. Auth via Privy JWT. + * + * POST body: { action: "delegate", wallet_pubkey: string, chain_type?: string } + * | { action: "revoke" } + * + * GET — returns { delegation: { wallet_pubkey, chain_type, delegated_at, + * revoked_at } | null } + */ + +import { NextResponse } from "next/server"; +import { createRemoteJWKSet } from "jose"; + +import { verifyPrivyToken } from "@/lib/gas-station-route-core"; +import { getServiceRoleClient } from "@/utils/supabase/service-role"; +import { + delegateWallet, + revokeWallet, + getDelegation, +} from "@/lib/agent-delegation-route-core"; + +export const runtime = "nodejs"; + +const PRIVY_APP_ID = process.env.NEXT_PUBLIC_PRIVY_APP_ID ?? ""; +const PRIVY_JWKS_URL = PRIVY_APP_ID + ? new URL(`https://auth.privy.io/api/v1/apps/${PRIVY_APP_ID}/jwks.json`) + : null; +const privyJWKS = PRIVY_JWKS_URL ? createRemoteJWKSet(PRIVY_JWKS_URL) : null; + +async function resolveUserId(req: Request): Promise { + if (!PRIVY_APP_ID || !privyJWKS) return null; + const h = req.headers.get("authorization"); + if (!h || !h.startsWith("Bearer ")) return null; + try { + const { sub } = await verifyPrivyToken(h.slice("Bearer ".length).trim(), { + privyAppId: PRIVY_APP_ID, + verifyKey: privyJWKS, + }); + return sub; + } catch { + return null; + } +} + +export async function GET(req: Request) { + const user_id = await resolveUserId(req); + if (!user_id) + return NextResponse.json({ error: "unauthorized" }, { status: 401 }); + + const result = await getDelegation(getServiceRoleClient(), user_id); + if (!result.ok) + return NextResponse.json({ error: "internal" }, { status: 500 }); + + return NextResponse.json({ delegation: result.delegation }); +} + +export async function POST(req: Request) { + const user_id = await resolveUserId(req); + if (!user_id) + return NextResponse.json({ error: "unauthorized" }, { status: 401 }); + + let body: unknown; + try { + body = await req.json(); + } catch { + return NextResponse.json({ error: "bad_request" }, { status: 400 }); + } + + if (!body || typeof body !== "object") { + return NextResponse.json({ error: "bad_request" }, { status: 400 }); + } + + const b = body as Record; + const action = b.action; + + if (action === "delegate") { + if (typeof b.wallet_pubkey !== "string" || b.wallet_pubkey.length === 0) { + return NextResponse.json( + { error: "wallet_pubkey required" }, + { status: 400 }, + ); + } + const chain_type = + typeof b.chain_type === "string" ? b.chain_type : "solana"; + + const result = await delegateWallet(getServiceRoleClient(), { + user_id, + wallet_pubkey: b.wallet_pubkey, + chain_type, + }); + if (!result.ok) + return NextResponse.json({ error: "internal" }, { status: 500 }); + + return NextResponse.json({ ok: true }); + } + + if (action === "revoke") { + const result = await revokeWallet(getServiceRoleClient(), user_id); + if (!result.ok) + return NextResponse.json({ error: "internal" }, { status: 500 }); + + return NextResponse.json({ ok: true }); + } + + return NextResponse.json({ error: "invalid action" }, { status: 400 }); +} diff --git a/frontend/lib/agent-delegation-route-core.ts b/frontend/lib/agent-delegation-route-core.ts new file mode 100644 index 0000000..a41913f --- /dev/null +++ b/frontend/lib/agent-delegation-route-core.ts @@ -0,0 +1,100 @@ +/** + * GHB-187 — pure handler for `POST /api/agent-delegation` and + * `GET /api/agent-delegation`. + * + * All logic lives here so tests can drive it without spinning up a + * Next.js server. The route file at + * `app/api/agent-delegation/route.ts` is a thin adapter. + * + * POST actions: + * delegate — upsert a row into `agent_delegations` (wallet_pubkey + chain_type) + * revoke — set revoked_at = now() on the caller's row + * + * GET — returns the caller's current delegation row (or null if none). + */ + +import type { SupabaseClient } from "@supabase/supabase-js"; +import type { Database } from "@/lib/db.types"; + +type Supabase = SupabaseClient; + +// --------------------------------------------------------------------------- +// Delegate +// --------------------------------------------------------------------------- + +export interface DelegateInput { + user_id: string; + wallet_pubkey: string; + chain_type?: string; +} + +export type DelegateResult = + | { ok: true } + | { ok: false; error: "internal"; detail: string }; + +export async function delegateWallet( + supabase: Supabase, + input: DelegateInput, +): Promise { + const now = new Date().toISOString(); + const { error } = await supabase.from("agent_delegations").upsert( + { + user_id: input.user_id, + wallet_pubkey: input.wallet_pubkey, + chain_type: input.chain_type ?? "solana", + delegated_at: now, + revoked_at: null, + updated_at: now, + }, + { onConflict: "user_id" }, + ); + if (error) return { ok: false, error: "internal", detail: error.message }; + return { ok: true }; +} + +// --------------------------------------------------------------------------- +// Revoke +// --------------------------------------------------------------------------- + +export type RevokeResult = + | { ok: true } + | { ok: false; error: "internal"; detail: string }; + +export async function revokeWallet( + supabase: Supabase, + user_id: string, +): Promise { + const now = new Date().toISOString(); + const { error } = await supabase + .from("agent_delegations") + .update({ revoked_at: now, updated_at: now }) + .eq("user_id", user_id); + if (error) return { ok: false, error: "internal", detail: error.message }; + return { ok: true }; +} + +// --------------------------------------------------------------------------- +// Get +// --------------------------------------------------------------------------- + +export type AgentDelegationRow = Pick< + Database["public"]["Tables"]["agent_delegations"]["Row"], + "wallet_pubkey" | "chain_type" | "delegated_at" | "revoked_at" +>; + +export type GetDelegationResult = + | { ok: true; delegation: AgentDelegationRow | null } + | { ok: false; error: "internal"; detail: string }; + +export async function getDelegation( + supabase: Supabase, + user_id: string, +): Promise { + const { data, error } = await supabase + .from("agent_delegations") + .select("wallet_pubkey, chain_type, delegated_at, revoked_at") + .eq("user_id", user_id) + .maybeSingle(); + if (error) return { ok: false, error: "internal", detail: error.message }; + return { ok: true, delegation: data }; +} diff --git a/frontend/lib/db.types.ts b/frontend/lib/db.types.ts index ae4ef81..450621e 100644 --- a/frontend/lib/db.types.ts +++ b/frontend/lib/db.types.ts @@ -334,6 +334,29 @@ export type Database = { Update: Partial; Relationships: []; }; + // GHB-187: server-side signing consent for MCP submit_pr flow. + agent_delegations: { + Row: { + user_id: string; + wallet_pubkey: string; + chain_type: string; + delegated_at: string; + revoked_at: string | null; + created_at: string; + updated_at: string; + }; + Insert: { + user_id: string; + wallet_pubkey: string; + chain_type?: string; + delegated_at?: string; + revoked_at?: string | null; + created_at?: string; + updated_at?: string; + }; + Update: Partial; + Relationships: []; + }; }; Views: Record; Functions: Record; diff --git a/frontend/tests/agent-delegation-route-core.test.ts b/frontend/tests/agent-delegation-route-core.test.ts new file mode 100644 index 0000000..5af477b --- /dev/null +++ b/frontend/tests/agent-delegation-route-core.test.ts @@ -0,0 +1,241 @@ +/** + * GHB-187 — tests for `lib/agent-delegation-route-core.ts`. + * + * Tests the three exported functions: + * - delegateWallet(supabase, input) + * - revokeWallet(supabase, user_id) + * - getDelegation(supabase, user_id) + * + * Uses hand-rolled Supabase mocks (no network), following the same + * pattern as api-keys-route-core.test.ts. + */ + +import { describe, expect, test, vi } from "vitest"; +import { + delegateWallet, + revokeWallet, + getDelegation, +} from "@/lib/agent-delegation-route-core"; +import type { SupabaseClient } from "@supabase/supabase-js"; +import type { Database } from "@/lib/db.types"; + +// --------------------------------------------------------------------------- +// Fixtures +// --------------------------------------------------------------------------- + +const USER_ID = "did:privy:test_user"; +const WALLET = "BPFLoaderUpgradeab1e11111111111111111111111"; +const NOW = "2026-05-18T00:00:00.000Z"; + +type DelegationRow = + Database["public"]["Tables"]["agent_delegations"]["Row"]; + +// --------------------------------------------------------------------------- +// delegateWallet +// --------------------------------------------------------------------------- + +describe("delegateWallet", () => { + test("returns ok:true on successful upsert", async () => { + const supabase = { + from: vi.fn().mockReturnValue({ + upsert: vi.fn().mockResolvedValue({ error: null }), + }), + } as unknown as SupabaseClient; + + const result = await delegateWallet(supabase, { + user_id: USER_ID, + wallet_pubkey: WALLET, + }); + + expect(result.ok).toBe(true); + expect(supabase.from).toHaveBeenCalledWith("agent_delegations"); + }); + + test("defaults chain_type to 'solana'", async () => { + let capturedRows: unknown; + const supabase = { + from: vi.fn().mockReturnValue({ + upsert: vi.fn().mockImplementation((rows: unknown) => { + capturedRows = rows; + return Promise.resolve({ error: null }); + }), + }), + } as unknown as SupabaseClient; + + await delegateWallet(supabase, { user_id: USER_ID, wallet_pubkey: WALLET }); + + expect((capturedRows as Record).chain_type).toBe("solana"); + }); + + test("passes chain_type when provided", async () => { + let capturedRows: unknown; + const supabase = { + from: vi.fn().mockReturnValue({ + upsert: vi.fn().mockImplementation((rows: unknown) => { + capturedRows = rows; + return Promise.resolve({ error: null }); + }), + }), + } as unknown as SupabaseClient; + + await delegateWallet(supabase, { + user_id: USER_ID, + wallet_pubkey: WALLET, + chain_type: "ethereum", + }); + + expect((capturedRows as Record).chain_type).toBe("ethereum"); + }); + + test("sets revoked_at to null on upsert", async () => { + let capturedRows: unknown; + const supabase = { + from: vi.fn().mockReturnValue({ + upsert: vi.fn().mockImplementation((rows: unknown) => { + capturedRows = rows; + return Promise.resolve({ error: null }); + }), + }), + } as unknown as SupabaseClient; + + await delegateWallet(supabase, { user_id: USER_ID, wallet_pubkey: WALLET }); + + expect((capturedRows as Record).revoked_at).toBeNull(); + }); + + test("returns ok:false with detail on Supabase error", async () => { + const supabase = { + from: vi.fn().mockReturnValue({ + upsert: vi.fn().mockResolvedValue({ + error: { message: "FK violation" }, + }), + }), + } as unknown as SupabaseClient; + + const result = await delegateWallet(supabase, { + user_id: USER_ID, + wallet_pubkey: WALLET, + }); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toBe("internal"); + expect(result.detail).toBe("FK violation"); + } + }); +}); + +// --------------------------------------------------------------------------- +// revokeWallet +// --------------------------------------------------------------------------- + +describe("revokeWallet", () => { + test("returns ok:true on successful update", async () => { + const eqFn = vi.fn().mockResolvedValue({ error: null }); + const supabase = { + from: vi.fn().mockReturnValue({ + update: vi.fn().mockReturnValue({ eq: eqFn }), + }), + } as unknown as SupabaseClient; + + const result = await revokeWallet(supabase, USER_ID); + + expect(result.ok).toBe(true); + expect(eqFn).toHaveBeenCalledWith("user_id", USER_ID); + }); + + test("returns ok:false with detail on Supabase error", async () => { + const supabase = { + from: vi.fn().mockReturnValue({ + update: vi.fn().mockReturnValue({ + eq: vi.fn().mockResolvedValue({ error: { message: "DB error" } }), + }), + }), + } as unknown as SupabaseClient; + + const result = await revokeWallet(supabase, USER_ID); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toBe("internal"); + expect(result.detail).toBe("DB error"); + } + }); +}); + +// --------------------------------------------------------------------------- +// getDelegation +// --------------------------------------------------------------------------- + +describe("getDelegation", () => { + test("returns delegation row when found", async () => { + const row: Partial = { + wallet_pubkey: WALLET, + chain_type: "solana", + delegated_at: NOW, + revoked_at: null, + }; + + const supabase = { + from: vi.fn().mockReturnValue({ + select: vi.fn().mockReturnValue({ + eq: vi.fn().mockReturnValue({ + maybeSingle: vi.fn().mockResolvedValue({ data: row, error: null }), + }), + }), + }), + } as unknown as SupabaseClient; + + const result = await getDelegation(supabase, USER_ID); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.delegation).not.toBeNull(); + expect(result.delegation?.wallet_pubkey).toBe(WALLET); + expect(result.delegation?.chain_type).toBe("solana"); + expect(result.delegation?.revoked_at).toBeNull(); + } + }); + + test("returns null delegation when no row exists", async () => { + const supabase = { + from: vi.fn().mockReturnValue({ + select: vi.fn().mockReturnValue({ + eq: vi.fn().mockReturnValue({ + maybeSingle: vi.fn().mockResolvedValue({ data: null, error: null }), + }), + }), + }), + } as unknown as SupabaseClient; + + const result = await getDelegation(supabase, USER_ID); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.delegation).toBeNull(); + } + }); + + test("returns ok:false with detail on Supabase error", async () => { + const supabase = { + from: vi.fn().mockReturnValue({ + select: vi.fn().mockReturnValue({ + eq: vi.fn().mockReturnValue({ + maybeSingle: vi.fn().mockResolvedValue({ + data: null, + error: { message: "query failed" }, + }), + }), + }), + }), + } as unknown as SupabaseClient; + + const result = await getDelegation(supabase, USER_ID); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toBe("internal"); + expect(result.detail).toBe("query failed"); + } + }); +}); From 5627e7d52ecd645cd3e8e7ef5aaf973fceb0b4ea Mon Sep 17 00:00:00 2001 From: gastonfoncea Date: Mon, 18 May 2026 22:07:08 -0300 Subject: [PATCH 16/21] =?UTF-8?q?feat(frontend):=20add=20AgentDelegationCa?= =?UTF-8?q?rd=20consent=20UI=20=E2=80=94=20GHB-187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../app/credentials/AgentDelegationCard.tsx | 449 ++++++++++++++++++ 1 file changed, 449 insertions(+) create mode 100644 frontend/app/app/credentials/AgentDelegationCard.tsx diff --git a/frontend/app/app/credentials/AgentDelegationCard.tsx b/frontend/app/app/credentials/AgentDelegationCard.tsx new file mode 100644 index 0000000..986662a --- /dev/null +++ b/frontend/app/app/credentials/AgentDelegationCard.tsx @@ -0,0 +1,449 @@ +"use client"; + +/** + * GHB-187 — Agent wallet delegation consent UI. + * + * Shows the current delegation state (active / not authorized) and lets the + * user authorize or revoke server-side Solana signing via Privy's headless + * delegation API. + * + * Hook notes (v3.22.x): + * - `useHeadlessDelegatedActions` — exists; provides `delegateWallet` and + * `revokeWallets` without any modal UI. + * - `useSolanaWallets` — does NOT exist in this version. We derive the + * Solana wallet from `usePrivy().user.linkedAccounts`, filtering for + * `type === 'wallet' && chainType === 'solana'`. + * - `useWallets` from `@privy-io/react-auth` returns Ethereum-only + * `ConnectedWallet[]` and does not include Solana wallets. + */ + +import { useCallback, useEffect, useState } from "react"; +import { + usePrivy, + useHeadlessDelegatedActions, + type LinkedAccountWithMetadata, +} from "@privy-io/react-auth"; + +import { Button } from "@/components/ui/button"; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +interface DelegationRecord { + wallet_pubkey: string; + chain_type: string; + delegated_at: string; + revoked_at: string | null; +} + +interface GetDelegationResponse { + delegation: DelegationRecord | null; +} + +/** Narrow a LinkedAccountWithMetadata to a Solana wallet entry. */ +function isSolanaWalletAccount( + account: LinkedAccountWithMetadata, +): account is Extract { + return account.type === "wallet" && account.chainType === "solana"; +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** "May 17, 2026, 14:32" — locale-aware, no external library. */ +function formatTimestamp(iso: string): string { + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return iso; + return d.toLocaleString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +} + +/** Truncate a pubkey to `AAAA…ZZZZ` for compact display. */ +function shortPubkey(pubkey: string): string { + if (pubkey.length <= 12) return pubkey; + return `${pubkey.slice(0, 6)}…${pubkey.slice(-4)}`; +} + +// --------------------------------------------------------------------------- +// Component +// --------------------------------------------------------------------------- + +export function AgentDelegationCard() { + const { user, getAccessToken } = usePrivy(); + const { delegateWallet, revokeWallets } = useHeadlessDelegatedActions(); + + const [delegation, setDelegation] = useState(null); + const [loadingState, setLoadingState] = useState<"idle" | "fetching" | "mutating">("fetching"); + const [error, setError] = useState(null); + + // Derive the first Solana wallet from the user's linked accounts. + // `useWallets()` from @privy-io/react-auth only surfaces Ethereum wallets; + // Solana wallets must be found via `user.linkedAccounts`. + const solanaWallet = + user?.linkedAccounts.find(isSolanaWalletAccount) ?? null; + + // --------------------------------------------------------------------------- + // Fetch current delegation from DB + // --------------------------------------------------------------------------- + + const load = useCallback(async () => { + setError(null); + try { + const token = await getAccessToken(); + if (!token) return; + const r = await fetch("/api/agent-delegation", { + headers: { Authorization: `Bearer ${token}` }, + }); + if (r.ok) { + const j = (await r.json()) as GetDelegationResponse; + setDelegation(j.delegation); + } else { + const body = (await r.json().catch(() => ({}))) as { error?: string }; + setError(body.error ?? `HTTP ${r.status}`); + } + } catch (e) { + setError(e instanceof Error ? e.message : "network_error"); + } + }, [getAccessToken]); + + useEffect(() => { + if (!user) return; + setLoadingState("fetching"); + void load().finally(() => setLoadingState("idle")); + }, [user, load]); + + // --------------------------------------------------------------------------- + // Authorize + // --------------------------------------------------------------------------- + + async function onAuthorize() { + if (!solanaWallet) return; + setError(null); + setLoadingState("mutating"); + try { + await delegateWallet({ + address: solanaWallet.address, + chainType: "solana", + }); + const token = await getAccessToken(); + if (!token) throw new Error("No access token"); + const r = await fetch("/api/agent-delegation", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + action: "delegate", + wallet_pubkey: solanaWallet.address, + chain_type: "solana", + }), + }); + if (!r.ok) { + const body = (await r.json().catch(() => ({}))) as { error?: string }; + throw new Error(body.error ?? `HTTP ${r.status}`); + } + await load(); + } catch (e) { + setError(e instanceof Error ? e.message : "unknown_error"); + } finally { + setLoadingState("idle"); + } + } + + // --------------------------------------------------------------------------- + // Revoke + // --------------------------------------------------------------------------- + + async function onRevoke() { + setError(null); + setLoadingState("mutating"); + try { + await revokeWallets(); + const token = await getAccessToken(); + if (!token) throw new Error("No access token"); + const r = await fetch("/api/agent-delegation", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ action: "revoke" }), + }); + if (!r.ok) { + const body = (await r.json().catch(() => ({}))) as { error?: string }; + throw new Error(body.error ?? `HTTP ${r.status}`); + } + await load(); + } catch (e) { + setError(e instanceof Error ? e.message : "unknown_error"); + } finally { + setLoadingState("idle"); + } + } + + // --------------------------------------------------------------------------- + // Derived state + // --------------------------------------------------------------------------- + + const isActive = delegation !== null && delegation.revoked_at === null; + const isBusy = loadingState !== "idle"; + const pubkey = isActive + ? delegation!.wallet_pubkey + : solanaWallet?.address ?? ""; + + // --------------------------------------------------------------------------- + // Render — loading skeleton + // --------------------------------------------------------------------------- + + if (loadingState === "fetching") { + return ( +
+
+ + Cargando estado de delegación… +
+
+ ); + } + + // --------------------------------------------------------------------------- + // Render — authorized state + // --------------------------------------------------------------------------- + + if (isActive) { + return ( +
+ {/* Header */} +
+

+ Agent authorization +

+
+ + {/* Status badge */} +
+

+ ✓ Authorized — your agent can submit PRs on your + behalf +

+
    +
  • + Wallet:{" "} + + {shortPubkey(delegation!.wallet_pubkey)} + +
  • +
  • + Delegated since:{" "} + {formatTimestamp(delegation!.delegated_at)} +
  • +
+
+ + {/* Error */} + {error && ( +
+ {error} +
+ )} + + {/* Revoke */} +
+ +
+
+ ); + } + + // --------------------------------------------------------------------------- + // Render — not authorized state + // --------------------------------------------------------------------------- + + return ( +
+ {/* Heading */} +
+

+ Authorize agent to act on-chain +

+

+ Your AI agent needs permission to sign Solana transactions on your + behalf to submit PRs to bounties. Without this, every action would + require you to open a browser and confirm — which defeats the point + of having an agent. +

+
+ + {/* What you're authorizing */} +
+
+

+ What you're authorizing: +

+
    +
  • + GhBounty server can sign{" "} + submit_solution transactions + using your wallet ( + + {pubkey ? shortPubkey(pubkey) : "—"} + + ) +
  • +
  • + This is scoped to the GhBounty escrow program only — we validate + every transaction server-side before signing +
  • +
+
+ +
+

+ What we cannot do: +

+
    +
  • Transfer your SOL or tokens
  • +
  • Withdraw funds from any escrow
  • +
  • + Sign any transaction outside the{" "} + ghbounty_escrow program +
  • +
+
+
+ + {/* Revoke notice */} +

+ Revoke any time: clicking the button below will revoke + all server-side signing permissions. Your agent will stop being able to + submit PRs until you re-authorize. +

+ + {/* Error */} + {error && ( +
+ {error} +
+ )} + + {/* CTA */} +
+ + + State: Not authorized + +
+ + {!solanaWallet && ( +

+ No Solana wallet found. Connect a Solana wallet first. +

+ )} +
+ ); +} From 4e1a95ec6c4ed9efadd87dc3fa4fa94bb038e213 Mon Sep 17 00:00:00 2001 From: gastonfoncea Date: Mon, 18 May 2026 22:10:18 -0300 Subject: [PATCH 17/21] =?UTF-8?q?feat(frontend):=20render=20AgentDelegatio?= =?UTF-8?q?nCard=20in=20/app/credentials=20=E2=80=94=20GHB-187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- frontend/app/app/credentials/CredentialsClient.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/app/app/credentials/CredentialsClient.tsx b/frontend/app/app/credentials/CredentialsClient.tsx index a78a1b5..84ffbf4 100644 --- a/frontend/app/app/credentials/CredentialsClient.tsx +++ b/frontend/app/app/credentials/CredentialsClient.tsx @@ -19,6 +19,7 @@ import { useEffect, useState } from "react"; import { useAuth } from "@/lib/auth"; import { createClient } from "@/utils/supabase/client"; +import { AgentDelegationCard } from "./AgentDelegationCard"; import { ApiKeysSection } from "./ApiKeysSection"; import { ConnectedAppsSection } from "./ConnectedAppsSection"; @@ -94,6 +95,8 @@ export function CredentialsClient() { + + ); } From bacb4c3ddb137aff3249bdaccfdc80155315383e Mon Sep 17 00:00:00 2001 From: gastonfoncea Date: Mon, 18 May 2026 22:26:14 -0300 Subject: [PATCH 18/21] =?UTF-8?q?feat(relayer):=20pre-scoring=20PR=20owner?= =?UTF-8?q?ship=20check=20=E2=80=94=20GHB-187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds defense-in-depth ownership verification to the relayer's submission handler (GHB-182). Before calling the analyzer, the handler now calls verifyPrOwnership (from @ghbounty/shared) with the solver's GitHub handle and the bounty's repo URL looked up from DB. Definitive failures (author_mismatch, repo_mismatch, pr_not_found, invalid_url) → auto_rejected; transient failures (rate_limited, upstream_error) → skip poll cycle, retry. Also fixes @ghbounty/shared's internal imports to use .js extensions so it works for both bundler and NodeNext module resolution consumers. Co-Authored-By: Claude Sonnet 4.6 --- packages/shared/src/gas-station/index.ts | 16 +- packages/shared/src/gas-station/solana.ts | 6 +- packages/shared/src/gas-station/types.ts | 2 +- packages/shared/src/index.ts | 12 +- pnpm-lock.yaml | 79 +++++-- relayer/package.json | 1 + relayer/src/db/ops.ts | 42 ++++ relayer/src/index.ts | 4 + relayer/src/submission-handler.ts | 148 ++++++++++++ relayer/tests/submission-handler.test.ts | 267 ++++++++++++++++++++++ 10 files changed, 540 insertions(+), 37 deletions(-) diff --git a/packages/shared/src/gas-station/index.ts b/packages/shared/src/gas-station/index.ts index 26aa557..0be7bb2 100644 --- a/packages/shared/src/gas-station/index.ts +++ b/packages/shared/src/gas-station/index.ts @@ -16,8 +16,8 @@ * Adding EVM (GHB-178) follows the same pattern. */ -import type { ChainId } from "../chains"; -import { GasStation, GasStationError } from "./types"; +import type { ChainId } from "../chains.js"; +import { GasStation, GasStationError } from "./types.js"; export function getGasStation(chainId: ChainId): GasStation { switch (chainId) { @@ -49,9 +49,9 @@ export type { SponsorPayload, SolanaSponsorPayload, GasStationErrorCode, -} from "./types"; +} from "./types.js"; -export { GasStationError } from "./types"; +export { GasStationError } from "./types.js"; // GHB-174 — Solana implementation. Exposed at the barrel level so // the route handler in GHB-175 can `new SolanaGasStation({...})` @@ -62,12 +62,12 @@ export { loadGasStationKeypair, loadTreasuryKeypair, makeConnectionRpcSubmitter, -} from "./solana"; +} from "./solana.js"; export type { SolanaGasStationDeps, SolanaRpcSubmitter, SponsorLogEntry, -} from "./solana"; +} from "./solana.js"; // GHB-173 — validator (used by SolanaGasStation but also exported // for tests / ops scripts that want to dry-run the rules). @@ -78,9 +78,9 @@ export { MAX_FEE_LAMPORTS, MAX_TOPUP_LAMPORTS, MAX_REVIEW_FEE_LAMPORTS, -} from "./solana-validator"; +} from "./solana-validator.js"; export type { ValidateOptions, ValidatorResult, ValidatorRejectionCode, -} from "./solana-validator"; +} from "./solana-validator.js"; diff --git a/packages/shared/src/gas-station/solana.ts b/packages/shared/src/gas-station/solana.ts index f6b0226..08286ff 100644 --- a/packages/shared/src/gas-station/solana.ts +++ b/packages/shared/src/gas-station/solana.ts @@ -36,14 +36,14 @@ import { type Commitment, } from "@solana/web3.js"; -import type { ChainId } from "../chains"; +import type { ChainId } from "../chains.js"; import { GasStation, GasStationError, SponsorRequest, SponsorResult, -} from "./types"; -import { validateSolanaSponsorTx } from "./solana-validator"; +} from "./types.js"; +import { validateSolanaSponsorTx } from "./solana-validator.js"; /** * Minimal RPC surface SolanaGasStation needs. A real `Connection` diff --git a/packages/shared/src/gas-station/types.ts b/packages/shared/src/gas-station/types.ts index 87b9a2f..a873410 100644 --- a/packages/shared/src/gas-station/types.ts +++ b/packages/shared/src/gas-station/types.ts @@ -18,7 +18,7 @@ * - GHB-178 (deferred) — EvmGasStation via ERC-4337 paymaster */ -import type { ChainId } from "../chains"; +import type { ChainId } from "../chains.js"; /** * Solana sponsor payload. The user partial-signs a VersionedTransaction diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index aac58f4..0035694 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,9 +1,9 @@ -export * from "./chains"; -export * from "./gas-station/index"; -export * from "./api-key"; -export * from "./oauth-token"; -export { verifyPrOwnership } from "./github/verify-pr-ownership"; +export * from "./chains.js"; +export * from "./gas-station/index.js"; +export * from "./api-key.js"; +export * from "./oauth-token.js"; +export { verifyPrOwnership } from "./github/verify-pr-ownership.js"; export type { VerifyPrOwnershipInput, VerifyPrOwnershipResult, -} from "./github/verify-pr-ownership"; +} from "./github/verify-pr-ownership.js"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 558f40c..96d2241 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -248,7 +248,7 @@ importers: dependencies: '@solana/web3.js': specifier: ^1.95.0 - version: 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + version: 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) bcryptjs: specifier: ^3.0.3 version: 3.0.3 @@ -267,7 +267,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.1.9 - version: 2.1.9(@types/node@20.19.39)(happy-dom@20.9.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(lightningcss@1.32.0) + version: 2.1.9(@types/node@20.19.39)(happy-dom@20.9.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(lightningcss@1.32.0) relayer: dependencies: @@ -276,16 +276,19 @@ importers: version: 0.91.1(zod@4.3.6) '@coral-xyz/anchor': specifier: ^0.30.1 - version: 0.30.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + version: 0.30.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) '@ghbounty/db': specifier: workspace:^ version: link:../packages/db '@ghbounty/diff-filter': specifier: workspace:^ version: link:../packages/diff-filter + '@ghbounty/shared': + specifier: workspace:^ + version: link:../packages/shared '@solana/web3.js': specifier: ^1.95.0 - version: 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + version: 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) bn.js: specifier: ^5.2.1 version: 5.2.3 @@ -297,7 +300,7 @@ importers: version: 0.45.2(@prisma/client@5.22.0(prisma@5.22.0))(@upstash/redis@1.38.0)(postgres@3.4.9)(prisma@5.22.0) genlayer-js: specifier: ^1.1.7 - version: 1.1.7(bufferutil@4.1.0)(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + version: 1.1.7(bufferutil@4.1.0)(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.3.6) tsx: specifier: ^4.19.0 version: 4.21.0 @@ -313,7 +316,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.1.0 - version: 2.1.9(@types/node@22.19.17)(happy-dom@20.9.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(lightningcss@1.32.0) + version: 2.1.9(@types/node@22.19.17)(happy-dom@20.9.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(lightningcss@1.32.0) packages: @@ -8017,12 +8020,41 @@ snapshots: - typescript - utf-8-validate + '@coral-xyz/anchor@0.30.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': + dependencies: + '@coral-xyz/anchor-errors': 0.30.1 + '@coral-xyz/borsh': 0.30.1(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)) + '@noble/hashes': 1.8.0 + '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + bn.js: 5.2.3 + bs58: 4.0.1 + buffer-layout: 1.2.2 + camelcase: 6.3.0 + cross-fetch: 3.2.0 + crypto-hash: 1.3.0 + eventemitter3: 4.0.7 + pako: 2.1.0 + snake-case: 3.0.4 + superstruct: 0.15.5 + toml: 3.0.0 + transitivePeerDependencies: + - bufferutil + - encoding + - typescript + - utf-8-validate + '@coral-xyz/borsh@0.30.1(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))': dependencies: '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) bn.js: 5.2.3 buffer-layout: 1.2.2 + '@coral-xyz/borsh@0.30.1(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))': + dependencies: + '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + bn.js: 5.2.3 + buffer-layout: 1.2.2 + '@drizzle-team/brocli@0.10.2': {} '@ecies/ciphers@0.2.6(@noble/ciphers@1.3.0)': @@ -14439,7 +14471,7 @@ snapshots: '@next/eslint-plugin-next': 16.2.4 eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.7.0)) @@ -14462,7 +14494,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -14477,14 +14509,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.59.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) transitivePeerDependencies: - supports-color @@ -14499,7 +14531,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) hasown: 2.0.3 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -14528,7 +14560,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) hasown: 2.0.3 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -14885,11 +14917,11 @@ snapshots: generic-pool@3.9.0: {} - genlayer-js@1.1.7(bufferutil@4.1.0)(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6): + genlayer-js@1.1.7(bufferutil@4.1.0)(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.3.6): dependencies: eslint-plugin-import: 2.32.0(eslint@9.39.4(jiti@2.7.0)) typescript-parsec: 0.3.4 - viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) + viem: 2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.3.6) transitivePeerDependencies: - '@typescript-eslint/parser' - bufferutil @@ -15232,6 +15264,10 @@ snapshots: dependencies: ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) + isows@1.0.7(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)): + dependencies: + ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) + iterator.prototype@1.1.5: dependencies: define-data-property: 1.1.4 @@ -17016,16 +17052,16 @@ snapshots: - utf-8-validate - zod - viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6): + viem@2.47.12(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.3.6): dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 abitype: 1.2.3(typescript@5.9.3)(zod@4.3.6) - isows: 1.0.7(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + isows: 1.0.7(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)) ox: 0.14.13(typescript@5.9.3)(zod@4.3.6) - ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) + ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -17125,7 +17161,7 @@ snapshots: - supports-color - terser - vitest@2.1.9(@types/node@20.19.39)(happy-dom@20.9.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(lightningcss@1.32.0): + vitest@2.1.9(@types/node@20.19.39)(happy-dom@20.9.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(lightningcss@1.32.0): dependencies: '@vitest/expect': 2.1.9 '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@20.19.39)(lightningcss@1.32.0)) @@ -17149,7 +17185,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.19.39 - happy-dom: 20.9.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + happy-dom: 20.9.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) transitivePeerDependencies: - less - lightningcss @@ -17373,6 +17409,11 @@ snapshots: bufferutil: 4.1.0 utf-8-validate: 5.0.10 + ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6): + optionalDependencies: + bufferutil: 4.1.0 + utf-8-validate: 6.0.6 + ws@8.20.0(bufferutil@4.1.0)(utf-8-validate@5.0.10): optionalDependencies: bufferutil: 4.1.0 diff --git a/relayer/package.json b/relayer/package.json index 9e54e3f..636644d 100644 --- a/relayer/package.json +++ b/relayer/package.json @@ -16,6 +16,7 @@ "@coral-xyz/anchor": "^0.30.1", "@ghbounty/db": "workspace:^", "@ghbounty/diff-filter": "workspace:^", + "@ghbounty/shared": "workspace:^", "@solana/web3.js": "^1.95.0", "bn.js": "^5.2.1", "dotenv": "^17.4.2", diff --git a/relayer/src/db/ops.ts b/relayer/src/db/ops.ts index 1ef7108..e4a89d7 100644 --- a/relayer/src/db/ops.ts +++ b/relayer/src/db/ops.ts @@ -4,6 +4,7 @@ import { chainRegistry, evaluations, issues, + profiles, submissions, type Db, } from "@ghbounty/db"; @@ -648,6 +649,47 @@ export async function getSubmissionIdByPda( return rows[0]?.id ?? null; } +/* ---------------------------------------------------------------- */ +/* GHB-182: PR ownership defense-in-depth */ +/* ---------------------------------------------------------------- */ + +/** + * Return the GitHub handle of the solver (via profiles.wallet_pubkey) + * and the repo URL parsed from the bounty's github_issue_url. + * + * JOIN shape: + * issues.pda = issuePda → issues.github_issue_url + * profiles.wallet_pubkey = solverWallet → profiles.github_handle + * + * Returns null when either side is missing (legacy row without a + * linked GitHub account, or the issue has no off-chain mirror yet). + * The handler must treat null as "skip ownership check" — a missing + * github_handle is not a reason to auto_reject. + */ +export async function getSolverOwnershipContext( + db: Db, + issuePda: string, + solverWallet: string, +): Promise<{ githubHandle: string; githubIssueUrl: string } | null> { + const rows = await db.execute(sql` + SELECT + p.github_handle AS github_handle, + i.github_issue_url AS github_issue_url + FROM profiles p + CROSS JOIN issues i + WHERE p.wallet_pubkey = ${solverWallet} + AND i.pda = ${issuePda} + LIMIT 1 + `); + type Row = { github_handle: string | null; github_issue_url: string | null }; + const first = rowsOf(rows)[0]; + if (!first || !first.github_handle || !first.github_issue_url) return null; + return { + githubHandle: first.github_handle, + githubIssueUrl: first.github_issue_url, + }; +} + /** * GHB-85 mirror: the off-chain `submission_reviews` row that the * frontend reads to render "Auto-rejected". The relayer is the only diff --git a/relayer/src/index.ts b/relayer/src/index.ts index 9bbb249..ab51d7e 100644 --- a/relayer/src/index.ts +++ b/relayer/src/index.ts @@ -106,6 +106,10 @@ async function runOnce(): Promise { // skips the spawn when token or app are null and falls back to // the "no test results available" prompt path. sandbox: cfg.sandbox, + // GHB-182: GitHub token for PR ownership check. Already loaded + // above for logging; pass through here so verifyPrOwnership can + // use it for higher rate limits. + githubToken: ghToken || null, }).then(() => undefined); await processBacklog(connection, client.getProgram(), handler); diff --git a/relayer/src/submission-handler.ts b/relayer/src/submission-handler.ts index e534e81..f9a96b8 100644 --- a/relayer/src/submission-handler.ts +++ b/relayer/src/submission-handler.ts @@ -1,4 +1,5 @@ import { type Db } from "@ghbounty/db"; +import { verifyPrOwnership, type VerifyPrOwnershipResult } from "@ghbounty/shared"; import { analyzeSubmission, type AnalyzeResult } from "./analyzer.js"; import { type GenLayerConfig, type SandboxConfig } from "./config.js"; @@ -8,6 +9,7 @@ import { getRejectThreshold, getSubmissionIdByPda, getSubmittedByUserId, + getSolverOwnershipContext, insertEvaluation, insertNotification, isBountyOpenForSubmissions, @@ -69,6 +71,17 @@ export interface SubmissionHandlerDeps { * machines. Must match the shape of the real `runSandboxedTests`. */ runSandbox?: typeof runSandboxedTests; + /** + * For tests: inject the PR ownership verifier so we don't hit GitHub. + * Must match the shape of `verifyPrOwnership` from `@ghbounty/shared`. + */ + verifyOwnership?: typeof verifyPrOwnership; + /** + * GitHub PAT for the relayer's ownership check calls. Set via + * GITHUB_TOKEN env var. Optional — public repos work without it but + * at a lower rate limit. + */ + githubToken?: string | null; } export interface HandleSubmissionResult { @@ -147,6 +160,35 @@ export async function handleSubmission( } } + // GHB-182: defense-in-depth — re-verify PR ownership before spending an + // Opus call. The MCP server already ran this check at submit time, but the + // relayer is the financial gate (it writes the on-chain score) so a second + // check here prevents a compromised or bypassed MCP path from causing a + // false payout. + if (deps.db) { + const ownershipResult = await checkPrOwnership(sub, deps); + if (ownershipResult === "transient_failure") { + // GitHub is flaky — skip this poll cycle, the watcher will retry. + return { + score: 0, + outcome: "pass", + threshold, + source: "stub", + txHash: "ownership_check_skipped", + }; + } + if (ownershipResult === "rejected") { + return { + score: 0, + outcome: "auto_rejected", + threshold, + source: "stub", + txHash: "ownership_check_failed", + }; + } + // ownershipResult === "ok" or "skipped" → proceed to scoring + } + // GHB-73: spin up the sandbox + run the PR's tests BEFORE asking Sonnet // to score, so Sonnet's prompt includes a real pass/fail signal. The // call is best-effort — any failure (disabled, infra, timeout, @@ -620,6 +662,112 @@ function combineOutputTails(stdout: string, stderr: string): string | null { return `--- stdout (tail) ---\n${out}\n--- stderr (tail) ---\n${err}`; } +// ── GHB-182: PR ownership defense-in-depth ──────────────────────── + +/** + * Transient failures (GitHub rate-limited or unreachable) → caller skips + * this poll cycle and the watcher retries on the next one. + * Definitive failures (mismatch, not found, invalid URL) → auto_rejected. + * "ok" / "skipped" (no ownership context in DB) → proceed to scoring. + */ +type OwnershipCheckOutcome = "ok" | "skipped" | "rejected" | "transient_failure"; + +const TRANSIENT_REASONS = new Set(["rate_limited", "upstream_error"]); + +/** + * Run the ownership check against GitHub. Returns "skipped" when the DB + * doesn't have enough context (no github_handle for the solver, or no + * off-chain issue row yet) — that is not a reason to auto_reject. + */ +async function checkPrOwnership( + sub: DecodedSubmission, + deps: SubmissionHandlerDeps, +): Promise { + if (!deps.db) return "skipped"; + + let ownershipCtx: { githubHandle: string; githubIssueUrl: string } | null; + try { + ownershipCtx = await getSolverOwnershipContext( + deps.db, + sub.bounty.toBase58(), + sub.solver.toBase58(), + ); + } catch (err) { + // DB error is transient — don't permanently reject. + log.warn("ownership_check: db lookup failed", { + submission: sub.pda.toBase58(), + err: String(err), + }); + return "transient_failure"; + } + + if (!ownershipCtx) { + // No github_handle or no off-chain issue row: can't check → skip, not reject. + log.debug("ownership_check: skipped (no context in db)", { + submission: sub.pda.toBase58(), + solver: sub.solver.toBase58(), + }); + return "skipped"; + } + + const bountyRepoUrl = parseIssueRepoUrl(ownershipCtx.githubIssueUrl); + if (!bountyRepoUrl) { + log.warn("ownership_check: could not parse repo URL from issue", { + submission: sub.pda.toBase58(), + githubIssueUrl: ownershipCtx.githubIssueUrl, + }); + // Treat unparseable bounty URL as a data error — not the solver's fault. + return "skipped"; + } + + const verify = deps.verifyOwnership ?? verifyPrOwnership; + let result: VerifyPrOwnershipResult; + try { + result = await verify({ + prUrl: sub.prUrl, + expectedGithubHandle: ownershipCtx.githubHandle, + expectedRepoUrl: bountyRepoUrl, + token: deps.githubToken ?? undefined, + }); + } catch (err) { + // verifyPrOwnership is documented as non-throwing but be defensive. + log.warn("ownership_check: verifier threw", { + submission: sub.pda.toBase58(), + err: String(err), + }); + return "transient_failure"; + } + + if (result.ok) return "ok"; + + if (TRANSIENT_REASONS.has(result.reason)) { + log.warn("ownership_check: transient failure — will retry next poll", { + submission: sub.pda.toBase58(), + reason: result.reason, + }); + return "transient_failure"; + } + + // Definitive failure — mark auto_rejected and skip scoring. + log.warn("ownership_check: rejected", { + submission: sub.pda.toBase58(), + reason: result.reason, + prUrl: sub.prUrl, + }); + await markAutoRejected(deps.db, sub.pda.toBase58()); + return "rejected"; +} + +/** + * Extract `https://github.com/owner/repo` from a GitHub issue URL. + * Returns null if the URL doesn't match the expected shape. + * Mirrors the same helper in `apps/mcp/lib/tools/submissions/create.ts`. + */ +function parseIssueRepoUrl(githubIssueUrl: string): string | null { + const m = githubIssueUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\//); + return m ? `https://github.com/${m[1]}/${m[2]}` : null; +} + /** * Parse a GitHub PR URL into the parts the SandboxSpec needs. Returns * null on shapes we don't recognize so the caller can skip the sandbox diff --git a/relayer/tests/submission-handler.test.ts b/relayer/tests/submission-handler.test.ts index de54d2a..5c46e62 100644 --- a/relayer/tests/submission-handler.test.ts +++ b/relayer/tests/submission-handler.test.ts @@ -1197,3 +1197,270 @@ describe("GHB-184: cap de submissions", () => { expect(executes.some((e) => /WITH bumped AS/i.test(String(e.payload)))).toBe(false); }); }); + +/* ────────────────────────────────────────────────────────────────────── + * GHB-182: relayer-side PR ownership check (defense-in-depth). + * + * The check fires after the bounty-open pre-check but before the + * sandbox + Opus scoring. We verify that: + * - Definitive failures → auto_rejected, Opus NOT called. + * - Transient failures → early return (skip this poll cycle), not rejected. + * - Missing DB context (no github_handle) → skipped, Opus IS called. + * - Ownership passes → Opus IS called, normal flow. + * ────────────────────────────────────────────────────────────────────── */ +describe("GHB-182: PR ownership check", () => { + /** + * The ownership lookup fires via db.execute(sql`SELECT p.github_handle ...`). + * We route it by matching the SQL text. The route must come BEFORE the + * defaults in fakeDb so it takes precedence. + */ + function ownershipRoute( + rows: Array<{ github_handle: string | null; github_issue_url: string | null }>, + ) { + return { + match: /p\.github_handle/i, + rows, + }; + } + + /** A verifyOwnership mock that returns a definitive failure. */ + function rejectOwnership(reason: "author_mismatch" | "repo_mismatch" | "pr_not_found" | "invalid_url") { + return vi.fn(async () => ({ ok: false as const, reason })); + } + + /** A verifyOwnership mock that returns a transient failure. */ + function transientOwnership(reason: "rate_limited" | "upstream_error") { + return vi.fn(async () => ({ ok: false as const, reason })); + } + + /** A verifyOwnership mock that passes. */ + function passOwnership() { + return vi.fn(async () => ({ ok: true as const })); + } + + /** DB state with the ownership context route pre-configured. */ + function dbWithOwnership( + githubHandle: string | null = "octocat", + issueUrl: string | null = "https://github.com/owner/repo/issues/1", + ) { + return fakeDb({ + executeRoutes: [ownershipRoute([{ github_handle: githubHandle, github_issue_url: issueUrl }])], + }); + } + + /** DB state where the ownership context is missing (no row). */ + function dbWithoutOwnership() { + return fakeDb({ + executeRoutes: [ownershipRoute([])], + }); + } + + test("author_mismatch → auto_rejected, Opus NOT called", async () => { + const state = dbWithOwnership(); + const db = buildDrizzleProxy(state); + const { client, setScore } = buildScorer(); + const analyze = vi.fn(async () => opusResult); + const verifyOwnership = rejectOwnership("author_mismatch"); + + const r = await handleSubmission(buildSub(), { + ...baseDeps, + db: db as never, + scorer: client, + analyze, + verifyOwnership, + }); + + expect(analyze).not.toHaveBeenCalled(); + expect(setScore).not.toHaveBeenCalled(); + expect(r.outcome).toBe("auto_rejected"); + expect(r.txHash).toBe("ownership_check_failed"); + + // markAutoRejected was called (drizzle update with state=auto_rejected). + const updates = state.calls.filter((c) => c.kind === "update"); + expect( + updates.some((u) => { + const patch = (u.payload as { patch: { state?: string } }).patch; + return patch?.state === "auto_rejected"; + }), + ).toBe(true); + }); + + test("repo_mismatch → auto_rejected, Opus NOT called", async () => { + const state = dbWithOwnership(); + const db = buildDrizzleProxy(state); + const { client, setScore } = buildScorer(); + const analyze = vi.fn(async () => opusResult); + const verifyOwnership = rejectOwnership("repo_mismatch"); + + const r = await handleSubmission(buildSub(), { + ...baseDeps, + db: db as never, + scorer: client, + analyze, + verifyOwnership, + }); + + expect(analyze).not.toHaveBeenCalled(); + expect(setScore).not.toHaveBeenCalled(); + expect(r.outcome).toBe("auto_rejected"); + }); + + test("pr_not_found → auto_rejected, Opus NOT called", async () => { + const state = dbWithOwnership(); + const db = buildDrizzleProxy(state); + const { client, setScore } = buildScorer(); + const analyze = vi.fn(async () => opusResult); + const verifyOwnership = rejectOwnership("pr_not_found"); + + const r = await handleSubmission(buildSub(), { + ...baseDeps, + db: db as never, + scorer: client, + analyze, + verifyOwnership, + }); + + expect(analyze).not.toHaveBeenCalled(); + expect(setScore).not.toHaveBeenCalled(); + expect(r.outcome).toBe("auto_rejected"); + }); + + test("rate_limited → skip this cycle (early return, NOT auto_rejected)", async () => { + const state = dbWithOwnership(); + const db = buildDrizzleProxy(state); + const { client, setScore } = buildScorer(); + const analyze = vi.fn(async () => opusResult); + const verifyOwnership = transientOwnership("rate_limited"); + + const r = await handleSubmission(buildSub(), { + ...baseDeps, + db: db as never, + scorer: client, + analyze, + verifyOwnership, + }); + + expect(analyze).not.toHaveBeenCalled(); + expect(setScore).not.toHaveBeenCalled(); + expect(r.txHash).toBe("ownership_check_skipped"); + + // Must NOT have marked the submission auto_rejected. + const updates = state.calls.filter((c) => c.kind === "update"); + expect( + updates.some((u) => { + const patch = (u.payload as { patch: { state?: string } }).patch; + return patch?.state === "auto_rejected"; + }), + ).toBe(false); + }); + + test("upstream_error → skip this cycle (early return, NOT auto_rejected)", async () => { + const state = dbWithOwnership(); + const db = buildDrizzleProxy(state); + const { client, setScore } = buildScorer(); + const analyze = vi.fn(async () => opusResult); + const verifyOwnership = transientOwnership("upstream_error"); + + const r = await handleSubmission(buildSub(), { + ...baseDeps, + db: db as never, + scorer: client, + analyze, + verifyOwnership, + }); + + expect(analyze).not.toHaveBeenCalled(); + expect(setScore).not.toHaveBeenCalled(); + expect(r.txHash).toBe("ownership_check_skipped"); + }); + + test("no github_handle in DB → check skipped, Opus IS called", async () => { + const state = dbWithoutOwnership(); + const db = buildDrizzleProxy(state); + const { client } = buildScorer(); + const analyze = vi.fn(async () => opusResult); + const verifyOwnership = vi.fn(); + + const r = await handleSubmission(buildSub(), { + ...baseDeps, + db: db as never, + scorer: client, + analyze, + verifyOwnership, + }); + + // verifyOwnership should NOT have been called — no context to check against. + expect(verifyOwnership).not.toHaveBeenCalled(); + expect(analyze).toHaveBeenCalledOnce(); + expect(r.outcome).toBe("pass"); + }); + + test("ownership passes → normal scoring flow, Opus IS called", async () => { + const state = dbWithOwnership(); + const db = buildDrizzleProxy(state); + const { client, setScore } = buildScorer(); + const analyze = vi.fn(async () => opusResult); + const verifyOwnership = passOwnership(); + + const r = await handleSubmission(buildSub(), { + ...baseDeps, + db: db as never, + scorer: client, + analyze, + verifyOwnership, + }); + + expect(verifyOwnership).toHaveBeenCalledOnce(); + expect(analyze).toHaveBeenCalledOnce(); + expect(setScore).toHaveBeenCalledOnce(); + expect(r.outcome).toBe("pass"); + expect(r.score).toBe(7); + }); + + test("no DB → ownership check skipped entirely, Opus IS called", async () => { + const { client } = buildScorer(); + const analyze = vi.fn(async () => opusResult); + const verifyOwnership = vi.fn(); + + const r = await handleSubmission(buildSub(), { + ...baseDeps, + db: null, + scorer: client, + analyze, + verifyOwnership, + }); + + expect(verifyOwnership).not.toHaveBeenCalled(); + expect(analyze).toHaveBeenCalledOnce(); + expect(r.outcome).toBe("pass"); + }); + + test("verifyOwnership receives correct handle and parsed repo URL", async () => { + const issueUrl = "https://github.com/myorg/myrepo/issues/42"; + const state = fakeDb({ + executeRoutes: [ + ownershipRoute([{ github_handle: "jsmith", github_issue_url: issueUrl }]), + ], + }); + const db = buildDrizzleProxy(state); + const { client } = buildScorer(); + const analyze = vi.fn(async () => opusResult); + const verifyOwnership = passOwnership(); + + await handleSubmission(buildSub("https://github.com/myorg/myrepo/pull/10"), { + ...baseDeps, + db: db as never, + scorer: client, + analyze, + verifyOwnership, + githubToken: "ghp_test_token", + }); + + expect(verifyOwnership).toHaveBeenCalledOnce(); + const args = verifyOwnership.mock.calls[0]![0]; + expect(args.expectedGithubHandle).toBe("jsmith"); + expect(args.expectedRepoUrl).toBe("https://github.com/myorg/myrepo"); + expect(args.prUrl).toBe("https://github.com/myorg/myrepo/pull/10"); + expect(args.token).toBe("ghp_test_token"); + }); +}); From fea5786eaf12da8e0a9ccce717c0d92bfc0dfb7b Mon Sep 17 00:00:00 2001 From: gastonfoncea Date: Mon, 18 May 2026 22:30:53 -0300 Subject: [PATCH 19/21] =?UTF-8?q?docs(runbooks):=20add=20Sprint=20B=20smok?= =?UTF-8?q?e=20test=20runbook=20=E2=80=94=20GHB-187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../runbooks/2026-05-18-sprint-b-smoke.md | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 docs/superpowers/runbooks/2026-05-18-sprint-b-smoke.md diff --git a/docs/superpowers/runbooks/2026-05-18-sprint-b-smoke.md b/docs/superpowers/runbooks/2026-05-18-sprint-b-smoke.md new file mode 100644 index 0000000..7f6e133 --- /dev/null +++ b/docs/superpowers/runbooks/2026-05-18-sprint-b-smoke.md @@ -0,0 +1,116 @@ +# Sprint B smoke test runbook (2026-05-18) + +End-to-end manual verification of `submissions.create` after deploy. Run by a human (Gaston) post-merge, post-deploy. + +## Pre-flight + +- [ ] Migration 0026 applied to devnet Supabase (`pnpm db:migrate` — see Task 16 of the plan). +- [ ] MCP deploy includes commits up to (and including) the `submissions.create` task. +- [ ] Relayer deploy includes the ownership pre-check. +- [ ] Frontend deploy includes the `AgentDelegationCard`. +- [ ] `PRIVY_APP_ID` / `PRIVY_APP_SECRET` set on the MCP env (Vercel project). +- [ ] `GITHUB_TOKEN` set on both MCP and relayer envs (recommended for higher GitHub rate limits). +- [ ] Gaston has an active dev profile in devnet with a linked GitHub handle. + +## Steps + +### 1. Delegate the wallet + +Log into `/app/credentials` as Gaston (dev role). Click "Authorize" in the new card. Confirm in Privy. Verify in Supabase Studio that `agent_delegations` has a row with `revoked_at IS NULL` for your `user_id`. + +### 2. Verify `submissions.list` (sanity check) + +Hit the MCP from Claude Code (or curl): + +```bash +curl -sS -X POST https://mcp.ghbounty.com/api/mcp/mcp \ + -H "Authorization: Bearer $GHB_API_KEY" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json, text/event-stream" \ + -d '{"jsonrpc":"2.0","id":1,"method":"tools/call", + "params":{"name":"submissions.list","arguments":{}}}' +``` + +Expected: `{ items: [...] }` (possibly empty pre-test). + +### 3. Open a real PR + +Against the bounty's target repo, using Gaston's GitHub account. + +### 4. Call `submissions.create` + +```bash +curl -sS -X POST https://mcp.ghbounty.com/api/mcp/mcp \ + -H "Authorization: Bearer $GHB_API_KEY" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json, text/event-stream" \ + -d '{"jsonrpc":"2.0","id":1,"method":"tools/call", + "params":{"name":"submissions.create", + "arguments":{ + "bounty_id":"", + "pr_url":"https://github.com///pull/"}}}' +``` + +Expected: `{ submission_id, status: "pending", tx_signature, submission_pda }`. + +### 5. Verify on-chain + +Check Solana Explorer (devnet) for the tx signature. Expect a `submit_solution` invocation in the program. + +### 6. Wait for relayer + +~30-60s. Poll `submissions.get`: + +```bash +curl -sS -X POST https://mcp.ghbounty.com/api/mcp/mcp \ + -H "Authorization: Bearer $GHB_API_KEY" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json, text/event-stream" \ + -d '{"jsonrpc":"2.0","id":1,"method":"tools/call", + "params":{"name":"submissions.get", + "arguments":{"submission_id":""}}}' +``` + +Expected: `state: "scored"` with a numeric `score`. + +### 7. Negative test — wrong PR author + +Submit a PR URL owned by someone else against another bounty. Expect: +```json +{"error": {"code": "Forbidden", "message": "PR ownership check failed: author_mismatch"}} +``` + +### 8. Negative test — revoke and try again + +Revoke the delegation from `/app/credentials`. Call `submissions.create` again. Expect: +```json +{"error": {"code": "Forbidden", "message": "Wallet delegation required — visit /app/credentials to authorize."}} +``` + +### 9. Negative test — role gating + +If you have a company-role API key handy, call `submissions.create` with it. Expect: +```json +{"error": {"code": "Forbidden", "message": "This tool requires `dev` role."}} +``` + +### 10. Idempotency check + +Call `submissions.create` again with the SAME `bounty_id + pr_url` as step 4. Expect: +```json +{"submission_id": "", "status": "", "idempotent": true} +``` + +## On failure + +If any step errors: +- Inspect MCP logs (Vercel dashboard) — they include structured tags for `submissions.create`. +- Inspect relayer logs (process supervisor) — look for `ownership_check_failed`. +- Inspect Supabase `submissions` row state for the relevant PDA. +- Inspect Supabase `agent_delegations` row for your user_id (should exist, `revoked_at IS NULL` for an active delegation). + +## Cleanup + +After validating, decide: +- If everything worked: smoke test passes. Update memory `project_mcp_state_2026_05_18.md` (or successor) noting Sprint B is live. +- If something broke: don't roll back the migration unless absolutely necessary (Drizzle migrations are forward-only); patch on the feature branch and re-deploy. From 2671b1d31eb9b919d517e48c1c699cb176ca1055 Mon Sep 17 00:00:00 2001 From: gastonfoncea Date: Tue, 19 May 2026 17:26:14 -0300 Subject: [PATCH 20/21] =?UTF-8?q?fix(mcp):=20resolve=20Privy=20walletId=20?= =?UTF-8?q?via=20getWalletByAddress=20=E2=80=94=20GHB-187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Privy's signTransaction expects the internal wallet id, not the on-chain pubkey. Resolve walletAddress → walletId via getWalletByAddress before signing; map 404 to delegation_revoked. Rename SignInput.walletId → walletAddress throughout signer, caller, and tests (5 tests total). Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/mcp/lib/privy/delegated-signer.ts | 25 +++- apps/mcp/lib/tools/submissions/create.ts | 9 +- apps/mcp/tests/privy/delegated-signer.test.ts | 109 ++++++++++++++---- 3 files changed, 113 insertions(+), 30 deletions(-) diff --git a/apps/mcp/lib/privy/delegated-signer.ts b/apps/mcp/lib/privy/delegated-signer.ts index 34ad7fb..3b8e47e 100644 --- a/apps/mcp/lib/privy/delegated-signer.ts +++ b/apps/mcp/lib/privy/delegated-signer.ts @@ -1,7 +1,7 @@ import type { PrivyClient } from "@privy-io/node"; export type SignInput = { - walletId: string; + walletAddress: string; // The Solana on-chain pubkey (base58). unsignedTx: Uint8Array; }; @@ -14,16 +14,37 @@ export type SignResult = * delegated their wallet to our server. Returns a partially-signed * transaction (only the user's signature slot filled) — the caller * still needs to get a fee-payer signature via the gas station. + * + * Privy's signTransaction API takes the internal wallet `id`, not the + * on-chain pubkey. We resolve the address → id via getWalletByAddress + * before signing. */ export async function signSolanaTransaction( client: PrivyClient, input: SignInput ): Promise { + let walletId: string; + try { + // Look up the Privy internal wallet ID by on-chain address. + // Privy's signTransaction API takes the internal id, not the pubkey. + const wallet = await (client.wallets as any).getWalletByAddress({ + address: input.walletAddress, + }); + walletId = wallet.id; + } catch (err: any) { + if (err?.status === 404) { + // Either the user never delegated, or revoked off-band via Privy dashboard. + // From our perspective the signing capability is gone — treat as revoked. + return { ok: false, reason: "delegation_revoked" }; + } + return { ok: false, reason: "upstream_error" }; + } + try { const response = await client .wallets() .solana() - .signTransaction(input.walletId, { + .signTransaction(walletId, { transaction: input.unsignedTx, }); diff --git a/apps/mcp/lib/tools/submissions/create.ts b/apps/mcp/lib/tools/submissions/create.ts index 837c427..c55ec5a 100644 --- a/apps/mcp/lib/tools/submissions/create.ts +++ b/apps/mcp/lib/tools/submissions/create.ts @@ -182,14 +182,11 @@ export async function handleSubmissionsCreate(raw: unknown) { // --- Privy delegated signing (Task 6) --- // - // ASSUMPTION: `walletId === wallet_pubkey` (Solana base58 address). - // If Privy's internal walletId differs from the on-chain address, the - // consent flow (Task 12 / GHB-187) must persist Privy's returned - // walletId at delegation time and we read it here. Document this in the - // PR so the Task 12 implementor is aware. + // We pass the on-chain pubkey as `walletAddress`. The signer resolves it + // to Privy's internal wallet id via getWalletByAddress before signing. const privyClient = getPrivyServerClient(); const signed = await signSolanaTransaction(privyClient, { - walletId: auth.profile.wallet_pubkey, + walletAddress: auth.profile.wallet_pubkey, unsignedTx: built.unsignedTx, }); diff --git a/apps/mcp/tests/privy/delegated-signer.test.ts b/apps/mcp/tests/privy/delegated-signer.test.ts index 11f009d..0fff99a 100644 --- a/apps/mcp/tests/privy/delegated-signer.test.ts +++ b/apps/mcp/tests/privy/delegated-signer.test.ts @@ -1,57 +1,122 @@ import { describe, it, expect, vi } from "vitest"; import { signSolanaTransaction } from "@/lib/privy/delegated-signer"; -function makeFakeClient(impl: (...args: any[]) => any) { - // Mirrors client.wallets().solana().signTransaction(...) +function makeFakeClient(opts: { + getByAddressImpl?: (...args: any[]) => any; + signImpl?: (...args: any[]) => any; +}) { return { - wallets: () => ({ - solana: () => ({ - signTransaction: vi.fn(impl), + wallets: Object.assign( + // function form for client.wallets().solana() + () => ({ + solana: () => ({ + signTransaction: vi.fn( + opts.signImpl ?? + (async () => { + throw new Error("signImpl not set"); + }) + ), + }), }), - }), + // property form for client.wallets.getWalletByAddress(...) + { + getWalletByAddress: vi.fn( + opts.getByAddressImpl ?? + (async () => { + throw new Error("getByAddressImpl not set"); + }) + ), + } + ), } as any; } describe("signSolanaTransaction", () => { it("returns signed bytes when Privy accepts", async () => { // base64 of [1, 2, 3] is "AQID" - const fakeClient = makeFakeClient(async () => ({ - encoding: "base64", - signed_transaction: "AQID", - })); + const fakeClient = makeFakeClient({ + getByAddressImpl: async ({ address }: { address: string }) => ({ + id: "wallet_xyz", + address, + }), + signImpl: async () => ({ + encoding: "base64", + signed_transaction: "AQID", + }), + }); const result = await signSolanaTransaction(fakeClient, { - walletId: "wallet-xyz", + walletAddress: "Solver111", unsignedTx: new Uint8Array([0]), }); - expect(result).toEqual({ - ok: true, - signedTx: new Uint8Array([1, 2, 3]), + expect(result).toEqual({ ok: true, signedTx: new Uint8Array([1, 2, 3]) }); + }); + + it("returns delegation_revoked when getWalletByAddress returns 404", async () => { + const err = Object.assign(new Error("not found"), { status: 404 }); + const fakeClient = makeFakeClient({ + getByAddressImpl: async () => { + throw err; + }, }); + + const result = await signSolanaTransaction(fakeClient, { + walletAddress: "Solver111", + unsignedTx: new Uint8Array([0]), + }); + + expect(result).toEqual({ ok: false, reason: "delegation_revoked" }); }); - it("returns delegation_revoked on 403 from Privy", async () => { + it("returns delegation_revoked on 403 from signTransaction", async () => { const err = Object.assign(new Error("forbidden"), { status: 403 }); - const fakeClient = makeFakeClient(async () => { - throw err; + const fakeClient = makeFakeClient({ + getByAddressImpl: async ({ address }: { address: string }) => ({ + id: "wallet_xyz", + address, + }), + signImpl: async () => { + throw err; + }, }); const result = await signSolanaTransaction(fakeClient, { - walletId: "wallet-xyz", + walletAddress: "Solver111", unsignedTx: new Uint8Array([0]), }); expect(result).toEqual({ ok: false, reason: "delegation_revoked" }); }); - it("returns upstream_error on other failures", async () => { - const fakeClient = makeFakeClient(async () => { - throw new Error("network"); + it("returns upstream_error on other signTransaction failures", async () => { + const fakeClient = makeFakeClient({ + getByAddressImpl: async ({ address }: { address: string }) => ({ + id: "wallet_xyz", + address, + }), + signImpl: async () => { + throw new Error("network"); + }, + }); + + const result = await signSolanaTransaction(fakeClient, { + walletAddress: "Solver111", + unsignedTx: new Uint8Array([0]), + }); + + expect(result).toEqual({ ok: false, reason: "upstream_error" }); + }); + + it("returns upstream_error when getWalletByAddress fails non-404", async () => { + const fakeClient = makeFakeClient({ + getByAddressImpl: async () => { + throw new Error("network"); + }, }); const result = await signSolanaTransaction(fakeClient, { - walletId: "wallet-xyz", + walletAddress: "Solver111", unsignedTx: new Uint8Array([0]), }); From d8410a3a3f3e223950aae0ba02b320c9da73f405 Mon Sep 17 00:00:00 2001 From: gastonfoncea Date: Tue, 19 May 2026 18:24:45 -0300 Subject: [PATCH 21/21] =?UTF-8?q?fix(shared,relayer):=20keep=20extensionle?= =?UTF-8?q?ss=20imports=20for=20Next/Turbopack=20=E2=80=94=20GHB-187?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 14 added .js extensions to @ghbounty/shared internal imports to make NodeNext resolution happy when the relayer started consuming the package. That undid the GHB-180 fix that made the package consumable by Next.js (Turbopack), breaking both MCP and frontend builds on Vercel. Revert the .js extensions in the shared package and align the relayer's tsconfig to use moduleResolution: bundler (same pattern as packages/shared/tsconfig.json). tsx handles either mode at runtime; the workspace tests + typecheck still pass for all packages. Verified locally: pnpm --filter @ghbounty/mcp build → ok pnpm --filter frontend build → ok pnpm typecheck → ok (7/7 packages) pnpm test → ok (641 tests pass) Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/shared/src/gas-station/index.ts | 16 ++++++++-------- packages/shared/src/gas-station/solana.ts | 6 +++--- packages/shared/src/gas-station/types.ts | 2 +- packages/shared/src/index.ts | 12 ++++++------ relayer/tsconfig.json | 9 ++++++++- 5 files changed, 26 insertions(+), 19 deletions(-) diff --git a/packages/shared/src/gas-station/index.ts b/packages/shared/src/gas-station/index.ts index 0be7bb2..26aa557 100644 --- a/packages/shared/src/gas-station/index.ts +++ b/packages/shared/src/gas-station/index.ts @@ -16,8 +16,8 @@ * Adding EVM (GHB-178) follows the same pattern. */ -import type { ChainId } from "../chains.js"; -import { GasStation, GasStationError } from "./types.js"; +import type { ChainId } from "../chains"; +import { GasStation, GasStationError } from "./types"; export function getGasStation(chainId: ChainId): GasStation { switch (chainId) { @@ -49,9 +49,9 @@ export type { SponsorPayload, SolanaSponsorPayload, GasStationErrorCode, -} from "./types.js"; +} from "./types"; -export { GasStationError } from "./types.js"; +export { GasStationError } from "./types"; // GHB-174 — Solana implementation. Exposed at the barrel level so // the route handler in GHB-175 can `new SolanaGasStation({...})` @@ -62,12 +62,12 @@ export { loadGasStationKeypair, loadTreasuryKeypair, makeConnectionRpcSubmitter, -} from "./solana.js"; +} from "./solana"; export type { SolanaGasStationDeps, SolanaRpcSubmitter, SponsorLogEntry, -} from "./solana.js"; +} from "./solana"; // GHB-173 — validator (used by SolanaGasStation but also exported // for tests / ops scripts that want to dry-run the rules). @@ -78,9 +78,9 @@ export { MAX_FEE_LAMPORTS, MAX_TOPUP_LAMPORTS, MAX_REVIEW_FEE_LAMPORTS, -} from "./solana-validator.js"; +} from "./solana-validator"; export type { ValidateOptions, ValidatorResult, ValidatorRejectionCode, -} from "./solana-validator.js"; +} from "./solana-validator"; diff --git a/packages/shared/src/gas-station/solana.ts b/packages/shared/src/gas-station/solana.ts index 08286ff..f6b0226 100644 --- a/packages/shared/src/gas-station/solana.ts +++ b/packages/shared/src/gas-station/solana.ts @@ -36,14 +36,14 @@ import { type Commitment, } from "@solana/web3.js"; -import type { ChainId } from "../chains.js"; +import type { ChainId } from "../chains"; import { GasStation, GasStationError, SponsorRequest, SponsorResult, -} from "./types.js"; -import { validateSolanaSponsorTx } from "./solana-validator.js"; +} from "./types"; +import { validateSolanaSponsorTx } from "./solana-validator"; /** * Minimal RPC surface SolanaGasStation needs. A real `Connection` diff --git a/packages/shared/src/gas-station/types.ts b/packages/shared/src/gas-station/types.ts index a873410..87b9a2f 100644 --- a/packages/shared/src/gas-station/types.ts +++ b/packages/shared/src/gas-station/types.ts @@ -18,7 +18,7 @@ * - GHB-178 (deferred) — EvmGasStation via ERC-4337 paymaster */ -import type { ChainId } from "../chains.js"; +import type { ChainId } from "../chains"; /** * Solana sponsor payload. The user partial-signs a VersionedTransaction diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 0035694..aac58f4 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,9 +1,9 @@ -export * from "./chains.js"; -export * from "./gas-station/index.js"; -export * from "./api-key.js"; -export * from "./oauth-token.js"; -export { verifyPrOwnership } from "./github/verify-pr-ownership.js"; +export * from "./chains"; +export * from "./gas-station/index"; +export * from "./api-key"; +export * from "./oauth-token"; +export { verifyPrOwnership } from "./github/verify-pr-ownership"; export type { VerifyPrOwnershipInput, VerifyPrOwnershipResult, -} from "./github/verify-pr-ownership.js"; +} from "./github/verify-pr-ownership"; diff --git a/relayer/tsconfig.json b/relayer/tsconfig.json index f677f8d..b26d477 100644 --- a/relayer/tsconfig.json +++ b/relayer/tsconfig.json @@ -2,7 +2,14 @@ "extends": "../tsconfig.base.json", "compilerOptions": { "outDir": "./dist", - "rootDir": "./src" + "rootDir": "./src", + // Align with @ghbounty/shared (GHB-180): the shared package source + // uses extensionless relative imports so Next.js/Turbopack can + // consume it. Since this package now depends on @ghbounty/shared + // (GHB-187), it must read those imports under the same resolution. + // tsx handles both modes the same way at runtime. + "module": "ESNext", + "moduleResolution": "bundler" }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"]