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. 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/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/privy/delegated-signer.ts b/apps/mcp/lib/privy/delegated-signer.ts new file mode 100644 index 0000000..3b8e47e --- /dev/null +++ b/apps/mcp/lib/privy/delegated-signer.ts @@ -0,0 +1,80 @@ +import type { PrivyClient } from "@privy-io/node"; + +export type SignInput = { + walletAddress: string; // The Solana on-chain pubkey (base58). + 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. + * + * 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(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/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/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/lib/tools/register.ts b/apps/mcp/lib/tools/register.ts index 725cff6..6ac4387 100644 --- a/apps/mcp/lib/tools/register.ts +++ b/apps/mcp/lib/tools/register.ts @@ -3,10 +3,14 @@ import { registerWhoami } from "./whoami"; 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); registerBountiesList(server); registerBountiesGet(server); registerSubmissionsGet(server); + registerSubmissionsList(server); + registerSubmissionsCreate(server); } 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/lib/tools/submissions/create.ts b/apps/mcp/lib/tools/submissions/create.ts new file mode 100644 index 0000000..c55ec5a --- /dev/null +++ b/apps/mcp/lib/tools/submissions/create.ts @@ -0,0 +1,308 @@ +/** + * 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) --- + // + // 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, { + walletAddress: 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/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/package.json b/apps/mcp/package.json index 68036e2..9a146ba 100644 --- a/apps/mcp/package.json +++ b/apps/mcp/package.json @@ -11,16 +11,19 @@ "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", - "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/apps/mcp/tests/privy/delegated-signer.test.ts b/apps/mcp/tests/privy/delegated-signer.test.ts new file mode 100644 index 0000000..0fff99a --- /dev/null +++ b/apps/mcp/tests/privy/delegated-signer.test.ts @@ -0,0 +1,125 @@ +import { describe, it, expect, vi } from "vitest"; +import { signSolanaTransaction } from "@/lib/privy/delegated-signer"; + +function makeFakeClient(opts: { + getByAddressImpl?: (...args: any[]) => any; + signImpl?: (...args: any[]) => any; +}) { + return { + 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({ + getByAddressImpl: async ({ address }: { address: string }) => ({ + id: "wallet_xyz", + address, + }), + signImpl: async () => ({ + encoding: "base64", + signed_transaction: "AQID", + }), + }); + + const result = await signSolanaTransaction(fakeClient, { + walletAddress: "Solver111", + unsignedTx: new Uint8Array([0]), + }); + + 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 signTransaction", async () => { + const err = Object.assign(new Error("forbidden"), { status: 403 }); + const fakeClient = makeFakeClient({ + getByAddressImpl: async ({ address }: { address: string }) => ({ + id: "wallet_xyz", + address, + }), + signImpl: async () => { + throw err; + }, + }); + + const result = await signSolanaTransaction(fakeClient, { + walletAddress: "Solver111", + unsignedTx: new Uint8Array([0]), + }); + + expect(result).toEqual({ ok: false, reason: "delegation_revoked" }); + }); + + 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, { + walletAddress: "Solver111", + unsignedTx: new Uint8Array([0]), + }); + + expect(result).toEqual({ ok: false, reason: "upstream_error" }); + }); +}); 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/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); + }); +}); 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.", + }, + }); + }); +}); 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..8efdcee --- /dev/null +++ b/apps/mcp/tests/tools/submissions/create.test.ts @@ -0,0 +1,431 @@ +/** + * 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; + 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( + opts.noDelegation + ? { data: null, error: null } + : { data: { revoked_at: null }, error: null } + ), + }), + }), + update: updateFn, + }; + } + if (table === "issues") { + return { + select: () => ({ + eq: () => ({ + maybeSingle: () => + 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, + } + ), + }), + }), + }; + } + 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"); + }); + + 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), + }); + }); +}); 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" }), + }); + }); +}); 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..233b518 --- /dev/null +++ b/docs/superpowers/plans/2026-05-18-mcp-sprint-b-onchain-tools.md @@ -0,0 +1,2314 @@ +# 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: 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** + +```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. 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. 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. 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/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. +

+ )} +
+ ); +} 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() { + + ); } 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"); + } + }); +}); 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") 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" }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3078069..96d2241 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 @@ -26,12 +29,18 @@ 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@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) @@ -74,34 +83,34 @@ 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: '@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 @@ -267,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 @@ -288,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 @@ -304,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: @@ -1181,6 +1193,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 +1688,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 +3874,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 +5449,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 +6943,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 +7069,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 +7821,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 +7829,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 +7841,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 +7934,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 +7975,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 +7983,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' @@ -8419,11 +8475,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 +8510,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 +8761,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 +8771,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 +8781,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 +8799,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 +9021,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 +9036,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 +9057,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@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/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 + '@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@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': {} - '@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 +9087,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 +9099,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 +9118,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 +9964,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 +10043,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 +10078,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 +10114,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 +10158,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 +10195,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 +10232,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 +10267,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 +10303,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 +10341,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 +10380,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 +10391,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 +10402,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 +10445,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 +10567,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 +10577,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 +10630,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 +11049,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 +11066,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 +11080,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 +11099,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: @@ -11033,6 +11114,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) @@ -11412,26 +11527,39 @@ 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: + - 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: @@ -11478,7 +11606,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 +11614,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 +11626,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 +11634,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) @@ -11518,6 +11646,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) @@ -11792,7 +11940,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 +11948,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 +11959,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 +11967,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) @@ -11830,6 +11978,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) @@ -12005,6 +12172,8 @@ snapshots: - typescript - utf-8-validate + '@stablelib/base64@1.0.1': {} + '@supabase/auth-js@2.104.1': dependencies: tslib: 2.8.1 @@ -12019,19 +12188,19 @@ snapshots: dependencies: tslib: 2.8.1 - '@supabase/realtime-js@2.104.1(bufferutil@4.1.0)(utf-8-validate@6.0.6)': + '@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@6.0.6) + ws: 8.20.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) transitivePeerDependencies: - 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,12 +12208,12 @@ snapshots: iceberg-js: 0.8.1 tslib: 2.8.1 - '@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)': 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/realtime-js': 2.104.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) '@supabase/storage-js': 2.104.1 transitivePeerDependencies: - bufferutil @@ -12415,19 +12584,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 +12637,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 +12666,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 +12680,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 +12710,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 +12724,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 +12754,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 +12768,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 +12798,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 +12846,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 +12887,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 +12966,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 +13027,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 +13063,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 +13099,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 +13135,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 +13291,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 +13300,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 +13331,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 +13340,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 +13371,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 +13380,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 +13411,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 +13420,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 +13451,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 +13469,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 +13495,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 +13513,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 +13539,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 +13559,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 +14214,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 @@ -14302,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)) @@ -14325,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 @@ -14340,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 @@ -14362,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 @@ -14391,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 @@ -14656,6 +14825,8 @@ snapshots: fast-safe-stringify@2.1.1: {} + fast-sha256@1.3.0: {} + fast-stable-stringify@1.0.0: {} fast-uri@3.1.2: {} @@ -14746,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 @@ -15085,9 +15256,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: @@ -15783,21 +15954,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 +16523,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 +16570,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 +16698,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 +16984,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 +17001,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 +17018,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 +17035,16 @@ 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: @@ -16872,15 +17052,15 @@ 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): + 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@3.25.76) + 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@6.0.6)) - ox: 0.14.13(typescript@5.9.3)(zod@3.25.76) + ox: 0.14.13(typescript@5.9.3)(zod@4.3.6) ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) optionalDependencies: typescript: 5.9.3 @@ -17089,14 +17269,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 +17399,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: @@ -17238,27 +17418,26 @@ snapshots: 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' 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"); + }); +}); 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"]