Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dashboard-gasless-wallet-fallback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@anticapture/dashboard": minor
---

Offer gasless votes only when the relayer's GET /relay/status answers available: true, and keep every other case on the voter's own wallet: the ballot says when gas-free voting is unavailable or a relayed attempt was refused and switches its button to "Vote with wallet", lets a voter opt out of the relayer at any time, and turns a relay the relayer never confirmed into a "Vote with my wallet instead" button once the chain shows no vote from that address. Delegation gets the same guarantee: a refused or unconfirmed gas-free delegation moves that account to its wallet with a "Delegate with my wallet" button. The fallback state follows the voter across account switches and remounts, a relayed vote whose hash never confirms can also fall back once the chain shows no vote and no pending relay, a wallet vote that finds the relayed vote already counted reports it as cast, and the relayer status refreshes every minute while a page is open
5 changes: 5 additions & 0 deletions .changeset/gateful-relayer-status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@anticapture/gateful": minor
---

Always answer GET /{dao}/relay/status with 200: pass the relayer's answer through, and answer on its behalf with available: false when it cannot, reporting RELAYER_NOT_CONFIGURED for a DAO without a relayer and RELAYER_UNREACHABLE when the relayer is down, times out, answers with anything but a valid status, or its circuit is open. The probe goes straight to the relayer, so it never trips or occupies the circuit breaker that real relay traffic depends on. It logs once when it starts answering for a relayer that cannot answer and once when that relayer is reachable again
5 changes: 5 additions & 0 deletions .changeset/relayer-gasless-status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@anticapture/relayer": minor
---

Add GET /relay/status, which answers whether gasless relaying can be used right now from every check the relayer can run on itself (pause switch, RPC reachability and chain, wallet balance, rate limiter, stuck pending transactions, repeated broadcast failures) and always answers 200 with the failed checks listed as reasons, and add the GASLESS_PAUSED kill switch, which refuses every relayed vote, delegation, queue and execute with 503 RELAYER_PAUSED before a signature is recovered or the chain is read, and keeps webhook-driven enactment from broadcasting. GASLESS_PAUSED accepts true/1/yes/on (any case) and refuses to start on a value it does not recognise, and the status is published on /metrics as relayer_gasless_available and relayer_gasless_unavailable_reason for the new GaslessRelayerUnavailable alert
Original file line number Diff line number Diff line change
@@ -1,23 +1,36 @@
import { ExternalLink, Hourglass } from "lucide-react";
import { ExternalLink, Hourglass, Wallet } from "lucide-react";
import type { Hash } from "viem";

import { InlineAlert } from "@/shared/components/design-system/alerts/inline-alert/InlineAlert";
import { Button } from "@/shared/components/design-system/buttons/button/Button";

interface VotePendingContentProps {
hash: Hash | null;
explorerBaseUrl?: string;
/**
* Offered for a relayed vote the relayer never confirmed: checks the chain
* and, if no vote from this address is there and no relayed transaction is
* still waiting to be mined, hands the ballot to the wallet. Absent, the
* screen keeps its old wait-only behaviour.
*/
onVoteWithWallet?: () => void;
isCheckingChain?: boolean;
}

const shortenHash = (hash: string) => `${hash.slice(0, 10)}…${hash.slice(-8)}`;

/**
* A vote that may be on-chain but could not be confirmed here. It offers no
* way to vote again: the wallet would sign the same ballot, and the relayer
* would only be asked to replay it.
* A vote that may be on-chain but could not be confirmed here. A relayed one
* may also never land: the relayer may not have sent it, or its transaction
* may be dropped or revert. So the voter gets a way to cast the vote from
* their own wallet rather than a screen that waits forever on a service that
* is not answering; the chain is read before that way opens.
*/
export const VotePendingContent = ({
hash,
explorerBaseUrl,
onVoteWithWallet,
isCheckingChain = false,
}: VotePendingContentProps) => (
<div className="flex flex-col items-center gap-3 px-4 py-8">
<div className="bg-surface-contrast flex size-12 items-center justify-center rounded-full">
Expand All @@ -43,10 +56,30 @@ export const VotePendingContent = ({
<InlineAlert
variant="warning"
text={
hash
? "It is not confirmed yet. Follow it on the explorer. This page shows your vote on its own once it is indexed, so there is no need to vote again."
: "The relayer did not answer clearly, so the vote may already be on its way. This page shows your vote on its own once it is indexed, so there is no need to vote again."
hash && onVoteWithWallet
? "It is not confirmed yet. Follow it on the explorer. If it never lands, you can cast your vote from your own wallet and pay the network fee."
: hash
? "It is not confirmed yet. Follow it on the explorer. This page shows your vote on its own once it is indexed, so there is no need to vote again."
: onVoteWithWallet
? "The relayer did not confirm your vote. If it is not on-chain, you can cast it from your own wallet and pay the network fee."
: "The relayer did not answer clearly, so the vote may already be on its way. This page shows your vote on its own once it is indexed, so there is no need to vote again."
}
/>

{onVoteWithWallet && (
<Button
variant="outline"
onClick={onVoteWithWallet}
disabled={isCheckingChain}
data-ph-event="vote_wallet_fallback"
data-ph-source="gov_fe"
data-umami-event="vote_wallet_fallback"
>
<Wallet className="size-3.5" />
{isCheckingChain
? "Checking the chain..."
: "Vote with my wallet instead"}
</Button>
)}
</div>
);
Loading
Loading