feat: [T07] integrate token rewards#17
Open
pewpewgogo wants to merge 7 commits into
Open
Conversation
Adds the PostgreSQL schema for the snake leaderboard: - users: one row per player handle, with bearer token for X-Player-Token auth - sessions: one row per game played (start/end + final score + meta JSONB) - scores: append-only score feed indexed for top-N reads Indexes: - users_player_lower_uniq case-insensitive unique handle - users_api_token_uniq token lookup for write auth - sessions_user_started_idx recent sessions per user - scores_score_created_idx global top-N (ties resolved by earliest submit) - scores_user_score_idx best-score-per-user Migrations live as numbered .sql files under backend/migrations/ and are idempotent. backend/src/db.ts exposes a lazy pool plus a runMigrations() helper for tests/bootstrap so typecheck does not require a running DB. Adds pg + @types/pg to backend.
REST endpoints under /api: POST /api/users/register create user, returns X-Player-Token POST /api/scores submit a score (auth required) GET /api/leaderboard?limit=N top-N (limit clamped 1..100, default 10) GET /api/users/:id/best all-time best for one user Layered as: routes/leaderboard.ts -> repo.ts -> db.ts. SQL lives only in repo.ts; routes do validation + auth. Validation uses zod (handle regex, score >= 0 and bounded, limit clamp). Auth model: a single bearer token per user, sent as X-Player-Token. That's sufficient for a hobby leaderboard and avoids dragging in JWT/bcrypt for T05's scope. Tests: 9 integration tests under src/__tests__/leaderboard.test.ts using pg-mem to provide an in-memory PostgreSQL adapter, so CI does not need a real database. Migrations are applied through the same runMigrations() helper used in production. Test runner: node --test (--import tsx). Shared types extended additively: - SubmitScoreRequest gains optional `meta` - ScoreEntry gains optional `rank` - new SubmitScoreResponse / LeaderboardResponse / RegisterUserResponse Adds zod (runtime), pg-mem + supertest (tests).
…d-ui # Conflicts: # backend/package.json
Adds frontend/src/leaderboard/:
- api.ts tiny fetch wrapper with typed response + LeaderboardError
- Leaderboard.tsx responsive React component (table of top-N)
Behaviour:
- Polls GET /api/leaderboard every 5s by default (pollMs prop)
- Pauses polling when document.hidden so background tabs don't burn cycles
- Cancels in-flight requests on unmount / next refresh via AbortController
- Loading, error, and empty states; transient failures keep prior data
visible with a warning banner (no UI blanking on a network blip)
- Manual Refresh button
- Relative timestamps (Ns/Nm/Nh/Nd ago)
Styling:
- Themed to match T01's dark palette (#0a0f1c base)
- Gold/silver/bronze player colour for top-3
- Mobile breakpoint at 480px hides the When column for narrow screens
Wired into App.tsx alongside the Board from T02 (additive — game UI is
unchanged). Uses LeaderboardResponse / ScoreEntry from @snake/shared.
# Conflicts: # backend/package.json
…at/T07-token-rewards
Adds a SNAKE-token reward system layered onto the T05 score API. The
conversion logic is intentionally a placeholder this PR (one-line swap in
T08); everything else — schema, routes, idempotency, auth, UI — is real.
Backend:
- migrations/002_token_rewards.sql:
token_rewards(player_id, score_id, amount_nano BIGINT, reason, tier).
UNIQUE(score_id WHERE NOT NULL) gives idempotency for free.
- rewards/conversion.ts: pure computeReward({score}) — T07 returns
floor(score) * 1e9 nano under tier 'flat'. T08 will replace the body.
- rewards/repo.ts: SQL helpers for the new table.
- routes/rewards.ts:
POST /api/rewards/claim — auth-gated, idempotent,
403 if score belongs to another player, 404 if missing.
GET /api/rewards/me — player history + totals.
GET /api/rewards/leaderboard-bonuses — placeholder 100/50/25 SNAKE.
- server.ts: mount the new router under /api.
Shared:
- Adds RewardEntry, ClaimRewardRequest/Response, MyRewardsResponse,
LeaderboardBonusEntry/Response, RewardReason, SNAKE_NANO_PER_TOKEN.
All purely additive; no existing types touched.
Frontend:
- leaderboard/api.ts: registerPlayer + claimReward fetch wrappers.
- leaderboard/Leaderboard.tsx: identity input persisted to localStorage,
"Claim" button on rows that match the saved handle, inline status msg
showing minted SNAKE amount + tier label.
Tests:
- 9 new rewards.test.ts cases (pg-mem + supertest), covering the happy
path, idempotency, cross-player claim rejection, missing score, auth
failures, validation, history totals, and bonuses endpoint.
Off-chain bookkeeping only — no TON wallet calls, per task body.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 tasks
Contributor
|
✅ Revalidation passed. An admin will merge. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a SNAKE-token reward layer on top of the T05 score API. The conversion math is intentionally a placeholder in this PR (one-line swap planned in T08); everything else (schema, routes, idempotency, auth, UI) is real.
Backend
migrations/002_token_rewards.sql—token_rewards(player_id, score_id, amount_nano BIGINT, reason CHECK IN ('score','top1','top3','top10'), tier TEXT, created_at).UNIQUE(score_id) WHERE NOT NULLgives idempotent claim semantics for free.rewards/conversion.ts— purecomputeReward({ score }). T07 returnsfloor(score) * 1_000_000_000nano under tier'flat'. T08 will swap the body for tiered/multiplier logic.rewards/repo.ts— SQL helpers for the new table.routes/rewards.ts:POST /api/rewards/claim— auth viaX-Player-Token, idempotent (returns existing row withalreadyClaimed: true), 403 if the score belongs to another player, 404 if missing.GET /api/rewards/me— player history + totals.GET /api/rewards/leaderboard-bonuses— placeholder 100/50/25 SNAKE for top-1/2/3.server.tsmounts the new router under/api.Shared
Adds purely additive types:
RewardEntry,ClaimRewardRequest/Response,MyRewardsResponse,LeaderboardBonusEntry/Response,RewardReason,SNAKE_NANO_PER_TOKEN. No existing types changed.Frontend
leaderboard/api.ts—registerPlayerandclaimRewardwrappers.leaderboard/Leaderboard.tsx— small inline identity input persisted tolocalStorageplus aClaimbutton that appears only on rows matching the saved handle (case-insensitive). Inline message shows the minted SNAKE amount and tier label.Tests
Nine new
rewards.test.tscases (pg-mem + supertest), covering happy path, idempotency, cross-player claim rejection, missing score, auth failures, body validation, history totals, and the bonuses endpoint. All 18 backend tests pass.Off-chain bookkeeping only — no real wallet/TON calls, per task body.
This branch composes atop the open T05 (#13) and T06 (#15) branches.
Test plan
npm run typecheck— cleannpm run build— cleannpm test --workspace @snake/backend— 18/18 pass🤖 Generated with Claude Code