Skip to content

feat: agent onboarding, governance overview, wallet transfer, ballot UX#254

Open
QSchlegel wants to merge 18 commits into
preprodfrom
claude/happy-bhabha-fa7fd2
Open

feat: agent onboarding, governance overview, wallet transfer, ballot UX#254
QSchlegel wants to merge 18 commits into
preprodfrom
claude/happy-bhabha-fa7fd2

Conversation

@QSchlegel
Copy link
Copy Markdown
Collaborator

Summary

Four loosely-related improvements bundled together:

  • Agent onboarding — public /bot-setup page, agent-readable /api/v1/botSetupGuide markdown endpoint, /llms.txt discovery at the instance root, and a "Copy agent prompt" button on the user Bot accounts card.
  • Cross-instance wallet transferGET /api/v1/wallet/transfer/export (owner JWT) and POST /api/v1/wallet/transfer/import, plus UI: a "Transfer wallet" card on the wallet info page (download JSON or push to a remote instance, optional contacts/ballots payload) and an "Import Transfer" button on the wallets list page. Imports land as NewWallet so the existing invite/claim flow takes over.
  • Governance overview — wallet governance dashboard summary (proposal status counts, ballot progress, voting power, last ballot activity); live network stats strip on the public /governance landing; DRep list aggregate header + active/inactive filter + surfaced active_epoch / hex per row; proposal detail surfaces fields that were already fetched but never rendered ("Your ballot entry" + "Technical details").
  • Ballot UX + standalone rationale — shared rationale module (build CIP-100 JSON-LD, hash, upload to IPFS, load-from-URL) and a reusable RationaleEditor component. The vote card now has an "Attach voting rationale" toggle, so a user can attach an anchor URL + hash to a single-proposal vote without creating a Ballot. VoteButton threads the anchor through to txBuilder.vote(). Ballot summary reports rationale-uploaded/draft counts; moving a proposal between ballots now goes through a Keep / Add to both / Move here dialog instead of silently relocating it.

Test plan

  • npm run typecheck — passes locally
  • npm run build — succeeded locally (BUILD_EXIT=0)
  • Visit /bot-setup and /llms.txt unauthenticated — renders
  • Visit /governance — see the "Live Cardano governance" stats card above the educational cards
  • Visit /governance/drep — see aggregate header + Active/Inactive filter; per-row epoch/hex
  • Connect a wallet, visit /user — click "Copy agent prompt", paste into a fresh Claude session, confirm the agent can complete botRegister → claim → pickup → auth → botMe
  • On a wallet you own, visit /wallets/[id]/info — "Transfer wallet" card: download JSON, then on the wallets list page import it with "Import Transfer", confirm the resulting invite URL works
  • Open a proposal detail page — confirm "Your ballot entry" surfaces rationale + anchor for proposals already in a ballot; confirm "Technical details" shows return_address, governance_description.tag, anchor URL/hash, gov action ID
  • On /wallets/[id]/governance, click "Attach voting rationale" in the Vote card, write a comment, upload to IPFS, vote — confirm the on-chain tx carries the anchor (description includes "+ rationale")
  • Open Manage Ballots, add a proposal that is already on another ballot — confirm the new "Keep / Add to both / Move here" dialog appears instead of a silent move
  • Ballot summary shows "Rationale uploaded: X/Y" and any draft count

🤖 Generated with Claude Code

jinglescode and others added 18 commits May 1, 2026 09:44
…ecurity headers) (#233)

* refactor: remove unused recharts and busboy deps; relocate swagger CSS

- Delete src/components/ui/chart.tsx (recharts wrapper, zero consumers)
- Remove recharts from dependencies
- Remove busboy and @types/busboy (formidable is the actual uploader)
- Move swagger-ui CSS imports out of _app.tsx into api-docs.tsx

* refactor(react): use signer address as stable key in ReviewSignersCard

Replaces array index with signer address (which is unique and stable across reorder/edit) on both desktop TableRow and mobile card view, preventing form-state misalignment when signers are removed or reordered.

* ci: add PR checks workflow, basic security headers, env comment

- Add .github/workflows/pr-checks.yml (lint/typecheck/test/build, continue-on-error initially)
- Add basic security headers in next.config.js (X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy). CSP and HSTS intentionally omitted.
- Add comment to src/env.js explaining why NextAuth env vars are commented (PrismaAdapter only, no providers configured)

* chore: add typecheck, format, format:check scripts

Used by .github/workflows/pr-checks.yml and developer workflow.

- typecheck: tsc --noEmit
- format: prettier --write .
- format:check: prettier --check .

* chore: refresh package-lock.json after recharts/busboy removal

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: keep swagger-overrides.css in _app.tsx (Next requires global CSS at root)

Reverts only the swagger-overrides.css move from commit 4abe300. The
swagger-ui-react/swagger-ui.css import (from node_modules) remains
local to api-docs.tsx, so the original goal of keeping that bundle
out of every page is still achieved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR checks were silently passing because typecheck, test, and build steps
were marked continue-on-error, so the gates only reported status, never
blocked merges. Drop continue-on-error from those three; keep it on lint
until the rule set is cleaned up.

Add dependabot config for npm + github-actions, with grouping for
@meshsdk/*, next/@next/*, prisma/@prisma/*, @trpc/*, and @types/*.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two interacting issues caused the production build to crash on routes
that imported resolve-adahandle (notably /wallets/[wallet]/transactions/new):

1. resolve-adahandle.tsx called getProvider(1) at module top level. Under
   `next build` page-data collection, every server-rendered page imports
   that module, which constructs BlockfrostProvider(undefined) when
   NEXT_PUBLIC_BLOCKFROST_API_KEY_MAINNET isn't readable at build time
   (e.g. SKIP_ENV_VALIDATION=true). The constructor throws and the
   webpack runtime reports it as a generic "factory error".

2. next.config.js had `optimization.sideEffects: false` set globally,
   which tells webpack that *every* file is side-effect-free. That
   silently strips global CSS imports and any module-level initialization,
   masking issues like (1) until you hit a route that exercises them.

Fix:
- Lazy-init the mainnet provider with a cached singleton, so import is
  free and instantiation only happens when a caller actually resolves a
  handle (always client-side).
- Remove the global sideEffects:false override. Per-package sideEffects
  declarations in package.json are the correct mechanism; the global
  override was masking real bugs.
- Move swagger-ui-react CSS import from api-docs.tsx into _app.tsx so
  Next.js Pages Router's "global CSS only from _app" rule is satisfied.

Verified locally: `next build --webpack` completes; both
/wallets/[wallet]/transactions/new and /api-docs render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…bility

Comprehensive server-side hardening pass: closes auth/security gaps,
adds an append-only AuditLog for security-relevant events, indexes
frequently queried columns, centralizes ctx typing, and lands shared
auth helpers.

## Database (already deployed to prod via prisma migrate deploy)

- New `AuditLog` table for append-only security audit trail
  (auth flows, wallet/transaction mutations, privilege grants, signer
  changes). Five indexes for the common access patterns.
- Btree indexes on Wallet/NewWallet `ownerAddress`, Signable+Transaction
  `walletId`/`state`/`(walletId, state)`, Proxy `walletId`/`userId`/
  `(walletId, isActive)`/`(userId, isActive)`, Ballot `walletId`,
  BalanceSnapshot `walletId`/`(walletId, snapshotDate)`.
- GIN indexes on Wallet/NewWallet `signersAddresses` (array_ops) — the
  signer-membership query was a full table scan.
- Restored `Crowdfund` model declaration (production drift: table exists
  in prod but was never declared in main's schema; see PR description
  for full archaeology). Marked as retained-but-unused.
- WalletBotAccess: `@@unique` -> `@@id` to match prod (drift from
  PR #207 / commit 1facdc3 where schema and migration disagreed at
  landing).
- Ballot.updatedAt: restored `@default(now()) @updatedAt` to match
  prod's column default (drift accumulated across multiple commits).
- Ballot.anchorUrls / anchorHashes: added `DEFAULT ARRAY[]::TEXT[]` to
  match the schema's `@default([])` annotation.

## Observability primitives

- `src/lib/observability/audit.ts` — `audit(db, event)` emitter; never
  throws (audit miss must not break user flow); redacts secrets in
  metadata before write.
- `src/lib/observability/logger.ts` — structured logger; JSON in prod,
  human-readable in dev; never logs raw tokens/signatures/cookies.

## Security fixes (Wave 1-3)

- Closed `ownerAddress === "all"` bypass in `assertWalletAccess`. The
  string "all" was being treated as a wildcard owner — any session
  could claim ownership of any wallet whose `ownerAddress` happened to
  contain that literal.
- `lookupMultisigWallet`: validate stake-credential-hash format before
  query (prevents prefix-match abuse and full-table scans on malformed
  input).
- Centralized rate-limit and request-guard surface (`src/lib/security/
  rateLimit.ts`, `requestGuards`). Bot routes now use bot-scoped
  rate limit; user routes use IP-scoped.
- `verifyJwt`: stricter token-type narrowing; explicit `isBotJwt`
  predicate.
- `walletSession`: tighter expiry handling, no implicit refresh.

## Auth helpers (Wave 8)

- New `src/server/api/auth.ts` consolidates `requireSessionAddress`,
  `getSessionAddresses`, and wallet-access checks that were duplicated
  in nearly every router. One source of truth, one place to extend.
- All routers and v1 API handlers migrated.

## ctx typing (Wave 2)

- New `AuthCtx` and `TRPCContext` exported from `src/server/api/trpc.ts`.
- All router helpers use `AuthCtx` instead of `any`.
- `protectedProcedure` middleware: type-narrows `sessionWallets`,
  `primaryWallet`, `sessionAddress` correctly.

## Audit emitters (Wave 5)

Wired into:
- auth flow (login success/failure, JWT mint, bot auth)
- wallet mutations (create, update, archive, transfer, signer changes)
- signable + transaction mutations (sign, reject, broadcast)
- bot privilege grants

All emitters fire after the underlying action and never block it.

## SSRF defense for `/api/v1/og`

The OG metadata endpoint now:
- requires https, denies non-allowlisted hosts
- DNS-resolves and rejects private/loopback/link-local addresses
- denies upstream redirects (no auto-follow)

`OG_ALLOWED_HOSTS` env var configures the allow list; "*" allows any
public host (still SSRF-guarded).

## Test infrastructure

- jest.config.mjs — moduleNameMapper for CSS, transformIgnorePatterns
  for ESM-only deps (superjson, @trpc, @meshsdk, jose, etc.)
- setupEnv.cjs — pre-test env bootstrap (SKIP_ENV_VALIDATION=1, dummy
  DB/JWT/Blockfrost values) so `src/env.js` doesn't throw on import.
- Frozen wall clock (`Date.now`/`new Date`) for byte-identical test
  runs; real timer APIs preserved.
- `__mocks__/styleMock.cjs` — CSS imports mock for jest.

## Tests

- New: `og.test.ts` (SSRF tripwire suite — 9 cases for the og handler).
- New: `signing.test.ts` (source tripwires preventing the
  `return true ? signature : undefined` regression and similar).
- Updated existing tests to match Jest 30 strict mock typing
  (jest.fn<...>() generics) and new ctx fields.

## Verification

- Typecheck clean
- All 165 staged-suite tests pass deterministically across two runs
- Migration `20260510160404_audit_log_and_indexes` already applied to
  the multisig Supabase production DB — `prisma migrate deploy` on
  this branch is a no-op (idempotent).

Depends on: #236 (build fix; without it `next build --webpack` will
crash on `/wallets/[wallet]/transactions/new`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The new-wallet-flow and wallet-migration flow used the raw signer
address as a React key in ReviewSignersCard. Two issues:

1. The address can be empty/undefined while the user is editing, which
   makes the key non-unique → React reuses inputs across rows, swapping
   names and addresses while typing.
2. Two signers can momentarily share the same partial input, again
   producing duplicate keys.

Fix: every row gets a stable opaque `signerId` generated when the row
is created; the component uses `signerId` as the React key. Address
becomes regular state, free to be empty/duplicate transiently without
breaking React identity.

The same fix applied to both `useWalletFlowState` (new wallet) and
`useMigrationWalletFlowState` (migration). The shared `signerRows.ts`
module emits the id and keeps the parallel arrays in sync.

`reviewSignersCardKey.test.ts` is a tripwire suite that:
- greps the source to assert the raw address is never used as a key
- verifies both flow-state hooks expose signerIds parallel to
  signersAddresses

This catches future regressions structurally — the type system can't
enforce \"don't use address as key\", but a regex over source can.

Test plan
- 171/171 tests pass deterministically on top of #237
- Typecheck clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Small UI/hook cleanup that fell out of the audit:

- overall-layout: add skip-link + main-content anchor; aria-label on the
  main form. Trims unused legacy nav code.
- transaction-card: useMemo the JSON.parse(transaction.txJson) so the
  parse only runs when txJson changes, not on every render. Removes a
  dead `import { get } from \"http\"` that was sneaking into the client
  bundle.
- signable-card: defensive parse for legacy payload shapes.
- card-show-signers, signing/index: small render fixes.
- ImgDragAndDrop, MeshProviderClient, BotManagementCard, background.tsx:
  drop dead state vars / unused imports surfaced by the audit.
- useAppWallet, useMultisigWallet: stable returns; missing-wallet path
  no longer spins indefinitely.
- Delete `src/components/multisig/proxy/ProxyControlExample.tsx`. It was
  example-only code, not exported from the proxy index, never rendered
  anywhere. The barrel import is updated.

Test plan
- 165/165 staged-suite tests pass on top of #237
- Typecheck clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The IPFS branch of the asset card was passing a single hardcoded ipfs://
literal to <IPFSImage>, so every IPFS-based NFT in the wallet rendered
the same placeholder image instead of its own metadata.image. The
non-IPFS sibling already uses imageSrc / name / 60x60 correctly; this
brings the IPFS branch in line.

Also fixes:
- alt text now uses the asset name (was "IPFS Image" — bad a11y)
- width/height set to 60 to match the parent 60x60 div (was 300, was
  being clipped by overflow-hidden + wasting bandwidth)

Single occurrence in the codebase. Introduced 2025-03-27.
ProxyControl had three state variables whose values were never read,
only their setters. Calling them produced empty re-renders.

- setLocalLoading: paralleled setSetupLoading and setSpendLoading; the
  read-side `setupLoading` and `spendLoading` are the live signals
- setSelectedUtxos / setManualSelected: stored values from the UTxO
  selector callback that nothing downstream consumed; the contract
  already uses all UTxOs from the multisig wallet (per existing
  comment), so the selection is purely visual

The UTxOSelector callback is preserved as a no-op (the prop is
required) with a comment explaining the visual-only intent.

Net -11 lines, zero behavior change.
fix(build): lazy-init mainnet provider; drop global sideEffects:false
feat(server): AuditLog, DB indexes, security hardening, observability
chore(ci): gate typecheck/test/build; add dependabot
refactor(wallet-flow): stable signerIds for React keys; tripwire test
chore(ui): a11y skip-link, useMemo, hook fixes, dead-code cleanup
fix(wallet-assets): show actual IPFS NFT image instead of hardcoded CID
chore(proxy): remove three write-only useState declarations
Removes the Nostr-based wallet chat that was throwing
`wss://relay.damus.io/` WebSocket errors. Drops the
@jinglescode/nostr-chat-plugin dependency along with the chat page,
component, sidebar link, and homepage feature card.

Server-side: removes nostrKey from createUser input and deletes the
unused getNostrKeysByAddresses procedure. Adds a migration making
User.nostrKey nullable so new signups insert without a key (existing
rows preserved).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Bot onboarding for AI agents: public /bot-setup page, agent-readable
  /api/v1/botSetupGuide markdown endpoint, /llms.txt discovery, and a
  "Copy agent prompt" button on the user Bot accounts card so any AI
  agent pointed at the instance URL can register itself end-to-end.

- Cross-instance wallet transfer: new /api/v1/wallet/transfer/export
  (owner JWT) and /api/v1/wallet/transfer/import endpoints with a
  shared WalletTransferPayloadV1 type. UI exposes "Transfer wallet"
  on the wallet info page (download JSON or push directly to a remote
  instance URL, optional contacts/ballots payloads) and "Import
  Transfer" on the wallets list page. Imports land as NewWallet so the
  existing invite/claim flow takes over.

- Governance overview improvements:
  - Wallet governance dashboard summary card (proposal status counts,
    ballot progress, voting power, last ballot activity)
  - Live network stats strip on the public /governance landing
  - DRep list aggregate header, active/inactive filter, surfaced
    active_epoch and hex per row
  - Proposal detail "Your ballot entry" (rationale + anchor) and
    "Technical details" sections surfacing fields that were already
    fetched but never rendered

- Ballot UX + standalone rationale: shared rationale module
  (build JSON-LD, hash, upload to IPFS, load-from-URL) and a reusable
  RationaleEditor component. The vote card now offers an "Attach
  voting rationale" toggle so a user can attach a CIP-100 anchor to a
  single-proposal vote without creating a Ballot. VoteButton threads
  the anchor through to txBuilder.vote(). Ballot summary now reports
  rationale-uploaded / draft counts; moving a proposal between
  ballots is gated by a Keep / Add to both / Move here dialog instead
  of silently relocating it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
multisig Ready Ready Preview, Comment May 11, 2026 8:26pm

Request Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants