Description: useWalletStore.connect(), useAcceptIntent, and useSolverRegistration have no cooldown after a failed attempt — a user (or a malicious script running in a compromised extension/page context) can trigger rapid repeated Freighter signature popups or repeated backend submission attempts with no client-side rate limiting beyond the in-flight isConnecting/acceptingId guards, which only prevent concurrent calls, not rapid sequential retries.
Problem Statement & Context: Rapid repeated wallet-popup triggering is both a poor UX (popup spam) and a mild security-hygiene concern (training users to reflexively approve repeated prompts, a known vector for social-engineering signature approval). Similarly, unthrottled retries against acceptIntent/registerSolver add unnecessary load and noise to the backend relay.
Scope & Acceptance Criteria:
- Add a short cooldown (e.g. a few seconds, tunable) after a failed
connect(), accept(), or register() attempt before the corresponding action can be retried, with the UI clearly reflecting the cooldown (e.g. a disabled button with a visible countdown) rather than silently swallowing rapid clicks.
- The cooldown must not degrade the legitimate happy-path retry experience beyond a few seconds' delay.
- Out of scope: any server-side rate limiting (that's a
vortex-backend concern) — this issue is scoped to client-side throttling only.
Implementation Guidelines:
- Key files:
src/store/wallet.ts, src/hooks/useAcceptIntent.ts, src/hooks/useSolverRegistration.ts, src/components/ConnectWalletButton.tsx, src/app/solve/SolvePageClient.tsx.
- A small shared
useCooldown(ms)-style hook is a reasonable, reusable shape for all three call sites rather than three bespoke implementations.
- Edge cases: cooldown state must reset appropriately after a successful attempt (no cooldown needed there), and must not persist across page reloads (in-memory only is fine, no
localStorage needed).
- Testing: unit test the cooldown hook in isolation with
vi.useFakeTimers(); extend ConnectWalletButton.test.tsx, useAcceptIntent.test.ts, and useSolverRegistration.test.ts to assert a rapid second attempt during the cooldown window is a no-op (and ideally surfaces a "please wait" toast rather than silently doing nothing).
Definition of Done:
- Code written, tested.
- Acceptance criteria met.
- PR passes CI.
- Reviewed and approved.
Resources: src/store/wallet.ts, src/hooks/useAcceptIntent.ts, src/hooks/useSolverRegistration.ts
Complexity: High (200 points)
Description:
useWalletStore.connect(),useAcceptIntent, anduseSolverRegistrationhave no cooldown after a failed attempt — a user (or a malicious script running in a compromised extension/page context) can trigger rapid repeated Freighter signature popups or repeated backend submission attempts with no client-side rate limiting beyond the in-flightisConnecting/acceptingIdguards, which only prevent concurrent calls, not rapid sequential retries.Problem Statement & Context: Rapid repeated wallet-popup triggering is both a poor UX (popup spam) and a mild security-hygiene concern (training users to reflexively approve repeated prompts, a known vector for social-engineering signature approval). Similarly, unthrottled retries against
acceptIntent/registerSolveradd unnecessary load and noise to the backend relay.Scope & Acceptance Criteria:
connect(),accept(), orregister()attempt before the corresponding action can be retried, with the UI clearly reflecting the cooldown (e.g. a disabled button with a visible countdown) rather than silently swallowing rapid clicks.vortex-backendconcern) — this issue is scoped to client-side throttling only.Implementation Guidelines:
src/store/wallet.ts,src/hooks/useAcceptIntent.ts,src/hooks/useSolverRegistration.ts,src/components/ConnectWalletButton.tsx,src/app/solve/SolvePageClient.tsx.useCooldown(ms)-style hook is a reasonable, reusable shape for all three call sites rather than three bespoke implementations.localStorageneeded).vi.useFakeTimers(); extendConnectWalletButton.test.tsx,useAcceptIntent.test.ts, anduseSolverRegistration.test.tsto assert a rapid second attempt during the cooldown window is a no-op (and ideally surfaces a "please wait" toast rather than silently doing nothing).Definition of Done:
Resources:
src/store/wallet.ts,src/hooks/useAcceptIntent.ts,src/hooks/useSolverRegistration.tsComplexity: High (200 points)