diff --git a/.claude/rules/contracts-vrf-treasury.md b/.claude/rules/contracts-vrf-treasury.md new file mode 100644 index 0000000..73a966b --- /dev/null +++ b/.claude/rules/contracts-vrf-treasury.md @@ -0,0 +1,82 @@ +--- +paths: + - 'packages/contracts/**' + - 'packages/sdk/src/vrf/**' + - 'packages/sdk/src/clients/**' +--- + +# Contracts, VRF and treasury — money-path invariants + +Loaded automatically by Claude Code when touching contract/VRF/generated-client code. Codex does not +auto-load this file — the same content lives in `AGENTS.md`'s Critical Invariants section; read that +manually before touching these paths from Codex. + +- **Never hardcode the VRF beacon app ID.** `947957720` (2022 beacon) is dead — three files in this + repo already say so explicitly (`packages/sdk/src/vrf/beacon.ts`, `packages/types/src/version.ts`, + `packages/contracts/smart_contracts/deploy_config.py`). Read the live ID from + `packages/types/src/version.ts` / the deployed `.env`, every time, including inside tests. + **Testnet is separately disputed**: `packages/sdk/src/vrf/beacon.ts`'s own comment says the testnet + ID `110096026` is "UNVERIFIED — confirm via indexer before testnet use", while `BOARD.md` FG-011 + claims it's "on-chain verified (identical bytecode)". Before configuring TestNet or writing + on-chain integration tests, verify that ID against an indexer yourself — don't trust either source + as settled, and don't treat the mainnet registry as TestNet evidence. +- **Commit round:** `ceil8(current_round + BEACON_DELAY)`, `BEACON_DELAY = 8` + (`packages/contracts/smart_contracts/coinflip/contract.py`). **Settle buffer:** + `BEACON_SETTLE_BUFFER = 4` — `resolve()` must assert `current_round >= commit_round + BEACON_SETTLE_BUFFER` + before calling `must_get()`. Do not reduce either constant. A premature `must_get()` (round not yet + written) reverts the whole transaction — the box is untouched and the keeper retries later + (`packages/keeper/src/resolver.ts` has explicit retry/reconcile handling); this is not a stranded + session. The real permanent-loss mechanism is different: the beacon only retains its last 189 + outputs (~70 min) — if `resolve()` isn't called before that window closes, the round is evicted and + `must_get()` panics forever (keeper marks the session `beacon_expired`; the player still has the 48h + refund). Don't reduce `BEACON_SETTLE_BUFFER` toward that 70-minute ceiling. +- **Refund independence is load-bearing.** `refund()` pays `bet + BOX_MBR` via a direct `Payment` from + the game contract's own balance — it must never read or debit `house_treasury`. This is what keeps + the 48h player backdoor (`REFUND_WINDOW_ROUNDS = 61_714` rounds) working when the treasury is paused + or insolvent. A change that routes any part of `refund()` through the treasury breaks the safety + property the whole design depends on. +- **Solvency check timing and formula:** `HouseTreasury.pay_winner()` (not `get_available_balance()`) + is what `resolve()` actually calls at settlement time, and it enforces + `payout <= (balance - min_balance) * max_payout_bps // 10000` — spendable balance (gross balance + minus the account's own minimum-balance requirement), not gross `live_balance`. + `get_available_balance()` is a separate, advisory read (`simulate()` before accepting a bet) and is + not itself the settlement guard — don't substitute it for `pay_winner()`'s check when reasoning + about solvency. +- **Box deletion order — this is asymmetric between `resolve()` and `refund()`, don't generalize a + single "always delete last" rule:** + - `resolve()`: submit every inner transfer for that resolution (referral, jackpot accrual, treasury + sweep, `pay_winner()` payout) first, **then** delete the session box, **then** return the box MBR + to the player as the last payment. Deleting frees the box's reserved MBR from the contract's + minimum-balance requirement — returning it before deletion can fail or misaccount, since that + balance isn't spendable while the box still exists. + - `refund()`: delete the box **before** its single combined `bet + BOX_MBR` payment (there's no + separate MBR-return step here). + - Either way, box deletion is the only idempotency guard (no `claimed` field) — a resolve/refund + that runs against a missing box reverts instead of silently no-op'ing, which is what makes + double-resolution impossible. Read the actual transfer/delete sequence in + `coinflip/contract.py`'s `resolve()`/`refund()` before changing it; don't infer an ordering from a + general "delete after transfers" heuristic. +- **Box MBR is a per-contract `typing.Final` constant, not a number to copy into a PR description or a + test literal from memory.** Read it from the contract file you're editing + (`grep -n "BOX_MBR\|_BOX_MBR" packages/contracts/smart_contracts//contract.py`) — different + contracts have different values (coinflip 49,300; house_treasury's game-registry box 20,500; + leaderboard's caller box 21,300 as of this check) and they change if the box layout changes. +- **When changing transaction construction** (SDK group-building code, keeper resolve calls): the + grouped payment into `flip()` must include the exact box MBR on top of the bet + (`coinflip/contract.py`'s `flip()` asserts `pay.amount > BOX_MBR`); a resolve/refund group must pool + enough fee to cover every inner transaction it triggers (referral, jackpot, treasury sweep, payout, + MBR return can total several inner txns — `packages/keeper/src/resolver.ts` sets extra fee + accordingly). Pending stakes stay escrowed in the game contract, not the treasury, until settlement. + `resolve()` is permissionless by design — any account can call it, not only the keeper — so don't + add an owner/caller check that would block a player or third party from settling their own session. +- **House edge / referral / jackpot / min-max bet / `max_payout_bps` are env/deploy-config values** + (`packages/contracts/smart_contracts/deploy_config.py`, live `.env`), not contract-code constants and + not numbers to trust from `docs/tokenomics.md` or the `fairground-contract` skill's + `reference/house-pool-math.md` — both disagree with each other and with `deploy_config.py`'s + defaults (`HOUSE_EDGE_BPS=500` / 5%, `REFERRAL_BPS=100` / 1%, `JACKPOT_BPS=150` / 1.5%; + `docs/tokenomics.md` says 2%/0.5%, `house-pool-math.md` computes referral at 25 bps / 0.25%). Query + the deployed contract's global state for the real value when it matters (e.g. writing payout-math + tests or UI copy). +- **Generated clients** (`packages/sdk/src/clients/**`) come from `algokit generate client`; never + hand-edit. Regenerate after any ABI change and re-run `packages/contracts/tests/` first. +- **`simulate()` before `send()`** on any non-trivial group built from these clients. diff --git a/.claude/skills/fairground-contract/SKILL.md b/.claude/skills/fairground-contract/SKILL.md index ae41a2c..3f6af0a 100644 --- a/.claude/skills/fairground-contract/SKILL.md +++ b/.claude/skills/fairground-contract/SKILL.md @@ -16,12 +16,12 @@ Load before writing or modifying any Puya contract in `packages/contracts/smart_ ## Read these references before acting -| File | When to load | -|------|-------------| -| `reference/avm-constraints.md` | Always -- box MBR, group limits, inner txn limits | +| File | When to load | +| ------------------------------ | --------------------------------------------------- | +| `reference/avm-constraints.md` | Always -- box MBR, group limits, inner txn limits | | `reference/vrf-integration.md` | Any VRF-related code (commit, resolve, beacon call) | -| `reference/house-pool-math.md` | Solvency invariant, payout math, bet limits | -| `reference/compliance-ops.md` | Foundation framing, geo-block, grant strategy | +| `reference/house-pool-math.md` | Solvency invariant, payout math, bet limits | +| `reference/compliance-ops.md` | Foundation framing, geo-block, grant strategy | Always invoke `/puya-gotchas` before writing any new Puya code. @@ -37,18 +37,22 @@ Always invoke `/puya-gotchas` before writing any new Puya code. 3. Write/modify contract - Use arc4.Address for BoxMap keys (not Bytes) -- avoids ARC-4 length prefix mismatch - - Execute all inner txns BEFORE deleting box (idempotency guard) + - Box deletion order is asymmetric, don't generalize a single rule: resolve() submits every + inner transfer, THEN deletes the box, THEN returns its MBR last; refund() deletes the box + BEFORE its one combined payment. See AGENTS.md Critical invariants for the code-verified + sequence before changing either. - MBR = 2500 + 400*(prefix_len + key_len + val_len) -- include prefix -4. Verify compilation - cd packages/contracts && algokit compile python smart_contracts/ +4. Verify compilation (per contract, with --out-dir -- without it puyapy 5.8.1 writes beside + contract.py, not into artifacts/) + cd packages/contracts && algokit compile python smart_contracts//contract.py --out-dir artifacts 5. Run tests python -m pytest tests/ -v # Kill-the-mutant check: comment out a key assertion, confirm at least one test fails -6. Generate TypeScript clients (after any ABI changes) - algokit generate client packages/contracts/artifacts/ --output packages/sdk/src/clients/ +6. Generate TypeScript clients (after any ABI changes), from repo root + make contracts-generate 7. Commit with update to BOARD.md task status ``` @@ -74,7 +78,7 @@ LeaderboardContract (optional, enabled via set_leaderboard_app_id) ## Key Invariants -1. Box deletion is ALWAYS last -- non-existence = idempotency guard on keeper retry +1. Box deletion is the idempotency guard (non-existence = already settled), but it is NOT the last step: resolve() submits every payout transfer, deletes the box, then returns the box MBR; refund() deletes the box, then makes one combined bet+MBR payment (coinflip/contract.py:264-265, 285-286) 2. Solvency check at resolve() time, not at flip() time 3. Beacon round: commit to N+8 (ceil8 semantics), resolve after N+10 (N+8+2 buffer) 4. 48h refund backdoor in every game contract -- keeper failure must never lock funds diff --git a/.claude/skills/puya-gotchas/SKILL.md b/.claude/skills/puya-gotchas/SKILL.md index 64ad9d8..647bdb2 100644 --- a/.claude/skills/puya-gotchas/SKILL.md +++ b/.claude/skills/puya-gotchas/SKILL.md @@ -173,10 +173,12 @@ itxn.ApplicationCall(pay_winner...).submit() # too late if this panics ``` ```python -# CORRECT -- execute all inner txns, then delete box last +# CORRECT (coinflip resolve) -- payout transfers first, then delete the box, then return its MBR itxn.ApplicationCall(pay_winner...).submit() # pay winner first # Box deletion is the idempotency guard -- non-existence = already resolved -del self.flips[player] # delete LAST +del self.flips[player] +itxn.Payment(receiver=player.native, amount=UInt64(BOX_MBR)).submit() # MBR only after deletion +# refund(): del self.flips[player], then ONE payment of bet + BOX_MBR ``` --- @@ -231,8 +233,8 @@ vim packages/sdk/src/clients/CoinflipContractClient.ts # manual edit ``` ```bash -# CORRECT -- regenerate from ARC-56 artifacts -algokit generate client packages/contracts/artifacts/ --output packages/sdk/src/clients/ +# CORRECT -- regenerate from ARC-56 artifacts, from repo root +make contracts-generate ``` --- diff --git a/AGENTS.md b/AGENTS.md index a7a43a9..9cb7a73 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,139 +1,249 @@ -# Fairground — Agent Operating Guide +# Fairground — instructions for AI agents -Concise reference for AI coding agents (Codex, remote Claude agents, etc.). CLAUDE.md is the authoritative source; this file is a field-deployable summary. +> Source of truth for every agent (Claude Code, Codex, Cursor). `CLAUDE.md` imports this file. +> Personal repo. Money-path: on-chain treasury + VRF payouts. Numbers in this file were verified +> against code on 2026-09-28 (commands below each claim) — re-verify before trusting an older copy, +> including copies inside this repo (see "Stale facts" below). -## What This Project Is +## What this is -Provably-fair on-chain degen games on Algorand. VRF beacon app ID `947957720` on mainnet. First game: CometaFlip (coinflip), ~150-200 lines of Puya, 10-14 day build. Everything is TypeScript except the smart contracts (Python/Puya). Shared platform infrastructure from day one: house treasury, cross-game leaderboard, proof-card PNG engine. +Provably-fair on-chain degen games on Algorand. TypeScript monorepo (pnpm + Turborepo) + Python/Puya +smart contracts. CoinFlip is live on mainnet; a v2 with a daily jackpot pot was deployed and then +reverted (`git log --oneline`: `f419e0d` deploy → `8f85e83` revert). **Do not assume the current game +set or bet economy from this file — check `BOARD.md` and `git log` first**, they change faster than +instructions do. -**Design bible:** `docs/research/algorand-degen-games-2026-05.md` — read before any product decision. +Brand is deliberately separate from Cometa (regulatory distance for Cometa's FaaS business). Sharing +`~/dev/cometa/` is local-dev convenience only (shared Algorand tooling context) — separate repo, +domain, on-chain entity, brand. The folder path is not a compliance boundary. -## Stack Summary +Stack, versions and rationale: `docs/architecture.md` (this file no longer carries its own copy — the +two had already diverged). Product rationale (historical, pre-launch): `docs/research/*.md`. Current +specs: `docs/game-specs/*.md`, `docs/tokenomics.md`, `docs/roadmap.md`, `docs/compliance.md`, +`docs/testing.md`. -| Layer | Tech | Version | -|-------|------|---------| -| Monorepo | pnpm + Turborepo | pnpm 11.5.0, turbo 2.9.16 | -| Contracts | algorand-python + puyapy | 3.5.0 + 5.8.1 | -| Contract tests | algorand-python-testing | 1.1.0 | -| AlgoKit CLI | algokit | 2.10.2 (not 3.x — does not exist) | -| SDK | algosdk + algokit-utils + generated clients | 3.5.2 + 9.2.0 | -| Wallet | @txnlab/use-wallet-react | 4.6.0 | -| API | Hono + @hono/node-server | 4.12.23 + 2.0.4 | -| Game app | Next.js App Router | 16.2.6 | -| Landing | Astro | 6.4.2 | -| DB | Drizzle + PostgreSQL | 0.45.2 + pg 8.21.0 | -| Realtime | Hono WS + ioredis | ws 8.21.0 + ioredis 5.11.0 | -| Keeper | tsx | 4.22.3 | -| Proof card | satori + @resvg/resvg-js + sharp | 0.26.0 + 2.6.2 + 0.34.5 | -| TypeScript | typescript | 6.0.3 | -| Testing | Vitest | 4.1.7 | - -## Critical Rules - -### Version Pins — No Drift Allowed - -All versions are pinned in `CLAUDE.md`. Never introduce a package version not listed there. Use `context7` MCP to verify API signatures before calling any versioned library. - -### bigint Rule - -All microALGO amounts, ASA amounts, Algorand app IDs, and round numbers are `bigint` in TypeScript. Never `Number`. API serializes `bigint` as `string` via JSON replacer. - -### Never Edit Generated Files - -`packages/sdk/src/clients/` is auto-generated by `algokit generate client`. Never edit by hand. Regenerate when contracts change. - -### No Raw algosdk in Apps - -All transaction group construction goes through `@fairground/sdk` typed clients. No raw algosdk group construction in `apps/game` or `packages/api`. - -### Simulate Before Send - -Call `simulate()` before `send()` on any non-trivial on-chain operation. - -### VRF Commit Round - -Minimum commit round is N+8. Never reduce. 48-hour player-triggered refund backdoor required in all game contracts. - -### Box Storage MBR - -Boxes must be pre-funded before access: `2500 + 400 * (key_len + val_len)` microALGO. Transaction fails with `invalid box reference` if MBR is not included. CometaFlip player box MBR = 35,000 microALGO. - -### House Treasury First - -`house_treasury` contract must be deployed before any game contract. All game contracts call `get_available_balance()` via foreign app ref. Max payout is enforced at `resolve()` time against live treasury balance. - -### Geo-Block Before Launch - -Blocked: US, UK, TH, ID, IN, BR. Thai block is non-negotiable (Bangkok legal exposure). Geo-block must be live before any Foundation RT or ecosystem newsletter submission. - -### Test Quality - -- Kill-the-mutant check: comment out a key SUT line, confirm at least one test fails. -- For every `mockResolvedValue`, add a `mockRejectedValue` sibling. -- Contract integration tests hit AlgoKit LocalNet Docker. -- Never commit if test count decreases. - -## File Structure - -``` -packages/contracts/ Python-only AlgoKit workspace (not pnpm) -packages/sdk/ @fairground/sdk — generated TS clients + VRF helpers -packages/types/ @fairground/types — shared Zod 4.4.3 schemas -packages/db/ @fairground/db — Drizzle schema + pg client -packages/api/ @fairground/api — Hono REST + WS -packages/keeper/ @fairground/keeper — VRF session resolver, 4s poll loop -packages/proof-card/ @fairground/proof-card — satori PNG generator -packages/price-client/ @fairground/price-client — Vestige price batching -apps/game/ Next.js 16 game dApp -apps/landing/ Astro 6 landing page -docs/research/ Design bible and ecosystem context -ops/ geo-blocking.md, weekly-plan.md, safety-state.md -``` - -## Key Commands +## Commands ```bash -# Install pnpm install - -# Build all (turbo) pnpm turbo build - -# Test all -pnpm turbo test - -# Contracts (in packages/contracts/) -algokit localnet start -algokit compile python smart_contracts/ -algokit generate client packages/contracts/artifacts/ --output packages/sdk/src/clients/ -python -m pytest tests/ -v - -# API dev -pnpm --filter @fairground/api dev - -# Game app dev -pnpm --filter apps/game dev +pnpm turbo test # Vitest, all packages +pnpm turbo lint && pnpm turbo typecheck +(cd packages/contracts && algokit localnet start) +(cd packages/contracts && algokit compile python smart_contracts/coinflip/contract.py --out-dir artifacts && \ + algokit compile python smart_contracts/house_treasury/contract.py --out-dir artifacts && \ + algokit compile python smart_contracts/leaderboard/contract.py --out-dir artifacts && \ + algokit compile python smart_contracts/fairjackpot/contract.py --out-dir artifacts) # after any contract change; without --out-dir puyapy 5.8.1 writes beside contract.py, not into artifacts/ +make contracts-generate # after any ABI change — from repo root; one `algokit generate + # client --language typescript` call per contract, see Makefile +(cd packages/contracts && python -m pytest tests/ -v) # deterministic, no LocalNet needed +pnpm test:e2e # Playwright; defaults to production URLs — set GAME_URL/LANDING_URL for local ``` -## Keeper Architecture - -4-second poll loop. Redis SETNX `keeperlock` (10s TTL, refreshed every 4s). Two Docker Compose instances: `fairground-keeper-primary` and `fairground-keeper-standby`. If primary crashes, standby acquires lock automatically. Keeper publishes to Redis pub/sub; Hono WS fans out to game clients. - -## Compliance - -This is house-banked gambling. Geo-block US/UK/TH/ID/IN/BR before launch. Frame as VRF tech demonstration in Foundation-facing communications. Never mention real-money gambling in xGov grant applications. Open-source contracts from day one. - -## Commit Format - -Lowercase verb, no type prefix. `add coinflip contract v1`. Update BOARD.md in same commit. Push after committing. Never `--no-verify`. - -## When to Use Which MCP - -| Need | Tool | -|------|------| -| Contract state, VRF beacon inspection, simulate txn | `algorand` MCP | -| API signatures for algokit-utils, algosdk, Hono, Drizzle | `context7` MCP | -| Ecosystem research, competitive analysis, regulatory | `perplexity` MCP | -| Visual QA, competitor scraping | `playwright` MCP | -| ALGO/USD price | `vestige` MCP or WebFetch `api.vestigelabs.org` | -| Quick fact check | Tavily / WebSearch | +Keep the parens: each subshell keeps its `cd` from leaking into the next command. The compile paths +are relative to `packages/contracts/` (`packages/contracts/README.md`) and resolve wrong if you drop +the subshell and just `cd` once for the whole block. **`pnpm contracts:build|test|generate` and +`make contracts-build|contracts-test` all shell out to `algokit project run build|test|generate` — +`packages/contracts/.algokit.toml` defines no `[project.run.*]` task, so none of those five commands +work.** Use the direct commands above instead. `make contracts-generate` is the one wrapper that does +work: it bypasses `algokit project run` entirely and calls `algokit generate client --language +typescript --output ` once per contract (see Makefile) — there's no single "regenerate +everything" command. Four testing tiers (contract pytest → package Vitest → frontend e2e → on-chain +TestNet e2e with a Mnemonic wallet), commands and the one documented regression (a keeper/contract +payout-factor mismatch): `docs/testing.md`. Kill-the-mutant convention: `packages/contracts/README.md` +(not `docs/testing.md`). + +## Critical invariants (verified against code, commands shown) + +- **bigint everywhere.** Every microALGO/ASA amount, Algorand app ID and round number is `bigint` in + TypeScript; never `Number()`. API responses serialize bigint as `string` via a JSON replacer. +- **Generated code is never hand-edited.** `packages/sdk/src/clients/**` comes from + `algokit-client-generator`; regenerate after any contract ABI change, don't patch the output. + **ARC-56/ARC-32 compatibility is resolved, not open**: `algokit-client-generator@6.0.1` fully accepts + ARC-56 input from `puyapy 5.8.1` (its npm description says "ARC-0032"; that's a stale description, + not a support claim). No `--output-arc32` fallback is needed — all four clients in + `packages/sdk/src/clients/` were generated this way (`ls packages/sdk/src/clients/` to re-verify). + `docs/architecture.md` and `packages/contracts/README.md` still called this unresolved as of the + same 2026-09-28 pass that wrote this file; corrected in the same commit. +- **No raw algosdk group construction** in `apps/game` or `packages/api` — go through the typed + `@fairground/sdk` clients. +- **`simulate()` before `send()`** on any non-trivial on-chain operation. +- **The VRF beacon app ID is not a constant to memorize or copy into a doc.** The mainnet beacon has + already changed once: `947957720` (2022 deployment) is explicitly dead — + `grep -rn 947957720 packages/sdk/src/vrf/beacon.ts packages/types/src/version.ts packages/contracts/smart_contracts/deploy_config.py` + turns up three independent comments calling it "DEAD". The live ID lives in + `packages/types/src/version.ts` / the deployed `.env` (`VRF_BEACON_APP_ID`), not in this file — + read it there every time, because the same drift will happen again. **TestNet is separately + disputed**: `beacon.ts`'s own comment calls TestNet ID `110096026` "UNVERIFIED", `BOARD.md` FG-011 + calls it "on-chain verified" — before configuring a network or writing on-chain integration tests, + verify that network's beacon against an indexer yourself; the mainnet registry is not TestNet + evidence. +- **Commit-reveal timing** (`packages/contracts/smart_contracts/coinflip/contract.py`): `flip()` + commits to `ceil8(current_round + BEACON_DELAY)` where `BEACON_DELAY = 8` (so the committed round is + 8–15 rounds out); `resolve()` requires `current_round >= commit_round + BEACON_SETTLE_BUFFER` + (`BEACON_SETTLE_BUFFER = 4`) before reading the beacon. Never reduce either constant. A premature + `must_get()` reverts (no state change, keeper retries — see `packages/keeper/src/resolver.ts`), it + does not strand the session; the real permanent-loss risk is the beacon's ~70-minute output + retention window closing before `resolve()` is ever called. +- **48h player-triggered refund is mandatory in every game contract** (`REFUND_WINDOW_ROUNDS = 61_714` + rounds ≈ 48h at 2.8s/round, same file). Keeper failure must never lock player funds. `refund()` pays + bet + box MBR directly from the game contract's own balance and never touches `house_treasury` — it + must keep working even when the treasury is paused or the keeper is down. When constructing the + `flip()` group, fund the exact box MBR on top of the bet; a resolve/refund group must pool enough + fee for every inner transaction it triggers. `resolve()` is permissionless by design — anyone can + call it, not only the keeper. +- **Treasury-first, solvency-checked-late.** `house_treasury` is deployed before any game contract; + `resolve()` calls `HouseTreasury.pay_winner()` (not `get_available_balance()`), which enforces + `payout <= (balance - min_balance) * max_payout_bps // 10000` — spendable balance, not gross — + **at settlement time**, not at bet time. `get_available_balance()` is a separate, advisory pre-bet + read; it is not the settlement guard. +- **Box deletion order is asymmetric — don't generalize "delete after every transfer" as a universal + rule.** In `resolve()`: submit every inner transfer (referral, jackpot, treasury sweep, payout) + first, delete the box, _then_ return its MBR last (returning MBR before deletion can fail — that + balance isn't spendable while the box exists). In `refund()`: delete the box _before_ its one + combined `bet + BOX_MBR` payment. Either way, box deletion (no `claimed` field) is what makes a + resolve/refund against an already-settled session revert instead of silently double-paying — check + the actual sequence in `contract.py` before changing it. +- **Keeper leader election**: Redis `SETNX` lock (`packages/keeper/src/lock.ts`), 10s TTL, refreshed + every 4s, released on graceful shutdown. Renewal and release are both owner-checked and atomic + (Lua `GET`+`PEXPIRE`/`DEL` — prevents a TOCTOU race where an instance extends or deletes a lock it + no longer holds). `packages/keeper/src/index.ts` also serializes ticks (`tickRunning` guard) because + a resolve batch can run longer than the poll interval — without it, an overlapping tick could pick + up a still-`pending` session and resolve it twice (second call reverts on-chain but still churns + the DB and burns algod calls). Preserve both the atomic lease and the serial-tick guard; don't run + resolve logic outside the lock holder, and don't remove the overlap guard to "speed up" polling. +- **Keeper accounting must match deployed contract behavior, not source defaults.** UI results, proof + cards and keeper-recorded payouts must reflect the actual deployed contract version's economics — + `packages/keeper/src/payout.ts` documents a real incident where it used a stale payout factor + (9800n / 1.96x) while the chain actually paid 9700n / 1.94x (`docs/testing.md`'s Tier-2 example). + Check `payout.ts` and its tests, and the deployment registry, before changing keeper accounting, UI + results or proof-card math — don't assume current contract-source defaults match what's deployed. +- **Numbers that move at deploy/env time — read them from source, not from a doc or this file**: box + MBR, house edge bps, referral bps, min/max bet, treasury auto-pause floor, `max_payout_bps`. Sources: + `packages/contracts/smart_contracts/{coinflip,house_treasury,leaderboard}/contract.py` (box MBR is a + `typing.Final` per contract), `packages/contracts/smart_contracts/deploy_config.py` (house edge / + referral bps env-overridable defaults), the live `.env` (bet limits, treasury floor). See "Stale + facts" below for concrete proof these numbers already disagree across the repo. +- Secrets never enter the repo. `HOUSE_SEED_WALLET_MNEMONIC` / any 25-word phrase must never be + staged — **there is currently no enforcing hook** (`BOARD.md` FG-008 is still `todo`, `.husky/` + only runs lint-staged). Grep before committing anything near env or deploy scripts. + +## Stale facts found in this repo (flagged, not fixed — out of scope for this instruction-layer pass) + +Verified 2026-09-28. None of these are edited by this change; they are called out so no agent (or +owner) treats them as current by accident. + +- **Dead VRF beacon ID baked into docs and a skill.** `docs/tokenomics.md`, `docs/game-specs/coinflip.md`, + and the `fairground-contract` skill's `reference/vrf-integration.md` / `reference/house-pool-math.md` + all still hardcode mainnet beacon `947957720`. Current code calls that ID dead in three places (see + invariant above). `SKILLS.md`'s workflow description for `fairground-contract` also quotes it. +- **House economics disagree across sources, not just against `deploy_config.py`.** `docs/tokenomics.md` + says "2% house edge / 0.5% referral"; the `fairground-contract` skill's `reference/house-pool-math.md` + computes referral at 25 bps (0.25%), not 0.5% — those two already disagree with each other. + `deploy_config.py` defaults are `HOUSE_EDGE_BPS=500` (5%) / `REFERRAL_BPS=100` (1%) / `JACKPOT_BPS=150` + (1.5%) (`grep -n HOUSE_EDGE_BPS\|REFERRAL_BPS\|JACKPOT_BPS packages/contracts/smart_contracts/deploy_config.py`). + Only the deployed contract's live global state is authoritative for any of these — the docs are + historical/aspirational and don't even agree among themselves. +- **`.env.example`'s bet-limit default is a schema default, not evidence of drift** — `MIN_BET_MICROALGO= +MAX_BET_MICROALGO=500000` (0.5 ALGO) is explicitly a scaffold/schema default, not a snapshot of + production config. `docker-compose.yml`'s build args (`NEXT_PUBLIC_MIN_BET_MICROALGO=1000000`, + `NEXT_PUBLIC_MAX_BET_MICROALGO=20000000`) agree with the mainnet `raise minimum bet to 1 ALGO` and + `update landing ticker to 1-20 ALGO bet economy` commits — production bet limits are corroborated, + not contradicted, by two independent sources. What's still unverified is whether the _deployed + contract's_ on-chain `min_bet`/`max_bet` global state actually matches 1–20 ALGO; check that + directly before quoting a number as currently enforced on-chain. +- **Production domain is configured as `fairground.quest`**, not `fairground.xyz` (not independently + re-verified live here — this is repository-config evidence): + `grep -n server_name ops/nginx/fairground.quest.conf`, `docker-compose.yml`'s build args + (`https://api.fairground.quest`), and `.env.example`'s `CORS_ORIGINS`/`PLAUSIBLE_DOMAIN` all agree + on `.quest`. `docs/architecture.md`, `docs/compliance.md`, `docs/tokenomics.md`'s referral URL, and + `apps/landing/astro.config.mjs`'s `site:` field still say `.xyz`. `docker-compose.yml` line 11's own + comment is stale too (routes `api.fairground.xyz`). +- **Geo-blocking status is unverified live.** The untracked `docs/audit/launch-readiness-2026-06-09.md` + (agent-authored, not part of this rework) reported `CF-IPCountry` never reaching the app — + geo-block fully bypassed — as of that date. Not reverified here. Compliance treats geo-block as + non-negotiable (`docs/compliance.md`); confirm current status before relying on it. + +## Testing + +Four tiers — contract pytest, package Vitest, Playwright frontend e2e (`?demo=win|loss`, no wallet), +Playwright on-chain e2e (TestNet, programmatic Mnemonic wallet, opt-in via `E2E_ONCHAIN=1`). Full +detail, commands and the one documented regression: `docs/testing.md`. Contract tests: kill-the-mutant +required (`packages/contracts/README.md` → Kill-the-Mutant Verification) — comment out the checked +line in the SUT and confirm a specific test goes red before committing; name the test after the +mutation it kills. + +## Commits and delivery + +- Lowercase verb, no type prefix (`add coinflip contract v1`, not `feat: ...`), no internal jargon in + the message (no `FG-NNN`, no "Phase N"), no AI attribution. Update `BOARD.md` task status in the + same commit as the code that closes it. Never `--no-verify` (Husky runs `eslint --fix && prettier +--write` on staged `*.ts`/`*.tsx`). +- **Commit authorization does not imply push authorization** — push only within the + task-authorized scope (see "For Codex and other agents" below); don't push automatically just + because a commit landed. +- No review skill/gate and no docs-drift check are installed in this repo (personal repo — installing + them is out of scope for this change). Self-review money-path diffs before pushing; nothing else + will catch them before CI does. + +## Compliance (short version — full policy `docs/compliance.md`) + +This is house-banked gambling; treating it as anything else operationally is a legal risk. Geo-block +of TH/US/UK/ID/IN/BR is non-negotiable — the operator is in Bangkok, direct personal exposure under +Thai law. Foundation-facing and xGov copy frames the product as VRF/provably-fair infrastructure, never +"casino"/"gambling"/"wager". See "Stale facts" above for the current domain and geo-block-verification +caveats before writing anything public-facing. + +Scope gates (not superseded by the reverted jackpot experiment — that reverted one implementation, +not the underlying restrictions): before proposing or implementing additional games, tokens, ASA +support or native mobile apps, read the current gates in `docs/roadmap.md` and `docs/tokenomics.md`. +Before public launch messaging or Foundation/ecosystem amplification, verify geo-blocking and the +treasury/TOS/open-source/proof-endpoint prerequisites in `docs/compliance.md` — don't treat "the code +exists" as "cleared to announce." + +## For Codex and other agents + +- This file is the source of truth; Claude Code loads it through `CLAUDE.md`, Codex reads it natively. + An untracked `AGENTS.override.md` currently shadows this file for Codex — its useful content (task + routing, money-path summary) is folded in above; delete the override once this file replaces the + tracked `AGENTS.md`, otherwise Codex keeps reading the stale override instead of this file. The + parent `~/dev/cometa/AGENTS.md` also exists and applies to this repo (shared workspace boundaries); + Codex loads it automatically by directory hierarchy, Claude/Cursor should read it once per session. +- Task routing: architecture/economics → `docs/architecture.md`, `docs/game-specs/`, `docs/tokenomics.md` + (treat its numbers as historical/aspirational, verify against contract state or `deploy_config.py` + before quoting one); test lanes → `docs/testing.md`; release/ops → `docs/runbooks/`, `ops/`, current + deployment config; product/design exploration → the specific `docs/design/` or `docs/research/` file + for the feature at hand, not the whole library for a narrow fix. +- Contract source `packages/contracts/` (Python/AlgoKit workspace, **not** a pnpm package); SDK + `packages/sdk/`; API `packages/api/` (Hono); keeper `packages/keeper/`; game `apps/game/` (Next.js + App Router); landing `apps/landing/` (Astro). +- No standing authorization to commit/push/deploy/sign/broadcast or publish content. Execute only the + task-authorized scope; verify the resulting artifact/health afterward rather than trusting a green + build. +- Node ≥22, pnpm ≥11.5.0 (`package.json` `engines`/`packageManager`) — read installed version pins from + `package.json`/lockfiles, not this file or any dated instruction table. + +## Repository map + +| Path | Purpose | +| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `packages/contracts/` | Python/AlgoKit workspace (not pnpm): `smart_contracts/{house_treasury,coinflip,leaderboard,fairjackpot}/`, `deploy_config.py`, per-contract `smart_contracts//artifacts/*.arc56.json` (generated, never hand-edit — there is no top-level `packages/contracts/artifacts/`), `tests/` | +| `packages/sdk/` | `@fairground/sdk` — generated ARC-56 TS clients (`src/clients/`, never hand-edit) + VRF helpers (`src/vrf/beacon.ts`) | +| `packages/types/` | `@fairground/types` — Zod schemas, env validation, beacon/version guard (`src/version.ts`) | +| `packages/db/` | `@fairground/db` — Drizzle schema + pg client | +| `packages/api/` | `@fairground/api` — Hono REST + WS | +| `packages/keeper/` | `@fairground/keeper` — VRF resolver, Redis SETNX leader lock (`src/lock.ts`) | +| `packages/proof-card/` | `@fairground/proof-card` — satori/resvg PNG generator | +| `apps/game/`, `apps/landing/` | Next.js game dApp; Astro landing | +| `docs/` | canonical docs: `architecture.md`, `testing.md`, `compliance.md`, `tokenomics.md`, `roadmap.md`, `game-specs/*`, `design/*`, `runbooks/*`, `research/*` (historical), `audit/*` (point-in-time, not live status) | +| `ops/` | `geo-blocking.md`, `safety-state.md`, `weekly-plan.md`, `nginx/*.conf` | +| `.claude/skills/` | `fairground-contract`, `fairground-frontend`, `fairground-landing`, `puya-gotchas`, `smart-contract-audit` (index: `SKILLS.md`); two `fairground-contract` reference files are stale, see above | + +## Gotchas + +- Sibling repos worth reusing from: `cometa-strategy` (design-bible origin, `research/algorand-degen-games-2026-05.md`), + `prediction-market` (Puya box-storage/AlgoKit workspace patterns), `metafarm-frontend` + (`walletConnectService.ts` iOS relayer-wake hook, not yet ported into `useRelayerWake()`). + +(The `BOARD.md` FG-008 secret-scan gap and `.env.example`'s stale schema defaults are covered once +each, in Critical invariants and Stale facts above — not repeated here.) diff --git a/CLAUDE.md b/CLAUDE.md index 513e8f2..9a55338 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,460 +1,41 @@ -# Fairground — Claude Code Project Instructions +@AGENTS.md -## Project Identity +# Claude Code specifics — Fairground -**Fairground** is a provably-fair on-chain degen games platform built on Algorand, powered by Algorand's native VRF beacon (app ID 947957720 on mainnet). The brand is intentionally separate from Cometa — regulatory distance from Cometa's FaaS business and the Algorand Foundation relationship is a deliberate architecture decision, not cosmetic. Rename is trivial if needed; the separation is not. +## Project identity & positioning (for copy/marketing tasks) -**Positioning:** "Provably fair games on Algorand." Survivors in crypto gambling win on honest, verifiable fairness (SatoshiDice 2012, Bustabit 2014, Rollbit 2020). The VRF proof card — a shareable PNG showing VRF round, beacon output hash, and transaction ID — is the marketing budget. +Positioning: "Provably fair games on Algorand." The VRF proof card (round, beacon output hash, txn ID) +is the marketing asset — survivor comps are SatoshiDice, Bustabit, Rollbit. Full narrative, validation +matrix and tokenomics playbook: `docs/research/algorand-degen-games-2026-05.md` — written pre-launch, +treat its specific numbers as historical rationale; `docs/tokenomics.md` and `docs/roadmap.md` are the +current specs (and are themselves known-stale in places, see `AGENTS.md` → Stale facts). -**Repository:** `~/dev/cometa/fairground/` — `git@github.com:nikitacometa/fairground.git` (personal account) -**npm scope:** `@fairground/*` -**Task IDs:** `FG-NNN` -**Status (May 2026):** Pre-launch, building CometaFlip v1 (Days 1-14 on the roadmap) +## MCP servers (`.mcp.json`) -**On the directory location:** Fairground lives under `~/dev/cometa/` next to `prediction-market` (same Puya/AlgoKit stack) and `cometa-strategy` (which holds the design bible). This is a _local dev convenience_ — it inherits the parent `~/dev/cometa/CLAUDE.md` Algorand context via the Claude ancestor chain. It does NOT make Fairground part of the Cometa product. Brand separation is enforced where it matters: separate GitHub repo, separate domain, separate on-chain entity, separate public brand. The folder path is not a compliance surface. +| Server | Purpose | When | +| ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | +| `algorand` (`@goplausible/algorand-mcp@4.2.5`) | beacon inspection, contract state reads, `simulate_transactions` before mainnet spend | always; defaults to `ALGORAND_NETWORK=localnet`, switch to mainnet only for explicit inspection tasks | +| `playwright` (`@playwright/mcp`) | visual QA of the game dApp before deploy, competitor scraping | before any production deploy | +| `vestige` (npm package is `vestige-mcp`, **not** `@goplausible/vestige-mcp`) | ALGO/USD price for jackpot/treasury display | if it fails to load, fall back to WebFetch against `api.vestigelabs.org` | +| `context7`, `perplexity` | inherited from global `~/.claude/mcp.json` — do not duplicate in `.mcp.json` | `context7` before calling a versioned library API; `perplexity` for multi-source ecosystem/regulatory research | -**Design bible (read this before any product decision):** -`docs/research/algorand-degen-games-2026-05.md` — full survival analysis, 11 game concepts, validation matrix, tokenomics playbook, launch roadmap, platform thesis, compliance notes. -`docs/research/algorand-build-ideas-2026-05.md` — ecosystem context. -Every architectural decision is traceable to these docs. When in doubt, re-read them. +## Skills ---- +Index: `SKILLS.md`. Triggers: writing Puya contract code → `fairground-contract` (its +`reference/vrf-integration.md` and `reference/house-pool-math.md` are stale — see `AGENTS.md` → +Stale facts — verify their numbers against `contract.py`/`deploy_config.py` before using them); +frontend work → `fairground-frontend`; landing page → `fairground-landing`; any Puya-specific gotcha → +`puya-gotchas`; auditing a contract → `smart-contract-audit`. -## Stack +## Compacting -| Layer | Technology | Pinned Version | Notes | -| ------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | -| monorepo | pnpm workspaces + Turborepo | pnpm 11.5.0, turbo 2.9.16 | `pnpm-workspace.yaml` covers `packages/*` and `apps/*` | -| contracts | algorand-python (Puya) + AlgoKit | algorand-python==3.5.0, puyapy==5.8.1, algorand-python-testing==1.1.0, algokit 2.10.2 | Never write "Puya v5.0" in technical docs. AlgoKit 3.x does not exist. puyapy 5.8.1 outputs ARC-56 JSON by default | -| sdk | algosdk + algokit-utils + generated ARC-56 TS clients | algosdk@3.5.2, @algorandfoundation/algokit-utils@9.2.0, @algorandfoundation/algokit-client-generator@6.0.1 | Generated clients from algokit-client-generator are the ONLY interface between TS code and contracts. No raw algosdk group construction in apps | -| wallet | @txnlab/use-wallet-react | @txnlab/use-wallet@4.6.0, @txnlab/use-wallet-react@4.6.0, @perawallet/connect@1.5.2, @blockshake/defly-connect@1.2.1 | Requires algosdk ^3.x — compatible with 3.5.2 | -| api | Hono + @hono/node-server | hono@4.12.23, @hono/node-server@2.0.4, @hono/zod-validator@0.8.0 | Web Standard API enables future Cloudflare Worker deployment for proof-card CDN | -| web (game) | Next.js App Router | next@16.2.6, react@19.2.6 | RSC for leaderboard/history pages; client components for game canvas + WS | -| web (landing) | Astro | astro@6.4.2, @astrojs/react@5.0.6 | Static, deploys via rsync in ~3s | -| db | Drizzle ORM + PostgreSQL | drizzle-orm@0.45.2, drizzle-kit@0.31.10, pg@8.21.0 | Drizzle generates pure SQL, no binary generation step | -| realtime | Hono WS + ioredis pub/sub | ws@8.21.0, ioredis@5.11.0 | Keeper publishes to Redis pub/sub; Hono WS fans out to clients | -| keeper | tsx polling process + Redis SETNX | tsx@4.22.3 | Two instances (primary + standby), 10s TTL lock refreshed every 4s | -| proof-card | satori + @resvg/resvg-js + sharp | satori@0.26.0, @resvg/resvg-js@2.6.2, sharp@0.34.5 | PNG generation, Redis caches `proof:{txnId}` permanently | -| styling | Tailwind CSS | tailwindcss@4.3.0, @tailwindcss/vite | OKLCH design tokens. Amber-gold primary: `oklch(0.78 0.18 65)` | -| testing | Vitest | vitest@4.1.7 | `vitest.workspace.ts` at root covers all packages | -| lint | ESLint + Prettier + Husky + lint-staged | eslint@10.4.1 (flat config), typescript-eslint@8.60.0, @eslint/js@10.0.1, globals@17.6.0, eslint-config-prettier@10.1.8, prettier@3.8.3, husky@9.1.7, lint-staged@17.0.6 | `no-floating-promises=error`, `no-explicit-any=error`. Config: `eslint.config.mjs` (flat config). No `.eslintrc.*` files. | -| vrf | Algorand VRF Beacon (Applied Blockchain) | app ID 947957720 (mainnet) | Free. Commit-reveal: bet commits to round N+8 (~22s wait) | -| build | TypeScript + tsup | typescript@6.0.3, tsup@8.5.1 | TS project references across packages | -| deploy | Docker Compose on Hostinger VPS | Separate stack from Cometa (`~/fairground/docker-compose.yml`) | Nginx reverse proxy | +Preserve, in priority order: current task (requested/done/remaining); which facts were actually +re-verified against code vs. trusted from a doc (this repo has several docs that disagree with the +code); modified files; last test results; commit state (branch, last hash, uncommitted files); +authorization scope for anything on-chain, deploy or publish; next steps. ---- +## Personal layer -## Monorepo Layout - -``` -fairground/ -├── pnpm-workspace.yaml -├── turbo.json -├── tsconfig.base.json -├── eslint.config.mjs -├── .prettierrc -├── .gitignore -├── CLAUDE.md -├── AGENTS.md -├── BOARD.md -├── SKILLS.md -│ -├── packages/ -│ ├── contracts/ # Python-only AlgoKit workspace (NOT a pnpm package) -│ │ ├── .algokit.toml -│ │ ├── pyproject.toml -│ │ ├── smart_contracts/ -│ │ │ ├── house_treasury/ # FIRST contract to build — all games depend on it -│ │ │ ├── coinflip/ # CometaFlip v1, ~150-200 lines -│ │ │ └── leaderboard/ # Cross-game leaderboard stub -│ │ ├── artifacts/ # *.arc56.json — auto-generated, never edit by hand -│ │ └── tests/ # algorand-python-testing suite -│ │ -│ ├── sdk/ # @fairground/sdk — bridge between Puya contracts and TS -│ │ └── src/ -│ │ ├── clients/ # Generated by algokit-client-generator — never edit -│ │ └── vrf/ -│ │ └── beacon.ts # MAINNET_BEACON_APP_ID=947957720n -│ │ -│ ├── types/ # @fairground/types — shared Zod 4.4.3 schemas -│ ├── db/ # @fairground/db — Drizzle schema + pg client -│ ├── api/ # @fairground/api — Hono REST + WebSocket -│ ├── keeper/ # @fairground/keeper — VRF session resolver -│ ├── proof-card/ # @fairground/proof-card — satori PNG generator -│ └── price-client/ # @fairground/price-client — Vestige price batching -│ -├── apps/ -│ ├── game/ # Next.js 16 App Router game dApp -│ └── landing/ # Astro 6 + Tailwind 4 + React 19 islands -│ -├── docs/ -│ └── research/ -│ ├── algorand-degen-games-2026-05.md # Design bible — source of truth -│ └── algorand-build-ideas-2026-05.md # Ecosystem context -│ -├── ops/ -│ ├── geo-blocking.md # Cloudflare Workers geo-block spec — required before launch -│ ├── weekly-plan.md -│ └── safety-state.md -│ -├── docker-compose.yml # Separate stack from ~/cometa/docker-compose.yml -├── .mcp.json -└── .claude/ - └── settings.json -``` - ---- - -## Build / Test / Lint / Deploy Commands - -### Monorepo (run from repo root) - -```bash -# Install dependencies -pnpm install - -# Build all packages (turbo, respects dependency graph) -pnpm turbo build - -# Run all tests -pnpm turbo test - -# Lint all packages -pnpm turbo lint - -# Type-check all packages -pnpm turbo typecheck - -# Run specific package -pnpm --filter @fairground/api dev -pnpm --filter apps/game dev -``` - -### Contracts (packages/contracts — AlgoKit, not pnpm) - -```bash -cd packages/contracts - -# Compile all contracts (outputs ARC-56 artifacts/) -algokit compile python smart_contracts/ - -# Generate typed TS clients from ARC-56 artifacts -algokit generate client packages/contracts/artifacts/ --output packages/sdk/src/clients/ - -# Run contract tests (LocalNet must be running) -algokit localnet start -python -m pytest tests/ -v - -# Bootstrap LocalNet -algokit localnet reset -``` - -### API - -```bash -pnpm --filter @fairground/api dev # hot reload via tsx watch -pnpm --filter @fairground/api build # tsup bundle -pnpm --filter @fairground/api start # production -``` - -### Game dApp - -```bash -pnpm --filter apps/game dev # Next.js dev server, port 3000 -pnpm --filter apps/game build -pnpm --filter apps/game start -``` - -### Landing - -```bash -pnpm --filter apps/landing dev # Astro dev server, port 4321 -pnpm --filter apps/landing build -pnpm --filter apps/landing deploy # rsync to VPS -``` - -### Docker (production) - -```bash -# Deploy all services -docker compose -f ~/fairground/docker-compose.yml up -d --build - -# Tail logs -docker compose -f ~/fairground/docker-compose.yml logs -f fairground-api -docker compose -f ~/fairground/docker-compose.yml logs -f fairground-keeper-primary - -# Services: fairground-api (port 3010), fairground-keeper-primary, -# fairground-keeper-standby, fairground-db (postgres:16-alpine, not exposed), -# fairground-redis (redis:7-alpine, not exposed) -``` - -### DB Migrations - -```bash -pnpm --filter @fairground/db db:generate # drizzle-kit generate -pnpm --filter @fairground/db db:push # push to local dev DB -# Production: keeper runs migrations on startup -``` - -### Health checks - -```bash -curl https://api.fairground.xyz/leaderboard -curl https://fairground.xyz/ # landing -``` - ---- - -## Conventions - -### TypeScript - -- TypeScript 6.0.3. Every package extends `tsconfig.base.json` at repo root. -- Options: `strict`, `strictNullChecks`, `noImplicitAny`, `noUncheckedIndexedAccess`, `noImplicitReturns`, `noUnusedLocals`, `noUnusedParameters`, `forceConsistentCasingInFileNames`, `target ES2021`, `resolveJsonModule`, `esModuleInterop`. -- **Do not add** `emitDecoratorMetadata` or `experimentalDecorators` — those are NestJS-specific. -- TS project references across packages for type safety without bundling during development. -- `paths` in each package's `tsconfig.json` for `@fairground/*` workspace imports. - -**bigint rule — no exceptions:** All microALGO amounts, ASA amounts, Algorand app IDs, and round numbers are `bigint` in TypeScript. Never `Number`. API responses serialize `bigint` as `string` via a JSON replacer. Zod schemas use `z.coerce.bigint()` for env vars and API inputs. - -- Prefer `unknown` over `any`. If you think you need `any`, you probably need a generic or a type guard. -- Declare return types on all exported/public functions. -- Use `jest.mocked()` or `DeepMocked` for mock type casting — never `as never` / `as unknown as`. - -### ESLint - -Config: `eslint.config.mjs` at repo root (flat config, ESLint 9). Plugins: `typescript-eslint@8.60.0` unified package with `recommendedTypeChecked`. Critical rules: - -- `no-floating-promises: error` — catches unhandled async in keeper bot code -- `require-await: error` -- `no-explicit-any: error` -- `no-unused-vars: error` with `argsIgnorePattern: '^_'` - -### Prettier - -`singleQuote: true, trailingComma: 'all', tabWidth: 2, semi: true, printWidth: 100, bracketSpacing: true, arrowParens: 'always', endOfLine: 'lf'` - -Husky 9.1.7 + lint-staged 17.0.6 runs `eslint --fix && prettier --write` on staged `*.ts`/`*.tsx` files before every commit. Never use `--no-verify`. - -### Testing - -- Vitest 4.1.7 everywhere. `vitest.workspace.ts` at root covers all packages. -- `IVrfBeacon` interface with a `LocalNetStub` for deterministic contract tests. -- **Kill-the-mutant check** required: comment out a key line in the SUT and confirm at least one test fails before committing. -- For every `mockResolvedValue`, add a `mockRejectedValue` sibling test. -- Use `it.each()` for parameterized bet amounts — never duplicate test structure in separate `it()` blocks. -- For `$transaction` mocks, include a test where the callback throws (verifies rollback behavior). -- Contract integration tests hit AlgoKit LocalNet (Docker). Unit tests mock the algosdk client. -- **Never commit if test count decreases.** Baseline test count is tracked here; update when adding test suites. - -### Commit Discipline - -- Lowercase verb, no type prefix: `add coinflip contract v1`, `fix max-bet enforcement in house treasury`, `implement proof card PNG generator`. -- No `feat:` / `fix:` / `chore:` prefixes. No internal jargon (no "Phase 1", "FG-007" in commit messages). No co-authorship lines. Never `--no-verify`. -- Update `BOARD.md` task status in the same commit as the code it closes. -- Push after committing. -- Verify contracts are tested before committing contract changes: `python -m pytest tests/ -v` must pass. - -### Error Handling - -- Never catch and swallow errors. Always rethrow, log with context via `pino`, or return a typed error. -- Every `try/catch` in production code needs a corresponding test for the catch branch. -- Empty catch blocks are forbidden. -- Use `simulate()` before `send()` on non-trivial on-chain operations. - -### Code Generation (Critical) - -- `packages/sdk/src/clients/` is auto-generated by `algokit-client-generator@6.0.1` from `packages/contracts/artifacts/*.arc56.json`. -- **Never edit generated files by hand.** Regenerate via `algokit generate client`. -- Verify every import exists in the actual package version before using it — do not invent API shapes. -- Verify package names exist before adding new dependencies. - ---- - -## Algorand / VRF Specifics - -### VRF Beacon - -- **Mainnet beacon app ID:** `947957720` — defined as `MAINNET_BEACON_APP_ID = 947957720n` in `packages/sdk/src/vrf/beacon.ts`. -- Commit-reveal pattern: bet transaction commits to VRF beacon round N+8 (~22 seconds at 2.8s block time). The `resolve()` call reads beacon output synchronously via inner app call after that round passes. -- **Minimum commit round is N+8.** N+4 can arrive before the user's transaction confirms under congestion. Never reduce this parameter. -- **`BEACON_SETTLE_BUFFER = 4`.** The beacon writes proofs up to 3 rounds after the N+8 target. `resolve()` asserts `current_round >= commit_round + BEACON_SETTLE_BUFFER` before calling `must_get()`. Using a buffer of 2 risks calling `must_get()` before the proof exists → panic → unresolvable session. -- **48-hour player-triggered refund backdoor** is required in all game contracts. Keeper failure must never lock player funds. -- `resolve()` is permissionless — any wallet can call it once the VRF round has passed. Keeper calls it; players can call it themselves. -- Idempotency key: `beaconRound + walletAddress + sessionNonce` prevents double-payout on retry. - -### TESTNET_BEACON_APP_ID - -The research cites `110096026` but this must be verified via `algorand` MCP (`api_algod_get_application_by_id`) before any testnet integration tests are written. Never hardcode without verification. - -### Box Storage - -- Max box size: 2500 bytes. -- Max box references per app call: 8. -- Box MBR: `2500 + 400 * (key_length + value_length)` microALGO must be pre-funded before box access, or the transaction fails with `invalid box reference`. -- CometaFlip box per player: key = address (32 bytes), value = vrf_round (8) + bet_amount (8) + salt_hash (32) + referrer (32) = **80 bytes**. No `claimed` field — box deletion is the idempotency guard. `BOX_MBR = 49,300 microALGO` (prefix `flip:` = 5 bytes → `2500 + 400*(37+80) = 49,300`). Include this in bet transaction payment. -- Never delete the flip box until all inner transactions (payout, referral transfer) have been submitted. - -### House Treasury Architecture - -- `house_treasury` contract is the single shared bankroll. It must be deployed **before** any game contract. All game contracts read `get_available_balance()` via foreign app ref. -- **Capital flow (decided):** bets are escrowed in the coinflip contract during the pending phase. On `resolve()`, the stake is swept to `house_treasury` via inner transfer before paying winners out of the treasury. On `refund()`, the bet + MBR are paid directly from the coinflip contract's own balance — the treasury is never touched during a refund, so the 48-hour player backdoor works even when the treasury is paused or the keeper is down. -- `max_payout_bps` default: 100 (1% of live treasury balance). Enforced at `resolve()` time — not at bet time. The balance can change between bet and resolve. -- `emergency_pause` flag halts all game contracts that check it. **It does not affect `refund()`** — refunds pay from the game contract directly. -- **Minimum viable treasury before any public announcement:** 2,000 ALGO for CometaFlip v1. Before Foundation amplification: 5,000-10,000 ALGO. Sub-2000 ALGO is below the solvency floor for a game with 0.5 ALGO max bets. - -### Inner Transaction Limits - -- Max 256 inner transactions per group. -- Max 16 transactions per atomic group. - -### ARC-56 Pipeline - -`algokit-client-generator@6.0.1` fully accepts ARC-56 input from `puyapy 5.8.1`. The npm description ("ARC-0032") is stale. Clients have been generated successfully — see `packages/sdk/src/clients/`. No `--output-arc32` fallback needed. v7.0.0-beta is in progress; stay on 6.0.1 until it stabilizes. - ---- - -## Degen Games Context - -### Platform Thesis - -The five games are five entry points into one identity system, not five separate products: - -- **Shared house treasury** (`house_treasury` contract) — accumulates rake from all games. Seeded at CometaFlip launch. -- **Cross-game leaderboard** — single box storage contract tracking per-wallet: games played, net ALGO P&L, jackpot hits, last active round. -- **Proof card engine** — one PNG generator endpoint, all games share it with different templates. - -### Game Roadmap - -| Phase | Game | Status | Contract Lines | House Pool Requirement | -| ----- | --------------------- | --------------------- | -------------------------------------- | --------------------------------- | -| 1 | CometaFlip (coinflip) | Building | ~150-200 Puya | 2,000 ALGO seed, 0.5 ALGO max bet | -| 2 | Algo Minefield | Planned (Weeks 2-10) | ~500-700 Puya | 5,000-10,000 ALGO before launch | -| 3 | Algo Oracle Games | Planned (Weeks 8-14) | ~80% reuse from prediction-market repo | Keeper pattern from Minefield | -| 4 | PackFight | Planned (Weeks 14-20) | ~700 Puya | Established player base required | -| 5 | Memecoin Death Race | Planned (Weeks 18-26) | ~600-800 Puya | ASA partner confirmation required | - -### What NOT to Build in v1 - -- No jackpot in CometaFlip v1 — add in v1.2 after measuring actual bets/day. -- No COMETA/MINE token until 500+ weekly active wallets. -- No crash/multiplier mode until CometaFlip confirms 50+ daily players. -- No ASA denomination in CometaFlip v1.0 — ALGO only; add in v1.1 as parameter change. -- No sSYN yield-bearing receipt token — security classification in every jurisdiction. -- No mobile-native app — Pera deep-links handle mobile. - -### Economics - -- House edge: 2% on all house-banked games (parimutuel games hold zero race risk — rake only). -- Referral: 0.5% of house rake to optional referrer wallet parameter in bet transaction (~20 lines of Puya). -- Max payout: 1% of live treasury balance enforced per `resolve()` app call. -- Auto-pause threshold: treasury drops below 2,000 ALGO. - -### Tokenomics (Phase 2 only) - -Fixed-supply token, zero emission, 20% of weekly house gross buys-and-burns from Tinyman. Token utility: proof card skin variants, leaderboard badge color, 1.5x jackpot draw weight. **No revenue share to token holders** — triggers SEC/CFTC security classification. Only after 500+ weekly active wallets and $50K+/month revenue. - ---- - -## Compliance / Regulatory - -**This is house-banked gambling.** Treating it as anything else operationally is a legal risk, not a framing choice. - -### Geo-blocking (Required Before Any Public Announcement) - -Blocked jurisdictions: **US, UK, TH (Thailand), ID (Indonesia), IN (India), BR (Brazil)**. - -Geo-block spec lives in `ops/geo-blocking.md`. Cloudflare Workers geo-block is preferred over IP detection middleware in `packages/api` — CDN layer cannot be bypassed by changing Accept-Language headers. - -**Thai block is non-negotiable.** Thailand bans online gambling including crypto formats, with active enforcement (220K+ URLs blocked in 3.5 months). Operating from Bangkok without blocking Thai IPs is direct legal exposure. - -The geo-block must be live and verified before submitting CometaFlip to any ecosystem newsletter or requesting a Foundation RT. - -### Foundation Amplification Strategy - -Framing: "VRF tech demonstration that also pays out." The Algorand Foundation amplifies open-source VRF tooling and provably-fair mechanics. It does not amplify casino games. These two facts must coexist in all public messaging. Never describe the platform as gambling in any content targeting Foundation channels. - -### xGov Path - -A shipped open-source on-chain game with VRF proof infrastructure and 10K+ on-chain bets qualifies for retroactive xGov at Medium tier (50K-250K ALGO, ~$5,900-$29,500). Open-source the contracts from day one. - -### Licensing - -At 0.5 ALGO max bets, sub-$1 stakes, crypto-only, no fiat on-ramp: enforcement interest threshold is below the horizon for a solo operator. Invest in Curacao licensing at $50K+/month revenue when it becomes the actual constraint — not before. - -### Parimutuel vs House-Banked Tradeoff - -Parimutuel (Memecoin Death Race, Algo Oracle Games): house holds zero outcome risk, only rake. Regulatory profile is cleaner in most jurisdictions. House-banked (CometaFlip, Algo Minefield): house takes directional risk, but simpler mechanics and no liquidity dependency. CometaFlip ships first precisely because house-banked math is tractable at 2,000 ALGO seed + 0.5 ALGO max bet. - ---- - -## Environment Variables - -All env vars are validated with Zod at startup. `bigint` amounts use `z.coerce.bigint()`. - -| Variable | Required | Default | Notes | -| -------------------------------- | ---------------- | -------------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------- | ----- | -| `NODE_ENV` | Yes | — | `development | production | test` | -| `PORT` | No | 3010 | API HTTP port | -| `DATABASE_URL` | Yes | — | `postgresql://user:pass@fairground-db:5432/fairground` | -| `REDIS_URL` | Yes | — | `redis://fairground-redis:6379` | -| `ALGOD_URL` | Yes | AlgoNode mainnet | Use `https://localhost:4001` for LocalNet | -| `ALGOD_TOKEN` | No | `""` | Empty string for public AlgoNode endpoints | -| `INDEXER_URL` | Yes | AlgoNode mainnet idx | — | -| `ALGORAND_NETWORK` | No | `localnet` | `.claude/settings.json` default. Override explicitly for mainnet operations | -| `HOUSE_TREASURY_APP_ID` | Yes (production) | — | `bigint`. Set after first deploy. Required before accepting bets | -| `COINFLIP_APP_ID` | Yes (production) | — | `bigint`. Required for coinflip routes | -| `VRF_BEACON_APP_ID` | No | `947957720n` | Defined in `@fairground/sdk` as constant. Override for testnet | -| `HOUSE_SEED_WALLET_MNEMONIC` | Yes (keeper) | — | Never commit. `block-secret-commit.sh` hook guards staging | -| `MIN_BET_MICROALGO` | No | `500000n` | Schema default. Mainnet prod: `1000000n` (1 ALGO, raised from 0.1 on 2026-06-12) | -| `MAX_BET_MICROALGO` | No | `500000n` | Schema default. Mainnet prod: `20000000n` (20 ALGO). Contract also caps payout vs treasury | -| `TREASURY_MIN_BALANCE_MICROALGO` | No | `2000000000n` | 2,000 ALGO auto-pause threshold | -| `CORS_ORIGINS` | No | localhost:3000 | Comma-separated. Split via Zod transform | -| `KEEPER_INSTANCE_ID` | Yes (keeper) | — | `primary | standby`. Used as Redis SETNX lock value | -| `PLAUSIBLE_DOMAIN` | No | — | `fairground.xyz` for landing analytics | -| `WALLETCONNECT_PROJECT_ID` | Yes (game app) | — | Register at cloud.walletconnect.com. Prefixed `NEXT_PUBLIC_` at build time | - ---- - -## MCP Servers - -See `.mcp.json` for project MCP config. - -| Server | Purpose | Routing Rule | -| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -| `algorand` | VRF beacon inspection (app 947957720), contract state reads, `simulate_transactions` before mainnet spend, competitor monitoring, algod/indexer queries | Always. Default `ALGORAND_NETWORK=localnet` during contract dev. Switch to `mainnet` for explicit inspection tasks | -| `context7` | Verify real API signatures for algokit-utils, algosdk v3, use-wallet, Hono, Drizzle before writing code | Before implementing any call against a versioned library. Prevents version hallucinations | -| `playwright` | Visual QA of game dApp before deploy, competitor scraping (Alpha Arcade, Haystack PVP), proof-card rendering verification | Before any production deploy. Also for Alpha Arcade monitoring | -| `perplexity` | Algorand ecosystem competitive analysis, regulatory landscape, xGov grant opportunities | Multi-source synthesis only — 10+ sources needed. Inherited from global `~/.claude/mcp.json` | -| `vestige` | ALGO/USD price feeds for jackpot display and treasury balance USD conversion | If `@goplausible/vestige-mcp` fails to load, fall back to WebFetch against `api.vestigelabs.org` | - -`context7` and `perplexity` are inherited from global `~/.claude/mcp.json`. Do not duplicate in `.mcp.json`. - ---- - -## Open Questions (Resolve Before Coding the Affected Area) - -1. ~~**ARC-56 pipeline:**~~ **RESOLVED (2026-06-01).** `algokit-client-generator@6.0.1` accepts ARC-56 artifacts from `puyapy 5.8.1`. Clients generated successfully in `packages/sdk/src/clients/`. No `--output-arc32` fallback needed. -2. **TESTNET_BEACON_APP_ID:** Research cites `110096026`. Verify via `algorand` MCP before writing any testnet integration tests. -3. **`@goplausible/vestige-mcp` availability:** Verify `npx -y @goplausible/vestige-mcp` loads before relying on it. -4. **Geo-blocking implementation:** Cloudflare Workers (preferred, CDN layer) vs IP middleware in `packages/api`. Decision required before public announcement. -5. ~~**HouseTreasury architecture:**~~ **RESOLVED (2026-06-01).** Shared bankroll design: bets escrow in the game contract during pending, then swept to `house_treasury` on `resolve()`. Winners paid from treasury. `refund()` pays from the game contract directly — no treasury dependency. This keeps the 48-hour refund backdoor functional regardless of treasury pause state. -6. **Domain:** `fairground.xyz` vs `fairground.app` vs `fairground.bet`. The `.bet` TLD signals gambling to payment processors. `.xyz` is neutral. -7. **WalletConnect Project ID:** Register at cloud.walletconnect.com before any wallet integration testing. Required at build time. -8. **Proof card CDN timing:** `api.fairground.xyz/proof/:txnId` must be publicly accessible before the first game goes public — Twitter card previews require a live URL at tweet time. - ---- - -## Skills Index - -See `SKILLS.md` for the full index of project skills and when to invoke each. - -## Sibling Projects - -| Project | Path | Relevance | -| ------------------- | -------------------------------- | ------------------------------------------------------------------------------- | -| `cometa-strategy` | `~/dev/cometa/cometa-strategy` | Design bible source: `research/algorand-degen-games-2026-05.md` | -| `prediction-market` | `~/dev/cometa/prediction-market` | Puya v5.0 patterns, box storage, AlgoKit workspace structure to reuse | -| `metafarm-frontend` | `~/dev/cometa/metafarm-frontend` | `walletConnectService.ts` iOS relayer-wake hook to port into `useRelayerWake()` | - -**Cometa VPS** (`hostinger`, 72.60.104.156): Fairground runs in a **separate Docker Compose stack** (`~/fairground/docker-compose.yml`) from Cometa (`~/cometa/docker-compose.yml`). Container crashes are isolated between the two stacks. +Local URLs, VPS/SSH shortcuts, and anything that would be noise in a shared-source file go to +`CLAUDE.local.md`, never here. That file doesn't exist yet, and `.gitignore` has no rule for it — +add one before creating it. diff --git a/Makefile b/Makefile index 7cd74ce..280aa0c 100644 --- a/Makefile +++ b/Makefile @@ -50,14 +50,16 @@ format-check: contracts-build: cd packages/contracts && algokit project run build -# Run after contracts-build. Outputs ARC-56 JSON to packages/contracts/artifacts/. +# Compiled ARC-56 JSON lives in packages/contracts/smart_contracts//artifacts/. contracts-test: cd packages/contracts && algokit project run test # Regenerate typed TS clients in packages/sdk/src/clients/ from ARC-56 artifacts. # Run after any contract ABI change. Never edit generated clients by hand. # algokit-client-generator@6.0.1 wants one arc56.json -> one output file per contract. -# Artifacts live in per-contract subdirs after `algokit compile python smart_contracts/`. +# Artifacts land in per-contract subdirs only when each contract is compiled explicitly with +# `algokit compile python smart_contracts//contract.py --out-dir artifacts` -- without +# --out-dir, puyapy 5.8.1 writes beside contract.py, not into artifacts/. contracts-generate: algokit generate client \ --language typescript \ diff --git a/README.md b/README.md index 2cc8428..5109b1e 100644 --- a/README.md +++ b/README.md @@ -109,12 +109,15 @@ cd packages/contracts algokit project bootstrap source .venv/bin/activate -# Compile all contracts (outputs ARC-56 JSON to artifacts/) -algokit compile py smart_contracts/ +# Compile each contract explicitly with --out-dir artifacts. Without --out-dir, puyapy 5.8.1 +# writes beside contract.py, not into smart_contracts//artifacts/. +algokit compile python smart_contracts/coinflip/contract.py --out-dir artifacts +algokit compile python smart_contracts/house_treasury/contract.py --out-dir artifacts +algokit compile python smart_contracts/leaderboard/contract.py --out-dir artifacts +algokit compile python smart_contracts/fairjackpot/contract.py --out-dir artifacts -# Generate typed TS clients (outputs to packages/sdk/src/clients/) -algokit generate client artifacts/house_treasury.arc56.json --output ../sdk/src/clients/HouseTreasuryClient.ts -algokit generate client artifacts/coinflip.arc56.json --output ../sdk/src/clients/CoinflipClient.ts +# Generate typed TS clients into packages/sdk/src/clients/ (all four contracts) +(cd ../.. && make contracts-generate) # Run contract tests (requires LocalNet) algokit localnet start @@ -155,21 +158,21 @@ Never deploy game contracts before the treasury is funded. Never make a public a ## Stack at a Glance -| Layer | Tech | Version | -|-------|------|---------| -| Contracts | Puya (algorand-python + puyapy) | algorand-python 3.5.0, puyapy 5.8.1 | -| TS SDK | algosdk + algokit-utils + generated clients | algosdk 3.5.2 | -| Wallet | @txnlab/use-wallet-react | 4.6.0 | -| API | Hono + @hono/node-server | 4.12.23 | -| Game dApp | Next.js App Router | 16.2.6 | -| Landing | Astro | 6.4.2 | -| Database | Drizzle ORM + PostgreSQL | drizzle-orm 0.45.2 | -| Realtime | Hono WS + ioredis pub/sub | ws 8.21.0 | -| Keeper | tsx polling + Redis SETNX | tsx 4.22.3 | -| Proof card | satori + @resvg/resvg-js + sharp | satori 0.26.0 | -| Styling | Tailwind CSS 4 | 4.3.0 | -| Tests | Vitest | 4.1.7 | -| Monorepo | pnpm workspaces + Turborepo | pnpm 11.5.0, turbo 2.9.16 | +| Layer | Tech | Version | +| ---------- | ------------------------------------------- | ----------------------------------- | +| Contracts | Puya (algorand-python + puyapy) | algorand-python 3.5.0, puyapy 5.8.1 | +| TS SDK | algosdk + algokit-utils + generated clients | algosdk 3.5.2 | +| Wallet | @txnlab/use-wallet-react | 4.6.0 | +| API | Hono + @hono/node-server | 4.12.23 | +| Game dApp | Next.js App Router | 16.2.6 | +| Landing | Astro | 6.4.2 | +| Database | Drizzle ORM + PostgreSQL | drizzle-orm 0.45.2 | +| Realtime | Hono WS + ioredis pub/sub | ws 8.21.0 | +| Keeper | tsx polling + Redis SETNX | tsx 4.22.3 | +| Proof card | satori + @resvg/resvg-js + sharp | satori 0.26.0 | +| Styling | Tailwind CSS 4 | 4.3.0 | +| Tests | Vitest | 4.1.7 | +| Monorepo | pnpm workspaces + Turborepo | pnpm 11.5.0, turbo 2.9.16 | ## Links diff --git a/docs/architecture.md b/docs/architecture.md index b5a668c..a61a0e6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2,22 +2,22 @@ ## Stack -| Layer | Tech | Version | Rationale | -|-------|------|---------|-----------| -| Monorepo | pnpm workspaces + Turborepo | pnpm 11.5.0, turbo 2.9.16 | pnpm strict isolation prevents accidental cross-package dep leakage; turbo caches build/test/lint tasks, saving 40-60s per iteration | -| Contracts | algorand-python + puyapy | 3.5.0 / 5.8.1 | Algorand Foundation standard toolchain. puyapy 5.8.1 outputs ARC-56 JSON; `algokit generate client` produces typed TS clients. Never write "Puya v5.0" in technical contexts — pip packages are `algorand-python` and `puyapy` with separate version numbers. AlgoKit 3.x does not exist. | -| SDK | algosdk + algokit-utils + generated clients | 3.5.2 / 9.2.0 / 6.0.1 | Generated clients from `algokit-client-generator@6.0.1` are the only interface between TS code and contracts. No raw algosdk group construction in apps. | -| Wallet | @txnlab/use-wallet-react | 4.6.0 | Handles Pera (native SDK), Defly, Lute, Kibisis, Exodus behind a single `useWallet()` hook. Replaces 500+ lines of hand-rolled WC v2 session management. | -| API | Hono + @hono/node-server | 4.12.23 / 2.0.4 | 3 HTTP routes per game + 1 WS namespace. Web Standard API enables future Cloudflare Worker deployment for the proof-card CDN endpoint. | -| Web | Next.js App Router (game) + Astro (landing) | 16.2.6 / 6.4.2 | Game dApp needs RSC for leaderboard/history pages + client components for game canvas + WS. Astro for static landing (zero JS for non-interactive sections). | -| DB | Drizzle ORM + PostgreSQL | 0.45.2 / pg 8.21.0 | Three tables per game + shared treasury/jackpot. Drizzle generates pure SQL, no binary generation. Keeper batch UPDATE is expressible verbatim. | -| Realtime | Hono WS + ioredis pub/sub | ws 8.21.0 / ioredis 5.11.0 | Keeper publishes to Redis pub/sub; Hono WS subscribes and fans out. Keeper crashes don't drop WS connections. | -| Keeper | tsx polling + Redis SETNX | tsx 4.22.3 | VRF resolution is not a retryable job — each event is a unique on-chain outcome. Two containers, SETNX leader election (10s TTL, refreshed every 4s). | -| Proof card | satori + @resvg/resvg-js + sharp | 0.26.0 / 2.6.2 / 0.34.5 | Generates shareable VRF result PNGs. Redis caches `proof:{txnId}` permanently (proofs are immutable). | -| Styling | Tailwind CSS 4 | 4.3.0 | OKLCH design tokens. Amber-gold primary `oklch(0.78 0.18 65)`. | -| Testing | Vitest | 4.1.7 | Native ESM. Contract integration tests hit AlgoKit LocalNet (Docker). | -| VRF | Algorand VRF Beacon | app ID 947957720 (mainnet) | Free. Commit-reveal: bet commits to round N+8 (~22s). `resolve()` reads beacon output synchronously. 48h player-triggered refund backdoor required in all game contracts. | -| Deploy | Docker Compose on Hostinger VPS | Separate from Cometa stack | `~/fairground/docker-compose.yml`. Landing deploys as static files via rsync. | +| Layer | Tech | Version | Rationale | +| ---------- | ------------------------------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Monorepo | pnpm workspaces + Turborepo | pnpm 11.5.0, turbo 2.9.16 | pnpm strict isolation prevents accidental cross-package dep leakage; turbo caches build/test/lint tasks, saving 40-60s per iteration | +| Contracts | algorand-python + puyapy | 3.5.0 / 5.8.1 | Algorand Foundation standard toolchain. puyapy 5.8.1 outputs ARC-56 JSON; `algokit generate client` produces typed TS clients. Never write "Puya v5.0" in technical contexts — pip packages are `algorand-python` and `puyapy` with separate version numbers. AlgoKit 3.x does not exist. | +| SDK | algosdk + algokit-utils + generated clients | 3.5.2 / 9.2.0 / 6.0.1 | Generated clients from `algokit-client-generator@6.0.1` are the only interface between TS code and contracts. No raw algosdk group construction in apps. | +| Wallet | @txnlab/use-wallet-react | 4.6.0 | Handles Pera (native SDK), Defly, Lute, Kibisis, Exodus behind a single `useWallet()` hook. Replaces 500+ lines of hand-rolled WC v2 session management. | +| API | Hono + @hono/node-server | 4.12.23 / 2.0.4 | 3 HTTP routes per game + 1 WS namespace. Web Standard API enables future Cloudflare Worker deployment for the proof-card CDN endpoint. | +| Web | Next.js App Router (game) + Astro (landing) | 16.2.6 / 6.4.2 | Game dApp needs RSC for leaderboard/history pages + client components for game canvas + WS. Astro for static landing (zero JS for non-interactive sections). | +| DB | Drizzle ORM + PostgreSQL | 0.45.2 / pg 8.21.0 | Three tables per game + shared treasury/jackpot. Drizzle generates pure SQL, no binary generation. Keeper batch UPDATE is expressible verbatim. | +| Realtime | Hono WS + ioredis pub/sub | ws 8.21.0 / ioredis 5.11.0 | Keeper publishes to Redis pub/sub; Hono WS subscribes and fans out. Keeper crashes don't drop WS connections. | +| Keeper | tsx polling + Redis SETNX | tsx 4.22.3 | VRF resolution is not a retryable job — each event is a unique on-chain outcome. Two containers, SETNX leader election (10s TTL, refreshed every 4s). | +| Proof card | satori + @resvg/resvg-js + sharp | 0.26.0 / 2.6.2 / 0.34.5 | Generates shareable VRF result PNGs. Redis caches `proof:{txnId}` permanently (proofs are immutable). | +| Styling | Tailwind CSS 4 | 4.3.0 | OKLCH design tokens. Amber-gold primary `oklch(0.78 0.18 65)`. | +| Testing | Vitest | 4.1.7 | Native ESM. Contract integration tests hit AlgoKit LocalNet (Docker). | +| VRF | Algorand VRF Beacon | app ID 947957720 (mainnet) | Free. Commit-reveal: bet commits to round N+8 (~22s). `resolve()` reads beacon output synchronously. 48h player-triggered refund backdoor required in all game contracts. | +| Deploy | Docker Compose on Hostinger VPS | Separate from Cometa stack | `~/fairground/docker-compose.yml`. Landing deploys as static files via rsync. | ## Monorepo @@ -33,8 +33,7 @@ fairground/ │ │ ├── smart_contracts/ │ │ │ ├── house_treasury/ │ │ │ ├── coinflip/ -│ │ │ └── leaderboard/ -│ │ ├── artifacts/ # *.arc56.json (puyapy output, never edit by hand) +│ │ │ └── leaderboard/ # each contract dir holds artifacts/*.arc56.json (never edit by hand) │ │ └── tests/ │ ├── sdk/ # @fairground/sdk │ │ └── src/ @@ -63,10 +62,10 @@ fairground/ 2. `coinflip` — reads `HOUSE_TREASURY_APP_ID` at construction time. 3. `leaderboard` — receives inner app calls from game contracts on every resolution. -Compile: `algokit compile py smart_contracts/` -Generate clients: `algokit generate client artifacts/coinflip.arc56.json --output ../sdk/src/clients/CoinflipClient.ts` +Compile: one `algokit compile python smart_contracts//contract.py --out-dir artifacts` call per contract (writes to per-contract `smart_contracts//artifacts/`, not a top-level `artifacts/`) — without `--out-dir`, puyapy 5.8.1 writes beside `contract.py` instead +Generate clients: `make contracts-generate` (repo root) — one `algokit generate client --language typescript --output ` call per contract, see `Makefile` -Open question: verify that `algokit-client-generator@6.0.1` accepts ARC-56 input from `puyapy 5.8.1`. Compile one contract, run the generator, check the output shape before committing to the pipeline. Fallback: use `--output-arc32` flag in puyapy if the generator rejects ARC-56. +Resolved: `algokit-client-generator@6.0.1` fully accepts ARC-56 input from `puyapy 5.8.1` — its npm description says "ARC-0032" but that's a stale description, not a support claim. All four clients in `packages/sdk/src/clients/` were generated this way; no `--output-arc32` fallback is needed. ## Data Flow @@ -215,24 +214,24 @@ ssh hostinger "cd ~/fairground && git checkout && docker compose u All required env vars validated with Zod at startup. Missing required vars crash the process immediately with a descriptive error. -| Variable | Required | Default | Notes | -|----------|----------|---------|-------| -| `NODE_ENV` | yes | — | `development\|production\|test` | -| `PORT` | no | `3010` | API HTTP port | -| `DATABASE_URL` | yes | — | `postgresql://user:pass@fairground-db:5432/fairground` | -| `REDIS_URL` | yes | — | `redis://fairground-redis:6379` | -| `ALGOD_URL` | yes | — | `https://mainnet-api.algonode.cloud` | -| `ALGOD_TOKEN` | no | `""` | Empty for public AlgoNode | -| `INDEXER_URL` | yes | — | `https://mainnet-idx.algonode.cloud` | -| `ALGORAND_NETWORK` | no | `localnet` | Claude Code default — forces explicit override for mainnet | -| `HOUSE_TREASURY_APP_ID` | yes | — | bigint via Zod coerce | -| `COINFLIP_APP_ID` | yes | — | bigint via Zod coerce | -| `VRF_BEACON_APP_ID` | no | `947957720` | bigint; override for testnet | -| `HOUSE_SEED_WALLET_MNEMONIC` | yes (keeper) | — | Never commit. 25-word mnemonic. | -| `MIN_BET_MICROALGO` | no | `500000` | 0.5 ALGO | -| `MAX_BET_MICROALGO` | no | `500000` | 0.5 ALGO | -| `TREASURY_MIN_BALANCE_MICROALGO` | no | `2000000000` | 2000 ALGO | -| `CORS_ORIGINS` | no | localhost + prod | Comma-separated | -| `KEEPER_INSTANCE_ID` | yes (keeper) | — | `primary\|standby` | -| `PLAUSIBLE_DOMAIN` | no | — | `fairground.xyz` | -| `WALLETCONNECT_PROJECT_ID` | yes (game) | — | Register at cloud.walletconnect.com | +| Variable | Required | Default | Notes | +| -------------------------------- | ------------ | ---------------- | ---------------------------------------------------------- | +| `NODE_ENV` | yes | — | `development\|production\|test` | +| `PORT` | no | `3010` | API HTTP port | +| `DATABASE_URL` | yes | — | `postgresql://user:pass@fairground-db:5432/fairground` | +| `REDIS_URL` | yes | — | `redis://fairground-redis:6379` | +| `ALGOD_URL` | yes | — | `https://mainnet-api.algonode.cloud` | +| `ALGOD_TOKEN` | no | `""` | Empty for public AlgoNode | +| `INDEXER_URL` | yes | — | `https://mainnet-idx.algonode.cloud` | +| `ALGORAND_NETWORK` | no | `localnet` | Claude Code default — forces explicit override for mainnet | +| `HOUSE_TREASURY_APP_ID` | yes | — | bigint via Zod coerce | +| `COINFLIP_APP_ID` | yes | — | bigint via Zod coerce | +| `VRF_BEACON_APP_ID` | no | `947957720` | bigint; override for testnet | +| `HOUSE_SEED_WALLET_MNEMONIC` | yes (keeper) | — | Never commit. 25-word mnemonic. | +| `MIN_BET_MICROALGO` | no | `500000` | 0.5 ALGO | +| `MAX_BET_MICROALGO` | no | `500000` | 0.5 ALGO | +| `TREASURY_MIN_BALANCE_MICROALGO` | no | `2000000000` | 2000 ALGO | +| `CORS_ORIGINS` | no | localhost + prod | Comma-separated | +| `KEEPER_INSTANCE_ID` | yes (keeper) | — | `primary\|standby` | +| `PLAUSIBLE_DOMAIN` | no | — | `fairground.xyz` | +| `WALLETCONNECT_PROJECT_ID` | yes (game) | — | Register at cloud.walletconnect.com | diff --git a/docs/game-specs/coinflip.md b/docs/game-specs/coinflip.md index 4537577..96573ba 100644 --- a/docs/game-specs/coinflip.md +++ b/docs/game-specs/coinflip.md @@ -2,7 +2,7 @@ **Contract name:** `CoinflipContract` **Source:** `packages/contracts/smart_contracts/coinflip/` -**ARC-56 artifact:** `packages/contracts/artifacts/coinflip.arc56.json` +**ARC-56 artifact:** `packages/contracts/smart_contracts/coinflip/artifacts/CoinflipContract.arc56.json` **TS client:** `packages/sdk/src/clients/CoinflipClient.ts` (generated, never edit) ## Core Loop diff --git a/packages/contracts/.algokit.toml b/packages/contracts/.algokit.toml index e87a2f3..aee9ac0 100644 --- a/packages/contracts/.algokit.toml +++ b/packages/contracts/.algokit.toml @@ -15,7 +15,7 @@ algorand_network = "localnet" client_generator = "client_ts" client_output_path = "../../packages/sdk/src/clients" # NOTE: puyapy 5.8.1 outputs ARC-56 by default. -# algokit-client-generator@6.0.1 accepts ARC-32 (its npm description says ARC-0032). -# Until ARC-56 acceptance is verified, add --output-arc32 to puyapy compile args. -# Test: compile one contract, run 'algokit generate client', inspect generated output. -# If generation fails, add 'compile_options = ["--output-arc32"]' below. +# RESOLVED: algokit-client-generator@6.0.1 fully accepts ARC-56 input despite its npm +# description saying "ARC-0032" (stale description, not a support claim). No +# --output-arc32 fallback needed -- all four clients in packages/sdk/src/clients/ were +# generated this way. See packages/contracts/README.md "ARC-56 vs ARC-32 note". diff --git a/packages/contracts/README.md b/packages/contracts/README.md index 5d1f622..ca4e8e2 100644 --- a/packages/contracts/README.md +++ b/packages/contracts/README.md @@ -83,28 +83,24 @@ poetry install ```bash cd packages/contracts -# Compile all contracts to ARC-56 artifacts (outputs to artifacts/) -algokit compile python smart_contracts/ - -# Individual contract: -algokit compile python smart_contracts/house_treasury/ -algokit compile python smart_contracts/coinflip/ -algokit compile python smart_contracts/leaderboard/ +# Compile each contract explicitly with --out-dir artifacts (outputs to that contract's own +# artifacts/ dir). Without --out-dir, puyapy 5.8.1 writes beside contract.py instead. +algokit compile python smart_contracts/house_treasury/contract.py --out-dir artifacts +algokit compile python smart_contracts/coinflip/contract.py --out-dir artifacts +algokit compile python smart_contracts/leaderboard/contract.py --out-dir artifacts +algokit compile python smart_contracts/fairjackpot/contract.py --out-dir artifacts ``` -Compiled artifacts are written to `packages/contracts/artifacts/` as `*.arc56.json` files. -These are auto-generated — never edit by hand. +Compiled artifacts are written to each contract's own `smart_contracts//artifacts/` as +`*.arc56.json` files — there is no top-level `packages/contracts/artifacts/`. These are +auto-generated — never edit by hand. ### ARC-56 vs ARC-32 note -puyapy 5.8.1 outputs ARC-56 JSON by default. `algokit-client-generator@6.0.1` describes -itself as "ARC-0032" in the npm description. Verify acceptance before committing to the -pipeline (see open question in `arch-spec.json`). If the generator rejects ARC-56, add -`--output-arc32` to the compile command: - -```bash -puyapy smart_contracts/ --output-arc32 --out-dir artifacts/ -``` +puyapy 5.8.1 outputs ARC-56 JSON by default. `algokit-client-generator@6.0.1` describes itself as +"ARC-0032" in the npm description, but that description is stale, not a support claim: the generator +fully accepts ARC-56 input — resolved, all four clients in `packages/sdk/src/clients/` were generated +this way, no `--output-arc32` fallback needed. --- @@ -113,11 +109,15 @@ puyapy smart_contracts/ --output-arc32 --out-dir artifacts/ After compiling, regenerate the typed TS clients consumed by `@fairground/sdk`: ```bash -cd packages/contracts - -algokit generate client artifacts/ --output ../../packages/sdk/src/clients/ +# from repo root +make contracts-generate ``` +This runs one `algokit generate client --language typescript --output ` call per contract +(see `Makefile`) — there is no single "regenerate everything" invocation; `algokit generate client +artifacts/ --output .../clients/` does not work (no top-level `artifacts/` dir, and without +`--language` or a per-file output the generator has nothing to select a generator from). + Generated files land in `packages/sdk/src/clients/`. Never edit them by hand. Regenerate whenever a contract interface changes (method signatures, struct fields). @@ -194,12 +194,13 @@ VRF beacon app IDs: ## Artifacts -`artifacts/` contains compiled ARC-56 JSON. This directory is committed to git -so that `packages/sdk` can regenerate TS clients without a full compile step in CI. -Files are auto-generated — never edit directly. +Each contract's compiled ARC-56 JSON lives in its own `smart_contracts//artifacts/` +directory. These files are committed so that `packages/sdk` can regenerate TS clients without a +full compile step in CI. Files are auto-generated — never edit directly. -Expected files after compile: +Files after compile: -- `artifacts/house_treasury.arc56.json` -- `artifacts/coinflip.arc56.json` -- `artifacts/leaderboard.arc56.json` +- `smart_contracts/coinflip/artifacts/CoinflipContract.arc56.json` +- `smart_contracts/fairjackpot/artifacts/FairJackpot.arc56.json` +- `smart_contracts/house_treasury/artifacts/HouseTreasury.arc56.json` +- `smart_contracts/leaderboard/artifacts/LeaderboardContract.arc56.json` diff --git a/packages/sdk/src/clients/README.md b/packages/sdk/src/clients/README.md index bebde8d..aaf185d 100644 --- a/packages/sdk/src/clients/README.md +++ b/packages/sdk/src/clients/README.md @@ -8,31 +8,28 @@ This directory contains typed ARC-56 clients auto-generated by ## Regenerate ```bash -# From repo root: -pnpm contracts:build # runs puyapy, writes artifacts/*.arc56.json -pnpm contracts:generate # runs algokit generate client for each artifact +# From repo root. Compile first: `make contracts-generate` only reads the committed ARC-56 files. +# Without --out-dir, puyapy 5.8.1 writes beside contract.py, not into artifacts/ -- compile each +# contract explicitly (pinned compiler: puyapy/compile.py:127-152 in algorandfoundation/puya @ v5.8.1). +(cd packages/contracts && \ + algokit compile python smart_contracts/coinflip/contract.py --out-dir artifacts && \ + algokit compile python smart_contracts/house_treasury/contract.py --out-dir artifacts && \ + algokit compile python smart_contracts/leaderboard/contract.py --out-dir artifacts && \ + algokit compile python smart_contracts/fairjackpot/contract.py --out-dir artifacts) +make contracts-generate ``` -Or manually: - -```bash -algokit generate client \ - packages/contracts/artifacts/HouseTreasury.arc56.json \ - --output packages/sdk/src/clients/HouseTreasuryClient.ts - -algokit generate client \ - packages/contracts/artifacts/CoinflipContract.arc56.json \ - --output packages/sdk/src/clients/CoinflipContractClient.ts - -algokit generate client \ - packages/contracts/artifacts/LeaderboardContract.arc56.json \ - --output packages/sdk/src/clients/LeaderboardContractClient.ts -``` +`pnpm contracts:build|generate` shell out to `algokit project run build|generate`, which is not +configured in `packages/contracts/.algokit.toml` ([project.run.*] is empty) — those wrappers don't +work. `make contracts-generate` is the one that does: it calls `algokit generate client` once per +contract, reading each contract's own `smart_contracts//artifacts/*.arc56.json` (there is +no top-level `packages/contracts/artifacts/`) — see `Makefile` for the exact four commands. ## Expected files after generation - `HouseTreasuryClient.ts` -- `CoinflipContractClient.ts` -- `LeaderboardContractClient.ts` +- `CoinflipClient.ts` +- `LeaderboardClient.ts` +- `FairJackpotClient.ts` The root `src/index.ts` re-exports these once they exist. diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 16e2643..bc1ed4b 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -1,9 +1,8 @@ // @fairground/sdk -- bridge between Puya contracts and TypeScript apps. // // src/clients/ is auto-generated by algokit-client-generator@6.0.1 -// from packages/contracts/artifacts/*.arc56.json. -// NEVER edit generated files by hand -- regenerate via: -// algokit generate client packages/contracts/artifacts/ --output packages/sdk/src/clients/ +// from each contract's own packages/contracts/smart_contracts//artifacts/*.arc56.json. +// NEVER edit generated files by hand -- regenerate via `make contracts-generate` (repo root). // Re-export VRF beacon utilities export * from './vrf/beacon.js';