Skip to content

feat: [T07] integrate token rewards#17

Open
pewpewgogo wants to merge 7 commits into
agntdev:mainfrom
pewpewgogo:feat/T07-token-rewards
Open

feat: [T07] integrate token rewards#17
pewpewgogo wants to merge 7 commits into
agntdev:mainfrom
pewpewgogo:feat/T07-token-rewards

Conversation

@pewpewgogo
Copy link
Copy Markdown
Contributor

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.sqltoken_rewards(player_id, score_id, amount_nano BIGINT, reason CHECK IN ('score','top1','top3','top10'), tier TEXT, created_at). UNIQUE(score_id) WHERE NOT NULL gives idempotent claim semantics for free.
  • rewards/conversion.ts — pure computeReward({ score }). T07 returns floor(score) * 1_000_000_000 nano 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 via X-Player-Token, idempotent (returns existing row with alreadyClaimed: 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.ts mounts 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.tsregisterPlayer and claimReward wrappers.
  • leaderboard/Leaderboard.tsx — small inline identity input persisted to localStorage plus a Claim button 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.ts cases (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 — clean
  • npm run build — clean
  • npm test --workspace @snake/backend — 18/18 pass

🤖 Generated with Claude Code

pewpewgogo and others added 7 commits May 14, 2026 13:46
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).
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.
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>
@agnt-platform agnt-platform Bot added agnt:approved PR passed automated validation. agnt:manual-review PR is borderline — awaiting admin verdict. and removed agnt:approved PR passed automated validation. labels May 14, 2026
@agnt-platform agnt-platform Bot added agnt:approved PR passed automated validation. and removed agnt:manual-review PR is borderline — awaiting admin verdict. labels May 15, 2026
@agnt-platform
Copy link
Copy Markdown
Contributor

agnt-platform Bot commented May 15, 2026

✅ Revalidation passed. An admin will merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agnt:approved PR passed automated validation.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant