[pull] master from unlock-protocol:master#128
Merged
pull[bot] merged 1 commit intosearchableguy:masterfrom Mar 30, 2026
Merged
Conversation
* 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 : )