Skip to content

Release: mobile/UX foundations, governance vote UX + DB-cached tallies, signing & infra fixes#303

Open
QSchlegel wants to merge 41 commits into
mainfrom
preprod
Open

Release: mobile/UX foundations, governance vote UX + DB-cached tallies, signing & infra fixes#303
QSchlegel wants to merge 41 commits into
mainfrom
preprod

Conversation

@QSchlegel

Copy link
Copy Markdown
Collaborator

Promotes everything currently on preprod to main (41 commits). Summary by theme:

Governance & voting

Transactions & assets UX

Mobile / UX foundations (#287#292)

Infra & deps

⚠️ Deploy checklist for main

🤖 Generated with Claude Code

QSchlegel and others added 30 commits June 13, 2026 10:44
…NNSESSION)

Production was 500ing every query with "(EMAXCONNSESSION) max clients
reached in session mode - max clients are limited to pool_size: 15".

Verified root cause (live + multi-agent audit):
- src/server/db.ts created the @prisma/adapter-pg pool with no `max`, so
  node-postgres defaulted to 10 connections per warm Vercel instance. A
  couple of instances overrun Supabase's session-mode pool (15 client
  slots) -> EMAXCONNSESSION on every query, including user.createUser.
- The retry wrapper amplified it: it called $connect() against the dead
  pool between retries and, once connectionTimeoutMillis is finite, would
  treat the "timeout exceeded when trying to connect" acquire error as a
  retryable connection error.

Changes:
- Cap the pool: max: 2, idleTimeoutMillis 10s, connectionTimeoutMillis 10s
  (finite timeout fails fast instead of pg's default infinite wait).
- isConnectionError(): never retry pool-saturation errors (max clients
  reached / pool_size / EMAXCONNSESSION / connect-timeout).
- Drop the $connect() reconnect between retries (the driver-adapter pool
  reconnects lazily; forcing connect just adds load).

The Prisma globalThis singleton was verified correct and left unchanged.
The 5 interactive $transaction blocks are pure-DB (no external I/O held),
so no leak fix is required for this to hold.

NOTE (maintainer action, env — cannot be done in code): point production
DATABASE_URL at the Supabase TRANSACTION pooler (port 6543, ?pgbouncer=true)
and keep DIRECT_URL on the direct connection (5432) for migrations. The
session pooler (5432) is the wrong mode for serverless; this code change is
the necessary client-side cap and works as an interim mitigation too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Break grouped quarters into individual Month 2–12 sections, each with
  Quirin/Andre task tables matching Month 1's format
- Add Document Sign-Off flagship feature (MVP→v1→v2→v3) woven across months
- Add Mesh 2.0 upgrade; extend FROST research to include Lemour PQC multi-sig
- Shift schedule one month earlier (June work completed); April is buffer
- Drop completed items (Aiken crowdfund, full address, pagination, collateral, 404)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
docs(roadmap): month-by-month breakdown with per-owner tasks
…e signature mismatch

DRep votes (and any tx with Conway voting_procedures) failed client-side with
"Wallet returned witness that does not verify against tx body hash", and would
be rejected on-chain (InvalidWitnessesUTXOW).

Root cause: the tx is BUILT with core-cst (MeshTxBuilder's default CardanoSDK
serializer), so the wallet signs the body hash core-cst produces. But the
witness verify + merge used core-csl (whisky) calculateTxHash / Transaction
reconstruction, which re-serializes the body to different bytes (voting_procedures
map order, set tag 258) → a different hash → valid witnesses fail to verify. The
two serializers agree for ordinary txs, so only Conway-vote-shaped txs broke.

Move the verify/merge/hash onto core-cst (the 2.0 stack, already a dependency),
so build and verify use one encoder and the original body bytes every signer
signed are preserved:
- mergeSignerWitnesses: verify new witnesses against resolveTxHash(originalTx)
  and merge via addVKeyWitnessSetToTransaction (preserves body bytes). Drops the
  body-swap workaround — every co-signer now signs the same stored body.
- filterWitnessesToScripts: rebuild the witness set with core-cst so dropping
  extraneous vkeys no longer re-encodes the body.
- diagnoseTxWitnesses + the server signature check in transactions.ts: hash with
  resolveTxHash so vote signatures are recognised (equal to the old hash for
  ordinary txs).

Verified invariants (tests): adding/filtering vkeys via core-cst preserves
resolveTxHash; a witness over resolveTxHash verifies; co-signers accumulate
without re-encoding. tsc clean; full suite 362 passed.

Follow-up (not in this PR): the server v1 bot path (signTransaction.ts +
addUniqueVkeyWitnessToTx) still uses core-csl calculateTxHash and needs the same
core-cst migration for bot-submitted votes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(governance): verify/merge witnesses with core-cst (DRep-vote signature mismatch)
…sets

Foundational mobile fixes (PR 1 of the UX/mobile quick-wins pass):
- viewport meta gains viewport-fit=cover so env(safe-area-inset-*) resolves
  (without it the insets are always 0).
- Full-height containers use 100dvh instead of 100vh/h-screen so the layout
  isn't clipped by mobile-Safari / wallet-webview dynamic toolbars
  (_app, layout root + inner content column).
- Main header grows by the safe-area top inset on mobile so it clears the
  notch/status bar, and honors side insets in landscape.
- Mobile nav drawer offsets by the safe-area top, uses 100dvh, and pads the
  bottom for the home indicator.
- Bottom Sheet variant pads the bottom safe area.

Desktop is unchanged (insets are 0; header rule is scoped below md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PR 2 of the UX/mobile quick-wins pass. Buttons/inputs were 32-36px, below
the recommended 44px touch target. A single @media (pointer: coarse) rule
enlarges interactive controls on touch devices only — no per-call-site
edits and zero change to desktop density. Plain text links are
intentionally excluded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…loading/empty conflation

PR 3 of the UX/mobile quick-wins pass.
- Add shared `Skeleton` (shadcn) and `EmptyState` (Card-based) primitives.
- Wallet detail routes (info/transactions/governance/assets) rendered a blank
  fragment while `appWallet` loaded — replace with a `WalletDetailSkeleton` so
  there's no white flash on every wallet open.
- all-transactions: it showed "No transactions yet" *while still loading*
  (undefined === loading). Split into a skeleton (loading) vs an `EmptyState`
  (loaded and empty).
- proposals: plain-text "No proposals found" → `EmptyState`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PR 6 of the UX/mobile quick-wins pass (input correctness).
- Recipient address fields used invalid type="string" with mobile autocorrect/
  autocapitalize active — which can silently corrupt case-sensitive bech32
  addresses. Now type="text" with inputMode="text" autoCapitalize="off"
  autoCorrect="off" spellCheck={false}.
- Amount fields get inputMode="decimal" (numeric keypad on mobile).
- Base Input uses text-base on mobile (sm:text-sm on desktop) so iOS Safari
  no longer zooms in on focus (<16px triggers it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(mobile): viewport-fit + dvh + safe-area insets (foundations)
feat(mobile): >=44px touch targets on coarse-pointer devices
feat(ux): Skeleton + EmptyState primitives; fix blank pages & loading/empty bug
fix(mobile): bech32-safe address inputs + decimal keypad + no iOS zoom
… + scroll)

PR 4 of the UX/mobile quick-wins pass. Centered Dialogs overflowed small
screens — content (and action buttons) got clipped off-screen, especially the
wide governance modals.

Make the base DialogContent mobile-safe without changing desktop layout:
w-[calc(100%-1.5rem)] (stay within the viewport with a small margin),
max-h-[90dvh] + overflow-y-auto (scroll internally instead of off-screen). The
max-w-lg cap and centered position are unchanged at desktop. BallotModal and
RegisterDrepModal switch their max-h from vh to dvh so the mobile toolbar
doesn't hide the bottom.

(Kept dialogs centered rather than converting to a bottom sheet to avoid
restructuring positioning on the shared base that backs every dialog; the
overflow fix is the critical part. Needs a quick on-device / Chrome-MCP visual
check before promoting past preprod.)

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

PR 5 of the UX/mobile quick-wins pass.
- src/utils/errors.ts `getFriendlyError(error)` maps the common raw errors
  (CIP-30 {code:-2}, account-changed, 429/too-many-requests, insufficient
  funds, Blockfrost/UTXOS, user-decline) to short human messages, falling back
  to the raw message.
- src/utils/toast-error.ts `toastError(error, title?)` — destructive toast with
  the normalized message (additive; doesn't touch the TOAST_LIMIT=1 reducer).
- Adopt in DRep registration's catch (raw e.message -> getFriendlyError), while
  keeping the "Copy Error" action for raw debug details.

Other error-prone flows (new-transaction, WalletAuthModal) can adopt the
helper incrementally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(mobile): keep dialogs within the viewport (responsive width + dvh + scroll)
feat(ux): friendly-error helper + toastError wrapper
The transactions pagination bar overflowed its card on the right and used
hardcoded dark colors + a raw <select>. Rewrite the body (props unchanged):
- container: themed `border bg-card`, `flex-wrap min-w-0 gap-3` so the groups
  reflow instead of spilling off the right edge.
- sort toggle: outline Button, icon-only on mobile (label hidden < sm).
- page size: shadcn Select (was a native unstyled <select>); same options +
  reset-to-page-1 behavior.
- nav: compact icon-only prev/next (h-9 w-9), muted tabular-nums page indicator;
  rely on Button's native disabled styling.

PaginationProps is unchanged, so both call sites (transactions + DRep list)
behave identically.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The on-chain history rows (desktop table + mobile cards) led with the raw
truncated tx hash — the least human-meaningful field — while the actual
description ("Ballot Vote: …") was buried below. Flip the hierarchy to match
the pending-tx cards:
- primary: the dbTransaction description, or a "Sent"/"Received" fallback (and
  the cert label on desktop) so rows without a DB record aren't identified by
  a bare hash.
- date below.
- hash demoted to a quiet muted mono link with the external-link arrow.

Also standardize the hash truncation on getFirstAndLast(hash, 8, 8) (was two
different inline substring schemes) and drop the now-redundant standalone
description block on mobile. Cardanoscan links, outputs, certs, signers, and
the row actions menu are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Long token names ($drep.collective) and raw quantities pushed the value/ticker
past the card's right edge (the clipped "↗1 $S…"). Root cause: the flex row +
left group had no min-w-0, so text couldn't shrink, and the name/quantity/ticker
were never truncated.

- min-w-0 on the row + flex-1 min-w-0 on the left group (lets text shrink).
- 60px avatar wrappers flex-shrink-0 (so the image never compresses instead).
- name h3 truncates (+ title tooltip) and is bounded via truncateTokenSymbol
  for the raw-unit hex fallback; remove ml-auto from the link.
- value block flex-shrink-0; quantity via numberWithCommas (tabular-nums);
  ticker truncates with a max width + title.

Full name stays reachable via tooltip + the Cardanoscan token link.

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

The per-proposal voting controls were confusing: a raw OS <select> for the
Yes/No/Abstain choice, plus TWO competing actions (a green "Add to Ballot"
button in proposals.tsx AND a mystery ballot icon in VoteButton).

- Replace the native <select> with a shadcn segmented Yes/No/Abstain control
  (color-coded selected state, icons). Guard `(v) => v && setVoteKind(v)` so a
  re-click can't blank the vote read into the tx.
- One clear primary: the Vote button now states the choice ("Vote Yes",
  "Vote Yes (Proxy)") and uses the themed primary style.
- One ballot entry: the icon-only ballot button becomes a labeled "Add to
  ballot" / "In N ballots" secondary, with a tooltip distinguishing the two
  flows (vote on-chain now vs collect for co-signers).
- Remove the now-duplicate green "Add to Ballot" buttons (mobile + desktop) in
  proposals.tsx; keep the View links.

vote()/voteProxy() bodies, proxy path, keepRelevant, metadata label 674, the
closed-vote Lock state, and all toasts are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The proposal meta row showed the governance action type as plain uppercase
text ("TREASURY WITHDRAWALS"), visually indistinguishable from the other meta
fields and hard to scan in a long list.

Add a GovernanceTypeChip that renders each Conway action type as a color-coded
outline Badge with an icon:
- treasury_withdrawals  amber   Coins
- info_action           blue    FileText
- parameter_change      purple  Settings2
- hard_fork_initiation  orange  GitBranch
- no_confidence         red     XCircle
- new_constitution      teal    FileText
- new/update_committee  indigo  Users
- unknown               slate   Hash (Title-Cased fallback label)

Used in both the mobile and desktop meta rows (single shared component, no
view drift). Purely presentational — no data or vote-flow changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(ux): themed, non-overflowing, mobile-friendly pagination
fix(ux): lead transaction rows with the label, demote the hash
fix(assets): stop token rows overflowing the Assets card
# Conflicts:
#	src/components/pages/wallet/governance/proposals.tsx
# Conflicts:
#	src/components/pages/wallet/governance/proposals.tsx
feat(governance): color-coded proposal type chips
# Conflicts:
#	src/components/pages/wallet/governance/proposals.tsx
feat(governance): clearer ballot voting UX (segmented vote + one entry per action)
QSchlegel and others added 11 commits June 13, 2026 19:02
SEO:
- Central src/lib/seo.ts; route-aware <Metatags> (titles, canonical, OG,
  Twitter, robots noindex, JSON-LD) hoisted out of the ssr:false boundary
- /robots.txt + /sitemap.xml; static public/og-image.png (+ generator script)
- NEXT_PUBLIC_SITE_URL env; favicon/manifest fixes; drop duplicate metatags

Landing & public pages:
- Live mock-data feature previews replace PNG screenshots (auto-track the UI)
- Unified SiteFooter across all public routes; hero polish; /features refresh
- Multisig 3-of-5 signing explainer; animated feature-card SVG icons
- API-docs intro; reconcile feature set (drop orphan "chat", add staking)

Background:
- Layered animated aurora + WebGL frosted-marble hero (cursor parallax,
  domain-warped veins); reduced-motion-safe

Fixes:
- Footer/min-h-screen flex-shrink overlap in RootLayout
- Extract GovernanceTypeChip to a shared, lightweight module

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Detect installed wallet providers; when none are present, lead with a
  friendly "Get Started with Multisig using UTXOS" CTA (with an escape hatch
  to open the full picker) so non-crypto users don't hit a wallet wall.
- When a wallet extension is detected, keep the normal flow but stop
  recommending UTXOS specifically: drop the "Recommended" badge/tint and the
  homepage "New to crypto? Try UTXOS" box.
- Redesign the connect dropdown — cleaner UTXOS card (brand tile, network
  toggle + connect row) and wallet rows that show each wallet's real icon.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wallet-aware connect CTA + polished wallet/UTXOS connector
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>
Reliable IPFS, rationale drafting/caching, and ballot CSV
The last open Dependabot alert (brace-expansion <=2.0.1, GHSA-v6h2-p8h4-qcjw,
ReDoS) was a copy bundled inside npm@9.9.4, which @cardano-sdk/crypto pulls in
via `npm@^9.3.0`. That npm dependency is spurious — @cardano-sdk/crypto declares
it but never imports it in its compiled code — so it only drags in npm's vendored
dependency tree (including the vulnerable brace-expansion and the noise behind
`npm audit`'s large count).

A `brace-expansion` override can't fix this: npm overrides don't reach into a
bundled package's vendored node_modules, and a blanket brace-expansion override
instead downgrades the legitimate brace-expansion@5.x that glob@11 / minimatch@10
require (named `expand` export), breaking glob.

Override `npm` to ^11 instead (resolves to 11.17.0), whose bundle ships the
patched brace-expansion@5.0.6. Net effect:
- zero vulnerable brace-expansion instances remain in the tree
- glob / minimatch still resolve and brace-expand correctly
- safe because @cardano-sdk/crypto never executes npm

The lockfile was regenerated with npm 10.9.3 (matching CI's node:22-slim, not the
local npm 11) via `npm install --package-lock-only`, so it stays in sync for the
`npm ci` CI runs — npm 11 drops @auth/core's optional @simplewebauthn entries that
npm 10 keeps, which made `npm ci` fail. Churn is limited to the npm subtree.

The other 14 Dependabot alerts were already remediated in the committed lockfile
(next@16, form-data@4.0.5, pbkdf2@3.1.5, ip/tar-fs absent, etc.) and dismissed.

Verified: `npm ci` (npm 10.9.3) succeeds; tsc --noEmit clean; next build --webpack succeeds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
deps: override npm to ^11 to clear bundled brace-expansion ReDoS
fix(db): cap pg pool + stop retrying pool saturation (Supabase EMAXCONNSESSION)
…urrent-vote UX

Redesign the proposals list into cards (replacing the desktop table) and surface
the "current voting state" two ways, per the requested mockup:

1. Live Yes/No/Abstain tally bar per proposal.
   - Blockfrost exposes no aggregate and no voting power, so the tally is counted
     by distinct voter (each voter's latest vote) from the proposal votes feed,
     labeled "by votes" — not stake-weighted.
   - Cached in the DB (new ProposalTally model) via a read-through tRPC router
     (governance.getProposalTallies / refreshProposalTally). The list reads the
     cache; refreshes are driven by user activity (on load when stale/missing,
     and on expand). A 10-min TTL guards both client and server so redundant
     activity doesn't re-hit Blockfrost.

2. The wallet's own vote as a "Voted Yes/No/Abstain" pill next to the type chip.

Also refines VoteButton: the segmented control no longer pre-selects Abstain
(it starts unselected and adopts the on-chain vote when known), the primary
button is disabled until a choice is made and its label reflects intent
(Vote / Change to / Re-submit, + Proxy), and vote()/voteProxy() guard against an
empty selection.

Backend: ProposalTally model + migration; governanceRouter registered in root.

NOTE: requires `prisma migrate deploy` (creates "ProposalTally") before the
tally router works in production.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(governance): proposal cards + DB-cached live vote tallies + current-vote UX
@vercel

vercel Bot commented Jun 14, 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 14, 2026 3:17pm

Request Review

Comment thread src/lib/ipfs.ts
let s = input.trim();
if (!s) return null;
if (s.startsWith("ipfs://")) s = s.replace(/^ipfs:\/\/(ipfs\/)?/i, "");
const m = s.match(/\/ipfs\/(.+)$/i);
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