Frontend Development¶
Guide to developing the FairWins React frontend in frontend/.
Technology stack¶
- React 18 + Vite — SPA, no server-side rendering, no backend
- wagmi — wallet connection (MetaMask, WalletConnect) and chain switching
- ethers.js v6 — contract reads/writes
- Vitest — unit tests (
npm run test:frontendfrom the repo root) - Cypress — E2E tests
- Plain CSS co-located with components
Project structure¶
frontend/src/
├── App.jsx # routes (see below)
├── pages/ # route-level pages (WalletPage, MarketAcceptancePage, legal/)
├── components/
│ ├── fairwins/ # Dashboard, FriendMarketsModal, MyMarketsModal,
│ │ # MarketAcceptanceModal, ShareWagerModal
│ ├── wallet/ # WalletButton (connect + network toggle)
│ ├── compliance/ # EntryGate (eligibility notice)
│ └── ui/ # WagerQRCode, QRScanner, PremiumPurchaseModal, ...
├── hooks/ # useFriendMarketCreation, useEncryption,
│ # useWalletManagement, useChainTokens, ...
├── contexts/ # FriendMarketsContext (wager cache), DexContext
├── data/wagers/ # EventsSource (RPC scan) + SubgraphSource (optional)
├── abis/ # contract ABIs (WagerRegistry, MembershipManager, ...)
├── config/
│ ├── contracts.js # per-chain addresses — GENERATED, do not hand-edit
│ ├── networks.js # chain capabilities (DEX, Polymarket availability)
│ └── wagmi.js # connectors + default chain
└── constants/wagerDefaults.js # canonical enums & defaults (resolution types,
# statuses, stake/deadline bounds)
Routes (src/App.jsx)¶
| Route | Page | Notes |
|---|---|---|
/ |
LandingPage | public marketing page |
/terms, /risk, /privacy |
LegalDocPage | versioned, hash-linked legal documents |
/app (aliases /main, /fairwins) |
Dashboard | main workspace, inside AppLayout (Header + EntryGate + Footer) |
/wallet |
WalletPage | Account Center: Account / Membership / Security / Preferences / Swap tabs |
/friend-market/accept |
MarketAcceptancePage | QR / deep-link wager acceptance (?marketId=N) |
/admin |
AdminPanel | role-gated (Guardian / Role Manager / Account Moderator / Admin) |
* |
redirect to / |
Getting started¶
Contract configuration¶
Addresses come from src/config/contracts.js, keyed by chain ID (137 Polygon
mainnet, 80002 Amoy, 1337 Hardhat, 63 legacy Mordor). The file is generated
from deployments/ records:
Never hand-edit addresses; fix the deployment record and re-sync.
Core patterns¶
Writing: the wager-creation flow¶
useFriendMarketCreation shows the canonical write pattern — every mutation
is preceded by the same guards the contracts enforce:
- membership check (
MembershipManager.getMembership) - expired-wager cleanup if the user is at their concurrent limit
(
batchExpireOpen) - ERC-20
approvefor the stake if allowance is insufficient - the actual
WagerRegistry.createWager(...)call - optional encrypted-terms upload to IPFS (CID stored in
metadataUri)
In-flight transactions are persisted to localStorage so a reload can resume the flow.
Reading: the wager cache¶
FriendMarketsContext is the single source of truth for the user's wagers,
cached per chain. It pulls from data/wagers/EventsSource.js (direct RPC event
scans + getUserWagers pagination); SubgraphSource.js exists but the
deployed subgraph indexes the legacy v1 factory, so RPC is the primary path.
Encryption¶
useEncryption derives encryption keys from a wallet signature, looks up
counterparty public keys in KeyRegistry, and envelope-encrypts wager terms
before pinning to IPFS. Decryption is lazy — triggered when the user opens a
wager's details. See Encryption Architecture.
Network handling¶
config/wagmi.js defines the default chain (Polygon 137, overridable via
VITE_NETWORK_ID); useNetworkMode implements the mainnet ↔ Amoy toggle in
the wallet dropdown. Per-chain feature flags (DEX availability, Polymarket
side-bets) live in config/networks.js — gate UI on those capabilities rather
than on chain IDs.
Environment variables¶
| Variable | Purpose |
|---|---|
VITE_NETWORK_ID |
default chain (137 production, 80002 testnet) |
VITE_RPC_URL |
default RPC endpoint |
VITE_WALLETCONNECT_PROJECT_ID |
WalletConnect cloud project |
VITE_IPFS_GATEWAY |
IPFS read gateway (Pinata) |
VITE_ORACLE_MODELS |
polymarket-only (default) or all — which oracle resolution types the UI exposes |
Secrets (e.g. the Pinata JWT) are never Vite build args — they're injected at runtime on Cloud Run. See Architecture.
Testing¶
Gotchas worth knowing before mocking contract hooks: vi.mock factories are
hoisted (no outer-scope references), and getContractAddress mocks must cover
every chain the component touches. Match existing test patterns in
frontend/src/**/__tests__/.
Building for production¶
Production images are built by cloudbuild.yaml (multi-stage Docker: Vite
build → nginx). Routing, caching, and security headers live in
frontend/nginx.conf — note the CSP origin allowlist and the
Permissions-Policy camera=(self) required by the QR scanner.