feat(dashboard): queue and execute proposals through the relayer (gasless) - #2155
Conversation
…less) Offer the relayer-sponsored path for queue()/execute() when the DAO has a funded relayer, gated by proposal state (succeeded / pending_execution), with the existing wallet flow kept as fallback. Relayer error codes are mapped to specific messages, the tx hash is shown on success, and the proposal queries are invalidated instead of reloading the page. Refs DEV-1168 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
|
🚅 Deployed to the anticapture-pr-2155 environment in anticapture-infra
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 55af56b0fc
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
🎨 UI Review
GovernanceActionModal — reopening during an in-flight action
Validated — reused existing patterns, no change needed
Generated by Claude Code |
|
🔍 Vercel preview: https://anticapture-kzc12nhb9-ful.vercel.app |
- Treat relayer failures without a pre-broadcast rejection code (gateway timeout, plain 5xx, network error) as an unknown submission state with no retry, since the transaction may already have been broadcast. - Stop reading a bare 503 as the relayer being out of funds. - Poll the proposal after a submission until the indexed status advances. - Offer relayed queue/execute to disconnected visitors; the wallet path still asks for an address. - Tag each attempt so a submission abandoned by closing the modal cannot repaint a reopened one. - Narrow the receipt dependency to a ReceiptWaiter type and drop the unknown cast from the test double. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Re the UI review point on reopening during an in-flight action: addressed in 151e91e. Every run is tagged with an attempt id and closing the modal bumps it, so a submission abandoned by closing can no longer set the step, error or tx hash of a reopened modal. A late success still fires the toast and refreshes the proposal (the chain did change), but only the modal's own screen is left alone. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 151e91e692
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 17534b6805
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…failure When the relayer call ended without a definitive answer the modal only showed a warning and "Done": the user had to close and reopen to reach the wallet flow, and nothing said so. The user must never be locked out of the action, so the unknown state now offers "Use my wallet anyway" next to Close, and the copy states the risk of a duplicate (a second transaction fails on-chain and its gas is paid) while the proposal status keeps being polled. No free retry is offered, since that could race the first send. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… feat/gasless-queue-execute
A proxy, CDN or crashed upstream can answer a 5xx with an HTML or plain-text page, which the client stores as a string. `"code" in data` then threw a TypeError that escaped the unknown outcome and reached the modal as a definitive error, offering the wallet retry although the original request may already have broadcast. The body is now only inspected when it is an object; anything else carries no relayer code and stays unknown. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b05332a00e
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…issed Closing the modal cleared the busy flag, so a user could reopen it while the relayer request was still in flight and pick "Use my wallet". The wallet transaction then raced the relayed one and the loser paid gas for a revert. The guard is now released by the request settling, never by the close, and a modal reopened on top of a submission shows it in progress instead of the action choices. The dismissed run keeps its screen and paints its own result there, which makes the attempt id redundant: no second run can start while one is in flight, so it is gone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 546bf4251f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Once the wallet had broadcast, a rejected waitForTransactionReceipt fell into the generic error step, which offers the relayer retry and could send the same governance call a second time while the first was still pending. The submit helpers now hand the hash to the caller at broadcast time, and a receipt failure after that point becomes the unconfirmed step: the hash and its explorer link are kept, no retry is offered, and the proposal is polled just as it is on success. The guard is held rather than released whenever a transaction exists whose receipt was never read, on the relayed path too. Pre-broadcast failures, a rejected signature or a simulation revert, keep the error step with its retry. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 12505eb224
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
A lost eth_sendTransaction response looks exactly like a send that never happened, so classifying by what failed was backwards: writeContract rejecting with no hash was read as pre-broadcast and offered a retry that could duplicate the governor call. The submit helper now announces the send attempt before making it and hoists it to one place after simulation, and a single classifier calls a failure retryable only when it is positively proven so: a wallet rejection, a viem error raised before the request leaves the client, or anything that failed before the attempt. Guard and outcome are now one state, idle, in-flight, ambiguous or done, with one pure function deciding the entry screen from it. An ambiguous outcome is terminal for the page session, so a dismissed relayer request that resolves as unknown while the modal is closed no longer falls through to the choices on reopen. The unconfirmed and unknown screens collapse into one that shows the explorer link only when a hash exists and offers no submission buttons. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tryable set The state lived in the component, so navigating away from the proposal and back reset it to idle while the previous mount's request was still unresolved, and the button that was still on screen could send a second governor call. It now lives in a module-level store keyed by DAO, proposal and action, which the modal reads synchronously and subscribes to, so a request that outlives its mount still blocks the next one and still reports how it ended. Relayer failures are classified in one place from the codes the relayer actually defines plus the status: every structured code but a reported revert means nothing was signed, as does any 4xx from the relayer or the gateway, and only a 5xx with no such code or a transport failure stays ambiguous. Six more viem errors join the wallet pre-send set, a reverted relay now shows the hash it names in its message, polling stops early when the proposal moves out of the action's reach, and the budget is the 24 refetches its comment promises. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A mount that inherited a settled submission fell through to the action choices, so a user who navigated away and back right after a relayed queue landed on a live "Queue for free" button with no polling behind it, the previous mount having taken its watch with it. The settled state now records whether a transaction went out, along with its mode and hash, and a mount that did not run it shows the transaction with no action button and picks the watch back up. It claims neither success nor revert, which the state does not record and that mount never saw. The store drops its oldest settled entries past fifty, and never an unresolved one, since those are what stop a duplicate. The poll budget starts fresh on an inherited watch rather than carrying across mounts. The relayer error list now points at the dashboard set that mirrors it, and the classifier says why an unrecognised failure defaults to ambiguous. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2758a2dcce
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
… outcome The inherited ambiguous branch restored the warning screen but never restarted polling, so its promise that the page keeps checking was false and a transaction that landed left the status stale. It now shares one inherit and watch path with the settled branch, guarded by a predicate that says which states leave a transaction unwatched. Recording an ambiguous outcome no longer starts the watch itself, so the screen and the watch come from the same place for every mount rather than only for the one that ran the submission. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ac686091e2
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
The entry point let a remounted modal fall through to the choices while the store said a transaction had landed and was waiting to be indexed, putting the stale action back on screen and contradicting the watch it had just restarted. Entry point, close handler and both action buttons now ask one predicate, canSubmitAgain, and everything it refuses keeps the screen already describing the submission. Settled runs record what they did rather than whether they sent something: a mined revert changed nothing, so it stays retryable and needs no watch, which a plain "sent" flag would have got wrong in both directions. That leaves the predicate and the watch as exact complements, which a test asserts over every state. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8d743e916c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…ath when Gateful refused Three follow-ups from a regression pass over yesterday's commits: - The store cap evicted any settled entry, landed ones included, although a landed transaction is what keeps the action closed until it is indexed. Failed attempts are now evicted first and a landed entry only when no failed one is left, so the map stays bounded without reopening the action. - A mount inheriting a landed transaction painted it with the ambiguous screen, which says the transaction is not confirmed. It is, so it now gets the success screen and the same proposal watch. - Gateful's open circuit answers 503 before proxying anything, so nothing was sent. It was classified ambiguous and locked the user out of the wallet path until a reload; it is now pre-broadcast like the other refusals. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Summary
Implements DEV-1168: let users queue and execute proposals from the governance UI with the relayer paying gas, keeping the wallet flow as fallback.
The SDK already exposed
relayQueue/relayExecute, so this is dashboard-only.Changes
relayGovernanceAction: state gating (queue only onsucceeded, execute only onpending_execution) and the relayer call. Returnssuccess/reverted/unconfirmedso a broadcast tx is never reported as "not submitted" and a reverted receipt is never reported as success.useGaslessEnactment: relayer configured for the DAO and funded. No voting power or per-address rate limit, since queue/execute are permissionless.GovernanceActionModal: with a funded relayer, opens a choice step ("Queue/Execute for free" vs "Use my wallet"). Without a relayer, the wallet flow starts on its own as before. Onqueuedwith the timelock still running it shows a "timelock not ready" warning and disables the free button. Shows the tx hash with an explorer link and invalidates the proposal queries instead of reloading the page.mapRelayerEnactmentError: mapsINVALID_PROPOSAL_STATE,TIMELOCK_NOT_READY,SIMULATION_FAILED,TRANSACTION_REVERTED,PROPOSAL_NOT_FOUND,PROPOSAL_DATA_MISMATCH,RELAYER_LOW_BALANCEto specific copy.SIMULATION_FAILEDsays the proposal would revert on-chain and that no gas was spent, with a "Try with my wallet" fallback.@anticapture/dashboardminor.Verification
pnpm dashboard typecheck && pnpm dashboard lintpass.pnpm dashboard test: 59 suites, 586 tests passing, including 19 new tests for the gating, the relayer call outcomes, and the error mapping.POST /api/gateful/ens/relay/executefor ENS EP 6.34 returns409 SIMULATION_FAILEDand/relay/balancereportshasEnoughBalance: true, so the error path is exercisable in prod as described in the ticket.Not covered
succeededorpending_executionthat can be enacted right now. Can be validated on a fork or with the next ENS proposal.queued. Disabling both buttons in that state is a product call.🤖 Generated with Claude Code
Known limitation
The duplicate-submission guard lives in the page session (a module-level store keyed by DAO, proposal and action). Two browser tabs on the same proposal can still each submit the same action; there is no client-side fix for that, the second transaction is refused at simulation once the first is mined.
Dependency
Depends on #2162: Gateful must forward the relayer's structured error codes (for example
RELAYER_LOW_BALANCE) so the dashboard can classify them as refusals before sending. Until it merges those errors reach the dashboard as a generic 500 and are treated as ambiguous.