Skip to content

fix(relayer,dashboard): relay a gasless vote once; no 500 or error toast after it lands - #2171

Merged
alextnetto merged 4 commits into
devfrom
fix/relayer-vote-revert-500
Oct 21, 2026
Merged

alextnetto merged 4 commits into
devfrom
fix/relayer-vote-revert-500

Conversation

@alextnetto

@alextnetto alextnetto commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

What happened (2026-10-20, ens.gov.blockful.io)

Gateful HTTP logs for POST /ens/relay/vote:

Time (UTC) Status Duration
13:33:52 200 357 ms
13:34:28 500 41 ms
13:35:51 500 42 ms
  • The first call relayed the vote, mined 7 s later in tx 0x06ce9a462cbcb608289a71005b48de0a4d3e768281eb5713225e4fa23c35e12d (voter 0x76A6…BbB8, FOR on "Activate ENSv2"). The indexer already has it.
  • Then the dashboard waited for the receipt with walletClient.extend(publicActions), so it went through the user's wallet provider for a transaction that wallet never sent. That wait failed.
  • The single catch in voteOnProposal treated the failure like a relay error and showed "Something went wrong. Try again later". The modal put the ballot back.
  • The user clicked again twice. A wallet signs the same Ballot deterministically, so each click replayed the same signature. The relayer's simulation reverted with "vote already cast", which it answered as an unhandled 500. Gateful counts those against the relayer:ens circuit breaker.

The modal also had other replay paths:

  • Opening it reset isLoading/isSuccess, and Cancel stayed visible during submission, so closing mid-request and reopening showed the ballot while the first call was still in flight.
  • The page kept offering "Vote" until the indexer caught up.

Fix

Relayer

  • Simulation reverts in relayVote/relayDelegation now return the existing SIMULATION_FAILED 409, not 500. An RPC failure is still a 500.
  • The revert helpers now live in a shared module, services/chain/revert.ts.
  • The vote and delegate routes document the 409.

Dashboard: one relay call per vote

  • relayGaslessVote, modeled on relayGovernanceAction:
    1. Reads hasVoted on-chain before asking for a signature. If the vote already exists, it doesn't sign or relay.
    2. Signs, then relays exactly once.
    3. Confirms the transaction on usePublicClient({ chainId }) for the DAO's chain, never the wallet.
  • How each result is handled:
    • A failed receipt wait → unconfirmed.
    • An ambiguous relay failure (5xx with no code, a timeout, a network error) → unknown.
    • The relayer's "vote already cast" → already-voted.
    • None of these shows an error toast. Only a refusal before broadcast, or a declined signature, rejects and brings the ballot back.
  • Vote state lives in the page-lifetime submissionStore, the one queue/execute already use, keyed by DAO, proposal and voter:
    • A synchronous claimVote before the first await blocks double clicks.
    • Closing, reopening or remounting the modal shows the in-flight, landed or pending vote, never the ballot.
    • New VotePendingContent screen for unconfirmed/unknown, with the explorer link and no retry.
  • Proposal page:
    • Shows "You voted" as soon as a vote has landed.
    • Polls the account's vote every 5 s until it is indexed, for up to 2 minutes.
    • Then refetches the tally and the votes table.
    • This replaces the window.location.reload() the modal used to do.
  • Duplicate GETs: the relayer balance, config and rate-limit queries get a 60 s staleTime. Every observer mount used to refetch them, which shows in the relayer logs as the same three GETs two or three times per second. The rate limit is invalidated after a relayed vote.
  • voteOnProposal now handles wallet votes only. A reverted wallet vote is no longer shown as a success.

Tests

  • Relayer:
    • Unit: 95/95.
    • e2e on an anvil fork: replayed vote, wrong nonce, expired delegation and the happy paths pass. Not run locally: "proposal no longer active" times out in its own anvil_mine setup (45,819 blocks) against a public RPC before any relayer code runs. Please confirm it in CI.
  • Dashboard:
    • Typecheck and lint are clean. Full Jest suite: 84 suites, 947 tests.
    • New tests in relayGaslessVote.test.ts cover the incident (200, then a failed receipt wait → unconfirmed, one relay call), plus the hasVoted short-circuit, ambiguous failures and "already cast".
    • New tests in voteSubmission.test.ts cover the claim, close/reopen mirroring and the outcome → state mapping.
  • Not verified in a browser. The dashboard's Jest has no DOM, and a real gasless vote needs a wallet signature. Please try one vote on the Vercel preview: vote once, close the modal with Escape, reopen it, and check that the header shows "You voted".

Note

Medium Risk
Changes core gasless governance voting and relayer error semantics; behavior is heavily tested but affects vote submission, indexing UX, and circuit-breaker-sensitive 5xx vs 409 handling.

Overview
Fixes gasless voting replay and false failures after a successful relay by tightening relayer errors and reshaping the dashboard vote flow.

Relayer: On-chain simulation failures for castVoteBySig / delegateBySig (duplicate vote, bad nonce, expired sig, etc.) now return 409 SIMULATION_FAILED instead of 500, with 409 documented on the relay vote/delegate routes. Revert parsing is shared via revert.ts.

Dashboard: Gasless voting moves to relayGaslessVote: optional hasVoted before signing, a single relay call, and receipt confirmation on the DAO chain (usePublicClient), not the wallet. Ambiguous relay failures and receipt timeouts become pending/unconfirmed outcomes—no error toast and no ballot replay. VotingModal uses a page-lifetime submissionStore (claimVote / settleVote) plus new already voted and pending screens so state survives close/reopen. The proposal page shows You voted from landed submission, polls until the submitted tx is indexed, and refetches tally/votes. voteOnProposal is wallet-only; relayer config queries get 60s staleTime; rate limits invalidate after relayed vote/delegation. mapRelayerError handles SIMULATION_FAILED (including “already voted”).

Reviewed by Cursor Bugbot for commit 913ba03. Configure here.

…IMULATION_FAILED

castVoteBySig and delegateBySig simulations that reverted reached the global
handler as 500 INTERNAL. A second click on Vote re-signs the same ballot byte
for byte, so after a successful gasless vote the replay reverted with
"vote already cast" and the user got a 500, which Gateful also counts against
the relayer circuit breaker.

Reverts now map to the existing SIMULATION_FAILED (409) contract that the
dashboard already reads as pre-broadcast. A simulation that could not run
(RPC down) still surfaces as 500. The revert helpers move from the governor
gateway to services/chain/revert.ts so both paths share them.
…odal close

Map the relayer's SIMULATION_FAILED to clear copy (already voted, or would
revert and was not sent) instead of "Something went wrong. Try again later",
which invited the same failing retry.

Escape and the mobile drawer closed the voting modal without the reload the
X button and backdrop already did after a successful vote, so the page kept
offering Vote and a second click replayed the same signature.
@vercel

vercel Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
anticapture-storybook Ready Ready Preview Oct 21, 2026 1:27pm UTC
1 Skipped Deployment
Project Deployment Actions Updated
anticapture Ignored Ignored Oct 21, 2026 1:27pm UTC

Request Review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-21T13:32:10.826255Z 913ba03 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@railway-app

railway-app Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

🚅 Deployed to the anticapture-pr-2171 environment in anticapture-infra

Service Status Web Updated
uniswap-indexer-offchain ✅ Success (View Logs) Oct 20, 2026 at 7:43 pm UTC
shutter-indexer-offchain ✅ Success (View Logs) Oct 20, 2026 at 7:42 pm UTC
ens-indexer-offchain ✅ Success (View Logs) Oct 20, 2026 at 7:42 pm UTC
nodeful 🚨 Crashed (View Logs) Oct 20, 2026 at 7:39 pm UTC
compound-indexer-offchain ✅ Success (View Logs) Oct 20, 2026 at 7:38 pm UTC
gitcoin-indexer-offchain ✅ Success (View Logs) Oct 20, 2026 at 7:33 pm UTC
prometheus ✅ Success (View Logs) Oct 20, 2026 at 2:57 pm UTC
grafana ✅ Success (View Logs) Web Oct 20, 2026 at 2:54 pm UTC
authful ✅ Success (View Logs) Web Oct 20, 2026 at 2:04 pm UTC
tempo ✅ Success (View Logs) Oct 20, 2026 at 2:04 pm UTC
alertmanager ✅ Success (View Logs) Web Oct 20, 2026 at 2:03 pm UTC
otelcol ✅ Success (View Logs) Oct 20, 2026 at 2:03 pm UTC
loki ✅ Success (View Logs) Oct 20, 2026 at 2:03 pm UTC
gateful ✅ Success (View Logs) Web Oct 20, 2026 at 2:03 pm UTC
docs ✅ Success (View Logs) Web Oct 20, 2026 at 2:01 pm UTC
mcp ✅ Success (View Logs) Web Oct 20, 2026 at 2:01 pm UTC
ens-api ✅ Success (View Logs) Oct 20, 2026 at 2:01 pm UTC
nouns-api ✅ Success (View Logs) Oct 20, 2026 at 1:58 pm UTC
gitcoin-api ✅ Success (View Logs) Oct 20, 2026 at 1:57 pm UTC
lil-nouns-api ✅ Success (View Logs) Oct 20, 2026 at 1:57 pm UTC
obol-indexer ✅ Success (View Logs) Oct 20, 2026 at 1:57 pm UTC
address-enrichment ✅ Success (View Logs) Web Oct 20, 2026 at 1:57 pm UTC
uniswap-indexer ✅ Success (View Logs) Oct 20, 2026 at 1:57 pm UTC
shutter-api ✅ Success (View Logs) Oct 20, 2026 at 1:57 pm UTC
aave-indexer ✅ Success (View Logs) Oct 20, 2026 at 1:57 pm UTC
lil-nouns-indexer ✅ Success (View Logs) Oct 20, 2026 at 1:57 pm UTC
scroll-indexer ✅ Success (View Logs) Oct 20, 2026 at 1:57 pm UTC
fluid-indexer ✅ Success (View Logs) Oct 20, 2026 at 1:57 pm UTC
ens-indexer ✅ Success (View Logs) Oct 20, 2026 at 1:57 pm UTC
obol-api ✅ Success (View Logs) Oct 20, 2026 at 1:57 pm UTC
uniswap-api ✅ Success (View Logs) Oct 20, 2026 at 1:57 pm UTC
tornado-indexer ✅ Success (View Logs) Oct 20, 2026 at 1:57 pm UTC
tornado-api ✅ Success (View Logs) Oct 20, 2026 at 1:57 pm UTC
nouns-indexer ✅ Success (View Logs) Oct 20, 2026 at 1:57 pm UTC
compound-indexer ✅ Success (View Logs) Oct 20, 2026 at 1:57 pm UTC
gitcoin-indexer ✅ Success (View Logs) Oct 20, 2026 at 1:57 pm UTC
shutter-indexer ✅ Success (View Logs) Oct 20, 2026 at 1:57 pm UTC
compound-api ✅ Success (View Logs) Oct 20, 2026 at 1:57 pm UTC
fluid-api ✅ Success (View Logs) Oct 20, 2026 at 1:57 pm UTC
aave-api ✅ Success (View Logs) Oct 20, 2026 at 1:57 pm UTC
ens-relayer ✅ Success (View Logs) Oct 20, 2026 at 1:57 pm UTC
erpc ✅ Success (View Logs) Oct 20, 2026 at 1:56 pm UTC
user-api ✅ Success (View Logs) Web Oct 20, 2026 at 1:54 pm UTC
scroll-api ✅ Success (View Logs) Oct 20, 2026 at 1:53 pm UTC

@railway-app
railway-app Bot temporarily deployed to anticapture-infra / anticapture-pr-2171 October 20, 2026 13:50 Destroyed

Copy link
Copy Markdown
Collaborator

🎨 UI Review

Automated review · ℹ️ Reviewed without a Figma reference — grounded in the diff + UX-expert evaluation. Vercel preview is auth-protected and no Figma/ClickUp spec was found, so visual claims are limited.


Voting modal — error copy

  • "You have already voted on this proposal." is clear and actionable copy. But the modal stays on the vote step with the Vote button still enabled, so the user can click again and hit the same error. After this message, swap the CTA for a close/"Done" action (or disable Vote) so the dead end is visible. (VotingModal.tsx) [UX-heuristic]
  • Delegation/vote fallback: "This delegation would revert on-chain, so it was not sent. No gas was spent." leaks dev jargon ("revert", "on-chain") into user-facing copy. Use plain language, e.g. "This delegation couldn't be completed, so nothing was sent. Try again in a moment." (shared/utils/gaslessRelayerError.ts) [UX-heuristic]
  • "No gas was spent." is noise on a gasless flow — the user never pays gas. Drop the sentence. [UX-heuristic]

Mobile

  • handleClose is now also wired to the drawer's onOpenChange, so swiping down or tapping the overlay after a successful vote now force-reloads the page (previously only the X / backdrop / Escape did). An unprompted full reload on a swipe-dismiss feels like a crash on mobile. Question for author: is this intended? If so, make sure the success state stays visible until the user taps an explicit "Close" button. [UX-heuristic]

Desktop

  • No visual changes to layout, spacing or tokens in this diff; Escape/backdrop/X behavior is unchanged. [Code-only]

Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0b31859845

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2 to +3
"@anticapture/relayer": patch
"@anticapture/dashboard": patch

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add the required Gateful changeset

The new documented 409 responses in the relay controllers change the relayer OpenAPI contract that mergeRelayerPaths exposes through Gateful, but this changeset bumps only the relayer and dashboard. Add an @anticapture/gateful entry so the aggregated API contract is versioned and released consistently.

AGENTS.md reference: AGENTS.md:L121-L124

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 913ba03: the changeset now bumps @anticapture/gateful too, since the relay routes it exposes document the new 409.

Comment on lines +58 to +61
const handleClose = useCallback(() => {
onClose();
if (isSuccess) window.location.reload();
}, [onClose, isSuccess]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Route success actions through the reload handler

This handler is not actually used by every success-state exit: the success branch still passes the raw onClose callback to VoteSuccessContent, whose two links invoke it directly. For example, Ctrl/Cmd-clicking either link opens the destination in a new tab while closing the modal in the current tab without reloading, leaving the stale Vote action available and allowing the same signature to be replayed; pass handleClose into the success content as well.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Superseded by f36bb0a. The modal no longer reloads at all: the vote's state lives in the page-lifetime submission store, so every exit (success links included, even a Cmd-click) leaves the page showing the landed vote, and the ballot cannot be submitted again.

…vote as failed

Production, 2026-10-20: the first relay call answered 200 in 357 ms and the
vote was mined, but the dashboard then waited for the receipt through the
user's wallet provider. That wait failed, the shared catch showed "Something
went wrong", the ballot came back, and two more clicks replayed the same
signature at the relayer.

- relayGaslessVote (modelled on relayGovernanceAction): reads hasVoted before
  asking for a signature, relays exactly once, and confirms the transaction
  on a public client for the DAO's chain. A failed receipt wait is
  "unconfirmed", an ambiguous relay failure is "unknown", and the
  relayer's "vote already cast" is a vote that exists. None of them is an
  error toast.
- The vote's submission lives in the page-lifetime submission store used by
  queue/execute, keyed by DAO, proposal and voter. A synchronous claim blocks
  double clicks, and closing, reopening or remounting the modal mirrors the
  in-flight, landed or pending vote instead of resetting to the ballot.
- The proposal page shows "You voted" as soon as the vote landed and polls
  the account's vote until it is indexed (two minutes at most), then
  refetches the tally and votes table. This replaces the page reload.
- Relayer balance, config and rate-limit queries get a 60 s stale time; each
  observer mount used to refetch them, two or three times per second.
- voteOnProposal now covers wallet votes only.
@railway-app
railway-app Bot temporarily deployed to anticapture-infra / anticapture-pr-2171 October 20, 2026 14:03 Destroyed
@alextnetto alextnetto changed the title fix(relayer): return 409, not 500, when a gasless vote or delegation would revert fix(relayer,dashboard): relay a gasless vote once; no 500 or error toast after it lands Oct 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Vercel preview: https://anticapture-6kj76wypc-ful.vercel.app

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f36bb0ae85

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +100 to +104
export const getVoteScreen = (submission: SubmissionState): VoteScreen => {
if (canSubmitAgain(submission)) return "ballot";
if (submission.kind === "in-flight") return "in-flight";
if (submission.kind === "ambiguous") return "pending";
return "landed";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow another ballot when the DAO supports changing votes

For UNI and TORN, where daoOverview.rules.changeVote enables a “Change your vote” action, any successful vote in the current page session leaves this submission permanently mapped to "landed". Nothing clears that state after the updated vote is indexed, so clicking “Change your vote” only reopens the success screen and cannot submit another ballot until the user reloads the page.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 913ba03. canStartVote lets a landed vote start a new ballot where rules.changeVote is true (UNI, TORN). An in-flight or ambiguous vote still blocks it. The modal keeps the success screen for the run that just landed and shows the ballot on the next open.

Comment on lines +84 to +87
refetchInterval: (query) =>
votePollIntervalMs && !query.state.data?.items.length
? votePollIntervalMs
: false,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep polling until a changed vote is indexed

When a UNI or TORN voter changes an existing vote, this query already contains the old vote, so items.length is nonzero and the polling interval is disabled immediately. The proposal effect consequently treats that old item as indexed and performs only an early invalidation that can race the indexer, leaving the old choice and tally displayed until an unrelated refetch or page reload; the stop condition needs to distinguish the newly submitted vote from the pre-existing one.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 913ba03. isSubmittedVoteIndexed treats the vote as indexed only when the latest indexed vote's transactionHash is the submitted one, so polling and the tally refresh wait for the changed vote rather than stopping on the one it replaced. With no known hash (already voted), any indexed vote counts.

Comment on lines 134 to 138
const { data, isLoading } = useGetRateLimit(
toDaoKey(daoId) as GetRateLimitPathParamsDaoEnumKey,
address ?? zeroAddress,
{ query: { enabled } },
{ query: { enabled, staleTime: RELAYER_QUERY_STALE_MS } },
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Invalidate the cached delegation allowance after relaying

After a successful gasless delegation, the relayer has decremented the address's delegation allowance, but DelegationModal only refetches the on-chain delegate and never invalidates this query. With the new one-minute staleTime, a user who immediately delegates again—especially after using their final allowance—still sees the action as free and is automatically sent through the relayer, where it fails with 429 instead of falling back to the wallet until this cache becomes stale.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 913ba03: DelegationModal invalidates the rate-limit query after every gasless delegation attempt, so the allowance is refetched before the next one.

Comment on lines +75 to +78
switch (outcome.status) {
case "success":
case "already-voted":
return { kind: "done", outcome: "landed", hash: outcome.hash };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not attribute the selected choice to an existing vote

When hasVoted or the relayer reports already-voted, no new ballot was cast and the code does not know the support of the existing on-chain vote. Nevertheless, this maps the outcome to a landed submission while retaining the just-selected support from claimVote, so submittedVoteSupport can make the header say, for example, “You voted Against” even though the pre-existing vote was For; keep the action suppressed without exposing the attempted choice as the recorded one until indexing supplies the actual support.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 913ba03. An already-voted outcome clears the attempted choice (alreadyVoted: true, support: null), so the header shows nothing until the indexer supplies the real vote, and the modal shows a dedicated already-voted screen.

- Votes can be changed again on UNI and TORN: a landed vote reopens the
  ballot where the DAO allows changing votes, while an in-flight or
  ambiguous one still blocks a second submission.
- "Indexed" now means the submitted transaction itself, so polling keeps
  going while the API still returns the vote a change replaced.
- An "already voted" answer no longer shows the attempted choice as the
  vote on record; the modal ends on its own screen with a Close button.
- A gasless delegation invalidates the cached delegation allowance, so the
  next one does not hit the relayer's 429 on a stale count.
- Plain-language copy for a relay the governor would refuse.
- Gateful changeset: the relay routes it exposes now document the 409.
@alextnetto

Copy link
Copy Markdown
Member Author

Thanks for the UI review. Addressed in 913ba03 and f36bb0a:

  • Dead end after "already voted": the modal now ends on a dedicated screen ("You have already voted on this proposal") with a Close button. No ballot and no Vote button.
  • Jargon: the fallback is now "This vote/delegation couldn't be completed, so nothing was sent.", without "revert", "on-chain" or "No gas was spent".
  • Mobile reload on swipe-dismiss: removed. The modal no longer reloads the page on any close. The vote's state lives in a page-lifetime store, and the page polls until the vote is indexed.

@alextnetto
alextnetto merged commit a2f82f1 into dev Oct 21, 2026
58 of 59 checks passed
@alextnetto
alextnetto deleted the fix/relayer-vote-revert-500 branch October 21, 2026 13:27
@railway-app
railway-app Bot temporarily deployed to anticapture-infra / anticapture-pr-2171 October 21, 2026 13:27 Destroyed
@alextnetto alextnetto mentioned this pull request Oct 21, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 913ba03a8c

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const reader = {
readContract,
waitForTransactionReceipt,
} as unknown as VoteChainReader;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove the unsafe casts from the vote test doubles

Type makeReader and the nearby makeSigner test double directly against their interfaces instead of coercing incomplete objects through unknown. These casts bypass the compiler, so changes to viem's overloaded methods or the production wrapper interfaces can leave these tests compiling while exercising an incompatible fake, and the repository explicitly prohibits casts through unknown.

AGENTS.md reference: AGENTS.md:L133-L133

Useful? React with 👍 / 👎.

request: {},
result: undefined,
})) as unknown as ChainReader["simulateContract"],
simulateContract: simulate as unknown as ChainReader["simulateContract"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Type the simulated chain reader without an unknown cast

Replace this as unknown as coercion with a correctly typed ChainReader stub. The cast suppresses validation of the simulated method's parameters and result, allowing the relay tests to keep compiling when the real interface changes even though the fake no longer models it, and it violates the repository's explicit ban on casting through unknown.

AGENTS.md reference: AGENTS.md:L133-L133

Useful? React with 👍 / 👎.

Comment on lines +335 to +342
if (outcome.hash) {
void queryClient.invalidateQueries({
queryKey: getRateLimitQueryKey(
daoId.toLowerCase() as GetRateLimitPathParamsDaoEnumKey,
address,
),
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Invalidate the vote allowance after every relay attempt

When the relayer returns already-voted, another SIMULATION_FAILED, or an ambiguous failure, there is no hash (or the call throws), so this branch skips invalidation. The relayer has already called assertWithinLimit before simulation, and that operation increments the monthly count; consequently, an attempt that consumes the user's final slot can leave the new 60-second cache reporting another proposal as free, only for its submission to fail with 429. Invalidate the rate-limit query whenever a signed request was made, not only when a hash was returned.

Useful? React with 👍 / 👎.

This branch was successfully deployed

1 active deployment
anticapture-infra / anticapture-pr-2171 — 913ba03a Deployed Oct 21, 2026 by railway-app[bot]
Preview – anticapture-storybook — 913ba03a Deployed Oct 21, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants