Dispute initiation flow (V2-FE-114): The canonical dispute initiation surface follows this state model. It reads dispute eligibility and evidence from the API projection, constructs the dispute transaction only from canonical contract ABI/address artifacts, and never fabricates calldata, gas estimates, transaction hashes, confirmations, or settlement. See §3 for its surface-specific projection.
Companion to: THREAT_MODEL.md Rule: Every surface that shows a transaction, reward, settlement, reputation, or protocol state must render every applicable state below. No surface may jump directly from
pendingtofinalized.
This document defines the canonical, deterministic UI state model for the TruthBounty frontend. It exists so that:
- Uncertainty is visible, never hidden.
- Degraded states are accessible, responsive, and recoverable.
- The frontend never fabricates transaction success, settlement, rewards, reputation, or protocol state.
The state model is derived from the actual hooks in this repo (useTransactionMachine, useReceiptProjection, useFinalizationDetection, useReorgReconciliation, useSettlementDetection, useStateReconciliation) — it is not aspirational.
| # | State | Definition | User-visible meaning |
|---|---|---|---|
| 1 | loading |
Fetch or transaction in flight; no result yet. | "Loading…" |
| 2 | empty |
Fetch succeeded; no records. | "Nothing here yet." |
| 3 | stale |
Data exists but is beyond its freshness bound. | "Last updated …" |
| 4 | rejected |
User rejected in the wallet. | "You rejected the request." |
| 5 | failed |
Chain/API error; no fabrication permitted. | "Something went wrong." |
| 6 | pending |
Submitted to the mempool, awaiting inclusion. | "Pending confirmation…" |
| 7 | confirmed |
Included in a block; not yet final. | "Confirming…" |
| 8 | finalized |
Finalized per chain finality rules. | "Settled." |
| 9 | reorged |
Previously confirmed block was reorged out. | "Chain reorganized; refreshing…" |
│ loading │ └────┬─────┘ │ (user submits) ▼ ┌──────────▶ rejected (user rejected in wallet) │ │ ┌──────────┐ └───────────│ pending │ └────┬─────┘ │ (included in block) ▼ ┌───────────┐ │ confirmed │ └────┬──────┘ │ (finality reached) ▼ ┌───────────┐ │ finalized │ └───────────┘ ▲ │ (reorg detected) │ ┌───────────┐ │ reorged │ ──▶ (reconcile → back to pending/confirmed or failed) └───────────┘
Forbidden transitions:
pending→finalized(must go throughconfirmed)failed→finalizedreorged→finalizedwithout re-confirmation- Any state → success without a receipt
loading → empty | data | stale | failed data → stale | failed stale → data (on successful refetch) | failed failed → loading (on retry) | data (on recover)
Every user-facing surface that touches protocol state must support the applicable states.
| Surface | loading | empty | stale | rejected | failed | pending | confirmed | finalized | reorged |
|---|---|---|---|---|---|---|---|---|---|
| Claim submission | ✅ | — | — | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Claim list | ✅ | ✅ | ✅ | — | ✅ | — | — | — | — |
| Claim detail | ✅ | — | ✅ | — | ✅ | — | — | — | — |
| Verification submission | ✅ | — | — | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Dispute submission | ✅ | — | — | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Dispute initiation | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Reward claim | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Reputation | ✅ | ✅ | ✅ | — | ✅ | — | — | — | — |
| Leaderboard | ✅ | ✅ | ✅ | — | ✅ | — | — | — | — |
| Settlement | ✅ | ✅ | ✅ | — | ✅ | ✅ | ✅ | ✅ | ✅ |
| Wallet connect | ✅ | — | — | ✅ | ✅ | — | — | — | — |
| SIWE auth | ✅ | — | — | ✅ | ✅ | ✅ | ✅ | — | — |
Legend: ✅ required • — not applicable
The dispute initiation surface (V2-FE-114) reads dispute eligibility and evidence from the API projection and constructs the dispute transaction only from canonical contract ABI/address artifacts. It exposes:
loading: skeleton while eligibility and evidence are requested;empty: no disputable claim/evidence is available; the action is disabled with an honest explanation (no sample data is inserted);stale: eligibility or evidence is beyond its freshness bound; the submit action is gated until a successful refresh;rejected: the user rejected the wallet request; return focus to the submit trigger and allow retry;failed: missing/unresolved canonical artifact, unsupported chain, simulation disagreement, or API error; fail closed with a retry and no fabricated state;pending: submitted to the mempool; show the real transaction hash;confirmed: included in a block; show confirmations, not success;finalized: finality reached per chain rules; show explorer link;reorged: previously confirmed dispute transaction was reorged out; reconcile before presenting any outcome.
The surface must never jump from pending to finalized, and must never
present a dispute as accepted without a validated receipt and finality.
The route-backed claim detail surface reads GET /api/claims/:id only through
the validated ClaimDetailEnvelope parser. A bare legacy claim response is
malformed and is never rendered as protocol state. The detail view exposes:
loading: skeleton while the projection is requested;ready: validated claim and evidence with fresh projection metadata;ready-stale: validated content with an amber freshness warning and refresh action when the server reportsstaleordegraded;not-found: the canonical404state;failed: unavailable, malformed, or rebuilding projections with a retry;- empty evidence: an honest “No evidence available” state. The client never inserts sample links, images, or testimony.
Each state must satisfy:
| State | ARIA | Focus | Keyboard | Screen reader | Visual |
|---|---|---|---|---|---|
loading |
role="status" aria-busy="true" |
Not stolen | No traps | "Loading…" announced | Skeleton / spinner |
empty |
role="status" |
None | No traps | "No items" announced | Neutral illustration + CTA |
stale |
role="status" |
Refresh control focusable | Refresh reachable | "Stale data" announced | Yellow banner + timestamp |
rejected |
role="status" (not alert) |
Return focus to trigger | Esc to dismiss | "Rejected in wallet" | Non-error info banner |
failed |
role="alert" aria-live="assertive" |
Move focus to error heading | Retry reachable | Error announced | Red banner + retry |
pending |
role="status" aria-busy="true" |
Do not steal | Cancel reachable if supported | "Pending" announced | Progress indicator + tx hash |
confirmed |
role="status" |
— | — | "Confirming" announced | Progress + confirmations |
finalized |
role="status" |
— | — | "Settled" announced | Success state + explorer link |
reorged |
role="alert" |
Move focus | Dismiss reachable | "Reorganized" announced | Amber banner + refresh |
Reduced motion: all non-essential animations disabled when prefers-reduced-motion: reduce (see useReducedMotion.ts).
A surface must fail closed (show failed or block action) when any of the following is true:
| Trigger | Resulting state | Evidence |
|---|---|---|
| Missing critical config in production | Fail-closed UI (IntegrityBoundary) |
IntegrityBoundary.tsx |
| Unsupported chain | Fail-closed UI (IntegrityBoundary) |
IntegrityBoundary.tsx, config/wagmi.ts |
| Missing CSP nonce | Build/middleware abort | docs/SECURITY_HEADERS.md |
| Canonical artifact unresolved | failed (no placeholder fallback) |
docs/CONTRACT_ARTIFACTS.md |
| Simulation disagrees with receipt | failed |
useReceiptProjection.ts |
| Reorg detected on previously confirmed tx | reorged |
useReorgReconciliation.ts |
| Critical API data stale beyond threshold | stale, write actions gated |
useApiWithFallback.ts, useNetworkStatus.ts |
| Wallet disconnected mid-flow | rejected / failed |
useCanonicalWallet.ts, useWallet.ts |
| SIWE nonce expired / replayed | rejected (classified by SiweFailureKind) |
docs/SIWE_AUTH.md, useSiweAuth.ts |
Every non-terminal state must be recoverable without a page reload:
| State | Recovery path |
|---|---|
loading |
Timeout → failed with retry |
stale |
Manual refresh; auto-refresh if enabled |
rejected |
Retry the action |
failed |
Retry; or navigate to safe surface |
pending |
Observe; support replacement/cancel where the chain allows |
confirmed |
Observe until finalized |
reorged |
Automatic reconciliation via useReorgReconciliation |
Determinism: given the same chain state and inputs, the UI must render the same state. Non-determinism (timers, animations) must not affect the state label.
| State | Primary source |
|---|---|
loading |
React Query isLoading / isFetching |
empty |
React Query result length === 0 |
stale |
React Query isStale / dataUpdatedAt vs freshness bound |
rejected |
wagmi UserRejectedRequestError; SiweFailureKind.USER_REJECTED |
failed |
Error boundary; hook error channels |
pending |
useTransactionMachine.ts |
confirmed |
useReceiptProjection.ts, useFinalizationDetection.ts |
finalized |
useFinalizationDetection.ts |
reorged |
useReorgReconciliation.ts, useStateReconciliation.ts |
| Dispute initiation | useDisputeInitiation.ts (eligibility, simulation, submit, receipt, finality) |
Per issue #408, the following must be covered in CI with no concealed skips:
- Unit / component: every state above renders its required ARIA, focus, and announcement contract.
- Wallet/provider integration: rejection, disconnect, account change, chain change, replacement, revert, finality.
- Accessibility: keyboard, focus, labels, announcements, contrast, reduced motion (see
docs/pr-a11y-wcag-aa.md). - E2E: against canonical mocks or staging that cannot leak into production bundles.