Skip to content

Reliable IPFS, rationale drafting/caching, and ballot CSV#300

Merged
QSchlegel merged 1 commit into
preprodfrom
claude/ballot-ipfs-rationale
Jun 14, 2026
Merged

Reliable IPFS, rationale drafting/caching, and ballot CSV#300
QSchlegel merged 1 commit into
preprodfrom
claude/ballot-ipfs-rationale

Conversation

@QSchlegel

Copy link
Copy Markdown
Collaborator

Summary

Fixes the flaky IPFS upload/download on the ballot voting UI, adds rationale drafting persisted to the DB, surfaces the rationale for review in the pending transaction, and adds ballot CSV import/export — the four asks from the "Manage Ballots" report (IPFS 504s loading rationale from ipfs.io).

IPFS reliability

  • New src/lib/ipfs.ts gateway helpers + /api/ipfs/resolve, a multi-gateway server proxy: tries the dedicated Pinata gateway first, then public fallbacks, 6 s per gateway, 2 MB cap, long-cached. Browser reads go through it instead of hitting the frequently-504ing ipfs.io directly (no CORS, no single-gateway failure).
  • Uploads (pinata-storage/put.ts, image/put.ts) now return a dedicated-gateway URL via ipfsGatewayUrl() rather than a hardcoded ipfs.io URL.
  • NEXT_PUBLIC_PINATA_GATEWAY_URL added to src/env.js (optional; accepts a bare host — scheme normalised in code). On-chain anchor hashes are unaffected (hashDrepAnchor hashes the JSON body, not the URL).

Rationale drafting + DB cache

  • Draft comments persist on blur via updateProposalRationale; uploading also caches the comment in the Ballot row. No schema migration — the existing parallel arrays already hold it.
  • transaction-card.tsx gains a VoteRationale block per vote that prefers the DB-cached comment (no network) and falls back to the IPFS proxy, with a gateway "source" link.

Ballot CSV

  • New BallotCsv.tsx (papaparse + react-dropzone). Columns: proposal_id,title,vote,comment,anchor_url,anchor_hash. Import merges by proposal_id (blank cells preserve existing values; new rows default to Abstain); export is correctly quoted via Papa.unparse. Available on both the populated and empty-ballot views.

Hardening (from an adversarial multi-agent review of this branch)

  • resolve.ts follows redirects manually and only to allow-listed gateway hosts (SSRF guard incl. *.ipfs.dweb.link/w3s.link subdomain forms), and pins Content-Type: application/json + nosniff.
  • fetchIpfsJson rejects non-https non-IPFS anchors (blocks http://localhost/169.254.169.254 from a hostile co-signer's anchor) and adds a timeout backstop.
  • The rationale auto-load effect now merges by proposalId + anchor so refetches (e.g. changing a vote) don't clobber in-progress edits, aborts in-flight fetches on cleanup, and de-dupes re-fetches.

Verification

  • tsc --noEmit clean; next build --webpack succeeds (/api/ipfs/resolve emitted as a function route).
  • 14 review findings triaged: 12 fixed; 2 consciously deferred — the updateBallot full-overwrite vs. concurrent per-row-edit race (architectural; worst-case silent vote loss already removed by the blank-cell-preserve fix) and a low-severity O(n·m) cache lookup that tRPC already dedupes.

Notes

  • No DB migration. .env's NEXT_PUBLIC_PINATA_GATEWAY_URL is already set (bare host) and now wired through.

🤖 Generated with Claude Code

Fix flaky IPFS up/download, add rationale drafting persisted to the DB,
surface rationale in pending-tx review, and add ballot CSV import/export.

IPFS reliability
- New src/lib/ipfs.ts gateway helpers + /api/ipfs/resolve, a multi-gateway
  server proxy (dedicated Pinata gateway first, then public fallbacks, 6s/
  gateway, 2MB cap, cached). Reads route through it instead of hitting the
  frequently-504ing ipfs.io directly.
- Uploads (pinata-storage put + image/put) return a dedicated-gateway URL via
  ipfsGatewayUrl() instead of a hardcoded ipfs.io URL.
- NEXT_PUBLIC_PINATA_GATEWAY_URL added to env (optional, bare host accepted;
  scheme normalised in code).

Rationale drafting + DB cache
- Draft comments persist on blur via updateProposalRationale; uploads also
  cache the comment in the Ballot row (no schema change).
- transaction-card VoteRationale shows each vote's rationale, preferring the
  DB cache and falling back to the IPFS proxy, with a gateway "source" link.

Ballot CSV
- New BallotCsv (papaparse + react-dropzone): columns
  proposal_id,title,vote,comment,anchor_url,anchor_hash. Import merges by
  proposal_id; blank cells preserve existing values; export is quoted via
  Papa.unparse.

Hardening (from adversarial review)
- resolve.ts follows redirects manually and only to allow-listed gateway hosts
  (SSRF guard), and pins Content-Type: application/json + nosniff.
- fetchIpfsJson rejects non-https non-IPFS anchors and adds a timeout backstop.
- Auto-load effect merges by proposalId+anchor so refetches don't clobber
  in-progress edits, aborts in-flight fetches, and de-dupes re-fetches.

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

vercel Bot commented Jun 13, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
multisig Ready Ready Preview, Comment Jun 13, 2026 9:46pm

Request Review

@QSchlegel QSchlegel merged commit 9e2f72e into preprod Jun 14, 2026
6 of 8 checks passed
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.

1 participant