Skip to content

[pull] master from unlock-protocol:master#128

Merged
pull[bot] merged 1 commit intosearchableguy:masterfrom
unlock-protocol:master
Mar 30, 2026
Merged

[pull] master from unlock-protocol:master#128
pull[bot] merged 1 commit intosearchableguy:masterfrom
unlock-protocol:master

Conversation

@pull
Copy link
Copy Markdown

@pull pull bot commented Mar 30, 2026

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

* feat: add governance proposal write flows

* fix: pass tokenSymbol as prop instead of reading from config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: remove canConnect from ProposalWritePanel — replaced by authenticated check

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: add snapshot block range to queryFilter and use live time for canExecute

- Use proposalSnapshot() to bound queryFilter calls — avoids full chain scan
- Replace server-rendered latestTimestamp with live Date.now() for canExecute
- Show loading state while vote status fetches instead of "0 UP"
- Use governanceConfig.chainName in vote success toast

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: auto-update execute timer and show error state in vote status

- Use useState + useEffect interval to tick now every 60s while Queued
- Show 'Unavailable' + error message when voteStatusQuery fails
- Remove unnecessary fragment wrapper in early return

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: bound queryFilter to voting period and track pending vote button

- Add proposalDeadline() as toBlock in queryFilter — prevents RPC range errors
  on proposals older than the provider's block cap (~2,000 blocks)
- Track pendingSupport to show loading only on the clicked vote button
- Tighten canExecute timer interval from 60s to 15s

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: query subgraph for user vote instead of queryFilter

queryFilter over the full voting period (300k+ blocks on Base) exceeds most
RPC provider block-range caps. Query the subgraph vote entity directly using
the <proposalId>-<lowercaseAddress> key instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: use GraphQL variables in subgraph query and check tx receipt status

- Use variables: { id } instead of string interpolation to prevent query injection
- Check receipt.status === 0 to surface on-chain reverts to the user

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: throw on subgraph failure to prevent false-positive canVote

fetchVoteFromSubgraph now throws on non-200 or GraphQL errors instead of
returning null, so voteStatusQuery.isError becomes true and vote buttons
stay disabled when vote status cannot be confirmed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: disable lifecycle button during pending mutation and clean up minor issues

- Add actionMutation.isPending to Queue/Execute button disabled state
- Use cached getRpcProvider() instead of allocating a new one per query
- Remove dead receipt.status checks (ethers v6 throws on revert)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: optimistic vote status update and show tx hash in toasts

- Optimistically set query cache on vote success instead of refetching —
  subgraph lags 1-10 min behind chain state so immediate refetch returns
  stale data and re-enables vote buttons
- Include tx hash prefix in submission toasts so users can track on explorer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: toast ordering and document subgraph vote ID format

- 'submitted' toast fires before tx.wait(); 'confirmed' fires in onSuccess
- Add comment citing the subgraph createVote() source for vote ID format

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: consistent button label during timelock wait and invalidate after action

- Show 'Waiting for timelock' on button when Queued but not yet executable
- Invalidate voteStatusQuery after queue/execute so state refreshes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: prevent double-submit, clean error messages, and add subgraph timeout

- Disable Queue/Execute button after isSuccess (prevents race during refresh)
- Map ACTION_REJECTED and verbose ethers errors to clean user messages
- Add 10s AbortSignal timeout to subgraph fetch
- Remove address! assertion — add explicit null check in queryFn

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: remove actionMutation.isSuccess from lifecycle button and add support bounds check

After a successful Queue, isSuccess persists across router.refresh() since the
component does not unmount. This left the Execute button permanently disabled
once the timelock elapsed. Dropping isSuccess lets canExecute alone gate the
button per the state machine.

Also validates vote.support is in [0,1,2] before returning to guard against
unexpected subgraph values.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: use ?? for votingPower fallback and type-safe ACTION_REJECTED check

0n is falsy so || 0n would replace a real zero with 0n unnecessarily — use ??
instead. Also switch to ethers v6 isError() for the ACTION_REJECTED guard and
strip verbose RPC parenthetical data from error messages shown to users.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: render proposal markdown, fix connect button, reorganize layout, truncate IDs

- Render proposal description as Markdown using react-markdown + prose styles
- Fix "Connect wallet" button to use useConnectModal (same as header) instead
  of bare Privy login() — fixes the button doing nothing when clicked
- Move ProposalWritePanel to top of right column, Lifecycle second,
  Governance settings last; add items-start so columns align at top
- Remove Lifecycle section from left column (it's now in the right column)
- Add TruncatedId component: shows first/last 4 chars with a copy button
  that stops link navigation propagation (safe inside <Link> cards)
- Use TruncatedId in ProposalCard and proposal detail page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: 2-col layout from top, fix mobile order, human-friendly durations

Layout:
- Remove full-width header card; 2-column grid now starts at the top of <main>
- DOM order [header → aside → main] so on mobile: title shows first, then
  cast vote panel, then vote breakdown/calls (no deep scroll to find actions)
- aside uses lg:row-span-2 to span both rows on desktop, staying alongside
  both the header and the breakdown sections
- Reduce padding on mobile (p-5 sm:p-8), title (text-2xl sm:text-4xl)
- Vote breakdown always 3 columns (grid-cols-3) — panels are compact enough

Formatting:
- Add formatDuration() — converts seconds to "4d 2h 30m" style
- Use formatDuration() for voting delay and voting period in governance settings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address latest claude review on PR #16325

- TruncatedId: wrap clipboard.writeText in try/catch (fails silently in
  non-HTTPS or when permission is denied)
- ReactMarkdown: add custom link renderer with target=_blank and
  rel=noopener noreferrer (proposal descriptions are user-controlled)
- fetchVoteFromSubgraph: validate json.data exists before accessing .vote
  to catch malformed subgraph responses that have no data and no errors
- formatDuration: add comment explaining seconds are intentionally dropped
  for multi-day periods

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address four issues from latest claude review on PR #16325

- Remove router.refresh() from vote onSuccess — RSC data doesn't change
  when a vote is cast; calling refresh raced against the optimistic update
  and could overwrite it with stale subgraph data, re-enabling vote buttons
- ReactMarkdown href: only allow http/https schemes; render non-http links
  as plain <span> to block data: and other non-http schemes from on-chain
  proposal descriptions
- TruncatedId: store setTimeout ID in a ref and clear it on unmount to
  avoid updating state on an unmounted component
- formatDuration: omit seconds when hours or days are present (not just days);
  update comment to match the actual cutoff

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address three issues from latest claude review on PR #16325

- ReactMarkdown: add img component filter with same http/https allowlist as
  the a component — blocks tracking pixels, fingerprinting, and NSFW images
  from on-chain proposal descriptions
- ProposalWritePanel: guard BigInt(value || '0') to avoid SyntaxError on
  empty or non-integer strings from proposal.values
- Add comment to ProposalWritePanel early-return explaining the no-hooks
  constraint that makes the pattern safe

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: vote buttons always 3-column, small size to fit in narrow aside

sm:grid-cols-3 never activated inside the 360px aside — switched to
grid-cols-3 unconditionally and size=small so labels fit on one line.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: force-wrap long strings in proposal description on mobile

Add break-words to the prose container so hex addresses, hashes, and
other unbreakable strings in on-chain proposal descriptions wrap instead
of overflowing the viewport on narrow screens.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: hide cast vote section when voting is not available

Show the cast vote box only when state === Active or the user has
already voted (so their vote record remains visible). Hide it entirely
for Pending, Succeeded, Queued, Executed, Defeated, Canceled, etc.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: show clear revert reason for queue/execute failures

toUserMessage was stripping the revert reason via a parenthesis regex —
ethers v6 CALL_EXCEPTION errors carry a clean reason field, use it directly.
For other errors, take only the first line instead of regex-stripping.

Add a pre-flight on-chain state check before queue/execute: reads the
governor's state() view and throws a human-readable error if the on-chain
state doesn't match the expected one (catches subgraph lag mismatches
before the user wastes gas on a doomed transaction).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(governance): hide vote form when no voting power, show delegation nudge

When a connected user has zero voting power at the proposal snapshot,
replace the cast-vote section with a WalletStateCard explaining the
situation. If the user held tokens but had not delegated, include a
link to /delegates so they can participate in future proposals.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(governance): hide empty lifecycle rows, explain not-yet-queued state

Instead of showing "Not available" for Executed/Canceled rows that have
no data, hide those rows entirely. For Queued/ETA, show an actionable
message when the proposal has Succeeded but hasn't been queued yet.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(governance): hide lifecycle action when no action available; strip markdown heading from title

- Only render the Lifecycle action section when canQueue or state===Queued;
  hide it for Pending/Active/Defeated/Canceled/Expired/Executed states.
- Also hide the action button while waiting for timelock (no action to take).
- Strip leading markdown heading markers (##, #, etc.) from proposal titles
  extracted from on-chain description text.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(governance): show vote confirmation instead of disabled form; remove duplicate title

- Replace the disabled vote form with a WalletStateCard confirmation
  when the user has already voted on a proposal.
- Strip the first line of the proposal description before rendering
  markdown, since the title is already shown as a separate <h2>.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(governance): verify Succeeded/Queued proposals against on-chain state

The subgraph-derived state can disagree with governor.state() — e.g. a
proposal appears Succeeded via vote math but is Defeated on-chain due to
quorum differences, or a Queued proposal is actually Expired on-chain.

After deriving local states, fetch governor.state() for all proposals
marked Succeeded or Queued and override with the authoritative on-chain
result. Also adds Expired to ProposalState and ProposalStateBadge.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(governance): never show vote form after voting; show date + tx hash

- Replace userSupport (nullable int) with castVote (object with support,
  createdAt, transactionHash) so the voted state is unambiguous.
- Gate the vote form behind !isLoading to prevent it flashing for
  in-flight queries on Active proposals.
- When castVote is set, show a confirmation card with the vote direction,
  date, and transaction hash instead of any form.
- Fetch createdAt and transactionHash from the subgraph alongside support.
- Optimistic cache after voting includes approximate timestamp; subgraph
  provides the real values on the next fetch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(governance): link all transaction hashes to Basescan explorer

Add txExplorerUrl() helper to governance config and use it in:
- Vote confirmation card (full tx hash, links to basescan.org/tx/<hash>)
- Governance settings "Transaction" row on proposal detail page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(governance): use hasVoted() on-chain to gate vote form, not subgraph alone

The subgraph can lag by minutes after a vote is cast. Add governor.hasVoted()
to the parallel fetch — if on-chain confirms a vote exists but the subgraph
hasn't indexed it yet, return a partial castVote so the form is never shown.
Show "Vote recorded / Confirming on the subgraph…" until details arrive.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(governance): improve proposal detail UI — explorer links, Address component, quorum bar

- Vote confirmation: show "View in block explorer" link instead of raw tx hash
- Governance settings: "Proposal submission transaction" link instead of truncated hash
- Proposer chip: use @unlock-protocol/ui Address component with Basescan external link
- Quorum section: replace raw numbers with a progress bar; show "Reached" badge
  when quorum is met, otherwise show remaining votes needed
- Add addressExplorerUrl() helper to governance config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(governance): wrap Address in client component to fix server component build error

The @unlock-protocol/ui bundle lacks "use client" directives, causing
a Next.js build error when Address is imported in a server component.
Add a thin 'use client' wrapper (AddressLink) and use it in the proposal
detail page.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address review nits — disabled button, canExecute guard, regex, url validation

- Add disabled={actionMutation.isPending} to queue/execute button
- Clarify canExecute: etaSeconds !== null && BigInt(etaSeconds) > 0n
- Fix description regex to handle \r\n line endings
- Validate tx hash and address format in explorer URL helpers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address claude review — remark-gfm, image stripping, ON_CHAIN_STATE dedup, regex fix

- Add remark-gfm plugin for GFM support (tables, strikethrough, task lists)
- Strip all images in proposal markdown — user-controlled on-chain content
  poses tracking pixel risk to every visitor
- Drop http:// links (only https:// allowed) to prevent mixed-content
- Fix description regex to handle single-line descriptions (no newline):
  /^[^\r\n]*[\r\n]*/ now matches even without trailing newline
- Export ON_CHAIN_STATE from proposals.ts; remove duplicate stateLabels
  map in ProposalWritePanel.tsx
- Use BigInt(value || 0) instead of BigInt(value || '0') for numeric safety

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address claude review — conditional tx link, vote reason maxLength, canExecute comment

- Render proposal submission transaction link conditionally (not href='#' fallback)
  when txExplorerUrl returns null
- Add maxLength={1000} to vote reason TextBox with updated description text
- Add inline comment explaining canExecute uses client-side clock with on-chain
  pre-flight as the authoritative guard
- Add inline comment explaining description first-line strip to avoid confusion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address claude review — anchor links, TruncatedId label prop, setInterval cleanup

- Allow #anchor links in ReactMarkdown (they were blocked by https-only filter)
- Add label prop to TruncatedId (default 'Copy full ID'); callers pass
  'Copy full proposal ID' explicitly — prevents stale label if reused for hashes
- Stop setInterval once canExecute is true (ETA has passed) to avoid
  indefinite 15s ticks after the Execute button appears

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: anchor links should not open in a new tab

#anchor links were getting target=\"_blank\" which opened a new tab
where the fragment couldn't resolve. Now only https:// external links
get target=\"_blank\"; #anchor links stay in the same tab.

Also add comment explaining why blocked http:/etc links render as plain
text without an error indicator.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
@pull pull bot locked and limited conversation to collaborators Mar 30, 2026
@pull pull bot added the ⤵️ pull label Mar 30, 2026
@pull pull bot merged commit 0fa41de into searchableguy:master Mar 30, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant