Indexer: live podium predictions from Postgres per category/epoch (GET /v1/arena/podiums) #273

Closed
opened 2026-06-01 03:31:02 +00:00 by PlasticDigits · 12 comments
PlasticDigits commented 2026-06-01 03:31:02 +00:00 (Migrated from gitlab.com)

Summary

Complete the #254 gap: while the arena is live, GET /v1/arena/podiums must return top-3 winner slots and scores derived from indexed Postgres (per category and current epoch), not from head TimeArena.podium(category) RPC alone. Consumers (Simple /arena, Arena views, play skills) use this route when VITE_INDEXER_URL is set.

Related: #254 (schema + HTTP baseline), #238 (epic), #247 (independent podium epochs), #252 (WarBow), frontend PODIUM_CONTRACT_CATEGORY_INDEX (#256).


Current codebase

HTTP (GET /v1/arena/podiums)

  • Handler: indexer/src/api_arena.rs (arena_podiums).
  • Winners / values come from the in-memory chain-timer snapshot (podium_contract[0..3]), which is populated by head RPC podium(cat) in indexer/src/chain_timer.rs.
  • Indexed data used today: only MAX(epoch) per category from idx_arena_podium_epoch (rolled epochs). Response always sets podium_prediction: true without building leaders from the DB.
  • Row category labels follow onchain index order (last_buy, time_booster, defended_streak, warbow), not the UX order the frontend expects when mapping rows by array index.

Frontend contract

Indexed inputs (partial)

Onchain category Index Leaderboard metric (live) Indexed today
Last Buy 0 Last 3 distinct buyers in current lastBuyEpoch (buyCount) idx_arena_buy (no epoch column; no order index)
Time Booster 1 totalEffectiveTimerSecAdded[buyer] not projected
Defended Streak 2 bestDefendedStreak[buyer] not projected
WarBow 3 battlePoints[buyer] in current podiumEpoch[3] partial via idx_warbow_epoch_score (post-log eth_call snapshots)

Settlement history: idx_arena_podium_epoch / view idx_arena_podium_snapshot — rolled epochs only, not live boards.

Product / onchain authority

Stale design note

docs/indexer/design.md (TimeCurve podiums section) still describes live DB predictions during !sale_ended; Arena v2 implementation was never completed — this issue tracks that work.


Why this is needed

  1. #254 acceptance explicitly requires GET /v1/arena/podiums: live predictions per category per epoch. #254 remains open on this slice.
  2. WarBow UX: head podium(3) is often empty mid-epoch while BP exists onchain; live sale UI must show indexed leaders (docs/indexer/design.md).
  3. Agent / API consumers (skills/play-active-time-arena) need a single HTTP source aligned with event history, not an extra RPC fan-out per page load.
  4. Row ordering: indexer response order must align with frontend UX slots or expose explicit category + stable sort so Simple/Arena do not swap WarBow and Time Booster.

Constraints and guardrails

  • AGPL repo; keep logic modular (docs/licensing.md).
  • No new authoritative business rules offchain — replicate onchain leaderboard inputs at ingest or read with block-tagged eth_call snapshots (same pattern as indexer/src/warbow_score.rs). Prefer event-sourced projections where events carry enough data; use bounded RPC only where logs omit scores.
  • One SQL transaction per block for ingest (#140); reorg rollback must include new tables (indexer/src/reorg.rs).
  • Epoch scope:
    • Last Buy leaders scoped to head lastBuyEpoch (from chain timer / LastBuyEpochStarted).
    • Categories 1–3 scoped to head podiumEpoch[cat] (RPC or idx_arena_podium_epoch + 1 for live window).
  • Arena v2 only — no TimeCurve tables or sale_ended gating (arena is live until paused).
  • After rollPodiumEpoch: live board for that category clears onchain; API may return empty slots or fall back to settled idx_arena_podium_epoch row for the just-finished epoch — document chosen behavior in API + invariants.
  • Performance: GET /v1/arena/podiums should stay O(1) or indexed top-3 queries, not full-table scans; target p95 acceptable on Anvil-rich dataset (see #255 wallet-stats note).
  • Schema version: bump SCHEMA_VERSION in indexer/src/api.rs and extend frontend/src/lib/indexerApi.ts types.

Relevant files

Area Paths
API indexer/src/api_arena.rs, indexer/src/chain_timer.rs
Ingest / decode indexer/src/ingestion.rs, indexer/src/decoder.rs, indexer/src/persist.rs, indexer/src/warbow_score.rs
Migrations indexer/migrations/
Tests indexer/tests/integration_stage2.rs
Frontend frontend/src/lib/indexerApi.ts, frontend/src/pages/arena/usePodiumReads.ts, frontend/src/pages/arena/podiumCopy.tsx
Docs / invariants docs/indexer/design.md, docs/testing/invariants-and-business-logic.md, docs/frontend/arena-views.md, skills/play-active-time-arena/SKILL.md
Onchain reference contracts/src/arena/TimeArena.sol, contracts/test/TimeArena.t.sol

  1. New projection table(s) (name TBD, e.g. idx_arena_podium_live or per-cat tables) storing, per (category, epoch, player): score, optional tie-break fields, block_number, updated_at — updated on ingest when Buy, WarBow PvP logs, or LastBuyEpochStarted affect leaders.
  2. Ingest enrichment module (e.g. indexer/src/arena_podium_live.rs):
    • On each qualifying log at block B, eth_call at B the same views TimeArena uses: buyCount, totalEffectiveTimerSecAdded, bestDefendedStreak, battlePoints (WarBow may reuse/extend warbow_score.rs).
    • Alternatively, port _updateTopThree math in Rust only if every input is recoverable from indexed events without drift — prefer snapshot-at-block when in doubt.
  3. arena_podiums read path:
    • Read head last_buy_epoch + podium_epoch[cat] from chain timer (extend snapshot if missing).
    • For each UX-ordered row (PODIUM_CONTRACT_CATEGORY_INDEX), query top-3 from Postgres for (cat, current_epoch).
    • Set podium_prediction: true when rows are DB-derived; set false when intentionally mirroring RPC (e.g. post-roll settled-only mode, if implemented).
    • Return rows in UX order (or add category_index + sort; update usePodiumReads if needed).
  4. Optional: GET /v1/arena/podiums?category=warbow later — out of scope unless trivial; single route is enough for v1.
  5. Close #254 only after this issue’s verification criteria pass.

Acceptance criteria

  • New migration(s) for live podium projection(s); included in ARENA_INDEX_TABLES / rollback_after.
  • Ingest persists live leaderboard state for all four categories on relevant events (at minimum every Buy + WarBow BP mutations).
  • GET /v1/arena/podiums returns top-3 winners + values from Postgres for the current epoch per category while the arena is live.
  • Response includes correct epoch (string decimal) per row matching onchain podiumEpoch / lastBuyEpoch semantics.
  • Rows are ordered for frontend UX (Last Buy, WarBow, Defended Streak, Time Booster) or frontend remaps by category without slot swap.
  • podium_prediction: true only when values are DB-derived; document any RPC fallback.
  • WarBow mid-epoch: non-zero leaders when idx_warbow_epoch_score / live table has data, even if head podium(3) is empty.
  • SCHEMA_VERSION bump + indexerApi.ts types updated.
  • INV-INDEXER-PODIUM-PREDICT-LIVE (or successor) added/updated in docs/testing/invariants-and-business-logic.md; cross-link docs/indexer/design.md.
  • integration_stage2.rs covers HTTP body shape + persist counts for at least two categories (e.g. Last Buy order + WarBow BP).

Test plan — functional paths

# Path Expected
1 Empty DB, chain timer ready GET /v1/arena/podiums → 200, four rows, zero/empty winners, epoch from head RPC
2 Single buyer, one DOUB buy Last Buy row shows buyer in 1st; buyCount score matches onchain at buy block
3 Two buyers, same lastBuyEpoch Last Buy top-3 order = last-three distinct buyers (3rd → 2nd → 1st buy order)
4 Buys adding timer seconds Time Booster top entrant matches totalEffectiveTimerSecAdded leader at head block
5 Streak buys under 15m Defended Streak leader matches bestDefendedStreak at head block
6 WarBow buys + steal WarBow top-3 matches battlePoints for current podiumEpoch[3]
7 LastBuyEpochStarted / hard reset Last Buy board resets for new epoch; prior-epoch buys not in live top-3
8 PodiumEpochRolled for cat 1 Live Time Booster board clears or switches to new epoch per documented API rule
9 Reorg rollback Live rows for reorged blocks removed; API consistent with surviving chain
10 chain_timer unavailable 503 chain_timer_unavailable (unchanged)
11 Frontend with VITE_INDEXER_URL Simple podium cards show same addresses as cast call podium(cat) at same block (manual or scripted)

Commands: YIELDOMEGA_PG_TEST_URL=… cargo test --test integration_stage2; local stack bash scripts/start-local-anvil-stack.sh + indexer; optional Playwright arena podium specs if present.


Test plan — attack, hack, and abuse vectors

Vector Risk Mitigation / test
SQL injection via query params Low (read-only route today) Keep parameterized SQL; no raw address concatenation
Oversized / malicious addresses in stored rows Display confusion Store lowercase 0x + 40 hex; API validates format; ignore malformed rows in top-3
Reorg double-spend of leaderboard Stale winners shown rollback_after deletes live rows; integration reorg test
Indexer drift vs chain Wrong payout preview Block-tagged snapshots at ingest; golden test: compare API vs cast call at read_block_number
RPC manipulation (indexer operator) False leaders Document trust model; compare against user RPC on dispute; do not accept client-supplied scores
DoS via heavy GET /v1/arena/podiums DB CPU Index (category, epoch, score DESC); limit work to top-3 per cat; rate limit at reverse proxy (ops)
WarBow BP generation bump Stale BP from prior epoch Tie snapshots to warbowBpGeneration / epoch; test epoch roll clears live WarBow table
Self-referral / sybil buys Game design, not indexer No special casing; ensure buyCount not inflated by indexer bugs (one row per log)
Flash-buy ordering Last Buy slot gaming Order by buy_index / block+log order from chain, not wall clock
Empty pool / zero address winners UI crash Match onchain address(0) handling; frontend already pads zeros

Verification criteria (close checklist)

  • All acceptance criteria checked.
  • cargo clippy --all-targets -- -D warnings and cargo test green in indexer/.
  • integration_stage2 passes with Postgres.
  • Manual: Anvil deploy → 3+ wallets buy → curl $INDEXER/v1/arena/podiums matches cast call podium for each PODIUM_CONTRACT_CATEGORY_INDEX at same block.
  • QA comment on #254 linking this issue; close #254 when both pass.
  • No new TimeCurve-only routes or tables introduced.
## Summary Complete the **#254** gap: while the arena is live, `GET /v1/arena/podiums` must return **top-3 winner slots and scores derived from indexed Postgres** (per **category** and **current epoch**), not from head `TimeArena.podium(category)` RPC alone. Consumers (Simple `/arena`, Arena views, play skills) use this route when `VITE_INDEXER_URL` is set. **Related:** [#254](https://gitlab.com/PlasticDigits/yieldomega/-/issues/254) (schema + HTTP baseline), [#238](https://gitlab.com/PlasticDigits/yieldomega/-/issues/238) (epic), [#247](https://gitlab.com/PlasticDigits/yieldomega/-/issues/247) (independent podium epochs), [#252](https://gitlab.com/PlasticDigits/yieldomega/-/issues/252) (WarBow), frontend `PODIUM_CONTRACT_CATEGORY_INDEX` ([#256](https://gitlab.com/PlasticDigits/yieldomega/-/issues/256)). --- ## Current codebase ### HTTP (`GET /v1/arena/podiums`) - Handler: [`indexer/src/api_arena.rs`](indexer/src/api_arena.rs) (`arena_podiums`). - **Winners / values** come from the in-memory **chain-timer** snapshot (`podium_contract[0..3]`), which is populated by head RPC `podium(cat)` in [`indexer/src/chain_timer.rs`](indexer/src/chain_timer.rs). - **Indexed data used today:** only `MAX(epoch)` per category from [`idx_arena_podium_epoch`](indexer/migrations/20240601000000_arena_v2.up.sql) (rolled epochs). Response always sets `podium_prediction: true` without building leaders from the DB. - Row **category labels** follow **onchain index order** (`last_buy`, `time_booster`, `defended_streak`, `warbow`), not the **UX order** the frontend expects when mapping rows by array index. ### Frontend contract - [`frontend/src/pages/arena/usePodiumReads.ts`](frontend/src/pages/arena/usePodiumReads.ts) — with indexer enabled, maps `response.rows[0..3]` directly to UI slots. - UX order is defined in [`frontend/src/pages/arena/podiumCopy.tsx`](frontend/src/pages/arena/podiumCopy.tsx): **Last Buy · WarBow · Defended Streak · Time Booster** via `PODIUM_CONTRACT_CATEGORY_INDEX = [0, 3, 2, 1]` (onchain cat indices). - [`frontend/src/lib/indexerApi.ts`](frontend/src/lib/indexerApi.ts) documents `podium_prediction` for live sale windows but the indexer does not yet populate live leaders from events. ### Indexed inputs (partial) | Onchain category | Index | Leaderboard metric (live) | Indexed today | |------------------|-------|---------------------------|---------------| | Last Buy | 0 | Last 3 distinct buyers in current `lastBuyEpoch` (`buyCount`) | `idx_arena_buy` (no epoch column; no order index) | | Time Booster | 1 | `totalEffectiveTimerSecAdded[buyer]` | **not projected** | | Defended Streak | 2 | `bestDefendedStreak[buyer]` | **not projected** | | WarBow | 3 | `battlePoints[buyer]` in current `podiumEpoch[3]` | partial via [`idx_warbow_epoch_score`](indexer/src/warbow_score.rs) (post-log eth_call snapshots) | Settlement history: `idx_arena_podium_epoch` / view `idx_arena_podium_snapshot` — **rolled** epochs only, not live boards. ### Product / onchain authority - Scoring rules: [`contracts/src/arena/TimeArena.sol`](contracts/src/arena/TimeArena.sol) (`_updateTopThree`, `_applyBuyWarBowBp`, defended-streak hooks). Spec: [`docs/product/arena-v2.md`](docs/product/arena-v2.md), [`docs/product/time-arena.md`](docs/product/time-arena.md). - **Guardrail:** onchain state is authoritative; the indexer is a **derived read model** only ([`docs/architecture/overview.md`](docs/architecture/overview.md), [yieldomega guardrails](.cursor/skills/yieldomega-guardrails/SKILL.md)). Predictions must **match chain** at the same block tag, not invent alternate rules. ### Stale design note [`docs/indexer/design.md`](docs/indexer/design.md) (TimeCurve podiums section) still describes live DB predictions during `!sale_ended`; Arena v2 implementation was never completed — this issue tracks that work. --- ## Why this is needed 1. **#254 acceptance** explicitly requires `GET /v1/arena/podiums`: *live predictions per category per epoch*. #254 remains open on this slice. 2. **WarBow UX:** head `podium(3)` is often **empty** mid-epoch while BP exists onchain; live sale UI must show indexed leaders ([`docs/indexer/design.md`](docs/indexer/design.md#timecurve-podiums-http)). 3. **Agent / API consumers** ([`skills/play-active-time-arena`](skills/play-active-time-arena/SKILL.md)) need a single HTTP source aligned with event history, not an extra RPC fan-out per page load. 4. **Row ordering:** indexer response order must align with frontend UX slots or expose explicit `category` + stable sort so Simple/Arena do not swap WarBow and Time Booster. --- ## Constraints and guardrails - **AGPL** repo; keep logic modular ([`docs/licensing.md`](docs/licensing.md)). - **No new authoritative business rules** offchain — replicate onchain leaderboard inputs at ingest or read with **block-tagged eth_call** snapshots (same pattern as [`indexer/src/warbow_score.rs`](indexer/src/warbow_score.rs)). Prefer **event-sourced projections** where events carry enough data; use **bounded RPC** only where logs omit scores. - **One SQL transaction per block** for ingest ([#140](https://gitlab.com/PlasticDigits/yieldomega/-/issues/140)); reorg rollback must include new tables ([`indexer/src/reorg.rs`](indexer/src/reorg.rs)). - **Epoch scope:** - Last Buy leaders scoped to head **`lastBuyEpoch`** (from chain timer / `LastBuyEpochStarted`). - Categories 1–3 scoped to head **`podiumEpoch[cat]`** (RPC or `idx_arena_podium_epoch` + 1 for live window). - **Arena v2 only** — no TimeCurve tables or `sale_ended` gating (arena is live until `paused`). - **After `rollPodiumEpoch`:** live board for that category clears onchain; API may return empty slots or fall back to settled `idx_arena_podium_epoch` row for the **just-finished** epoch — document chosen behavior in API + invariants. - **Performance:** `GET /v1/arena/podiums` should stay **O(1) or indexed top-3 queries**, not full-table scans; target p95 acceptable on Anvil-rich dataset (see #255 wallet-stats note). - **Schema version:** bump `SCHEMA_VERSION` in [`indexer/src/api.rs`](indexer/src/api.rs) and extend [`frontend/src/lib/indexerApi.ts`](frontend/src/lib/indexerApi.ts) types. --- ## Relevant files | Area | Paths | |------|--------| | API | [`indexer/src/api_arena.rs`](indexer/src/api_arena.rs), [`indexer/src/chain_timer.rs`](indexer/src/chain_timer.rs) | | Ingest / decode | [`indexer/src/ingestion.rs`](indexer/src/ingestion.rs), [`indexer/src/decoder.rs`](indexer/src/decoder.rs), [`indexer/src/persist.rs`](indexer/src/persist.rs), [`indexer/src/warbow_score.rs`](indexer/src/warbow_score.rs) | | Migrations | [`indexer/migrations/`](indexer/migrations/) | | Tests | [`indexer/tests/integration_stage2.rs`](indexer/tests/integration_stage2.rs) | | Frontend | [`frontend/src/lib/indexerApi.ts`](frontend/src/lib/indexerApi.ts), [`frontend/src/pages/arena/usePodiumReads.ts`](frontend/src/pages/arena/usePodiumReads.ts), [`frontend/src/pages/arena/podiumCopy.tsx`](frontend/src/pages/arena/podiumCopy.tsx) | | Docs / invariants | [`docs/indexer/design.md`](docs/indexer/design.md), [`docs/testing/invariants-and-business-logic.md`](docs/testing/invariants-and-business-logic.md), [`docs/frontend/arena-views.md`](docs/frontend/arena-views.md), [`skills/play-active-time-arena/SKILL.md`](skills/play-active-time-arena/SKILL.md) | | Onchain reference | [`contracts/src/arena/TimeArena.sol`](contracts/src/arena/TimeArena.sol), [`contracts/test/TimeArena.t.sol`](contracts/test/TimeArena.t.sol) | --- ## Recommended direction 1. **New projection table(s)** (name TBD, e.g. `idx_arena_podium_live` or per-cat tables) storing, per **(category, epoch, player)**: score, optional tie-break fields, `block_number`, `updated_at` — updated on ingest when `Buy`, WarBow PvP logs, or `LastBuyEpochStarted` affect leaders. 2. **Ingest enrichment module** (e.g. `indexer/src/arena_podium_live.rs`): - On each qualifying log at block `B`, eth_call at `B` the same views TimeArena uses: `buyCount`, `totalEffectiveTimerSecAdded`, `bestDefendedStreak`, `battlePoints` (WarBow may reuse/extend `warbow_score.rs`). - Alternatively, port `_updateTopThree` math in Rust **only if** every input is recoverable from indexed events without drift — prefer **snapshot-at-block** when in doubt. 3. **`arena_podiums` read path:** - Read head `last_buy_epoch` + `podium_epoch[cat]` from chain timer (extend snapshot if missing). - For each UX-ordered row (`PODIUM_CONTRACT_CATEGORY_INDEX`), query top-3 from Postgres for **(cat, current_epoch)**. - Set `podium_prediction: true` when rows are DB-derived; set `false` when intentionally mirroring RPC (e.g. post-roll settled-only mode, if implemented). - Return rows in **UX order** (or add `category_index` + sort; update `usePodiumReads` if needed). 4. **Optional:** `GET /v1/arena/podiums?category=warbow` later — out of scope unless trivial; single route is enough for v1. 5. **Close #254** only after this issue’s verification criteria pass. --- ## Acceptance criteria - [ ] New migration(s) for live podium projection(s); included in `ARENA_INDEX_TABLES` / `rollback_after`. - [ ] Ingest persists live leaderboard state for all **four** categories on relevant events (at minimum every `Buy` + WarBow BP mutations). - [ ] `GET /v1/arena/podiums` returns **top-3 `winners` + `values`** from Postgres for the **current epoch** per category while the arena is live. - [ ] Response includes correct **`epoch`** (string decimal) per row matching onchain `podiumEpoch` / `lastBuyEpoch` semantics. - [ ] Rows are ordered for **frontend UX** (Last Buy, WarBow, Defended Streak, Time Booster) or frontend remaps by `category` without slot swap. - [ ] `podium_prediction: true` only when values are DB-derived; document any RPC fallback. - [ ] WarBow mid-epoch: non-zero leaders when `idx_warbow_epoch_score` / live table has data, even if head `podium(3)` is empty. - [ ] `SCHEMA_VERSION` bump + `indexerApi.ts` types updated. - [ ] `INV-INDEXER-PODIUM-PREDICT-LIVE` (or successor) added/updated in [`docs/testing/invariants-and-business-logic.md`](docs/testing/invariants-and-business-logic.md); cross-link [`docs/indexer/design.md`](docs/indexer/design.md#arena-v2-schema-http-gitlab-254). - [ ] [`integration_stage2.rs`](indexer/tests/integration_stage2.rs) covers HTTP body shape + persist counts for at least two categories (e.g. Last Buy order + WarBow BP). --- ## Test plan — functional paths | # | Path | Expected | |---|------|----------| | 1 | Empty DB, chain timer ready | `GET /v1/arena/podiums` → 200, four rows, zero/empty winners, `epoch` from head RPC | | 2 | Single buyer, one DOUB buy | Last Buy row shows buyer in 1st; `buyCount` score matches onchain at buy block | | 3 | Two buyers, same `lastBuyEpoch` | Last Buy top-3 order = last-three distinct buyers (3rd → 2nd → 1st buy order) | | 4 | Buys adding timer seconds | Time Booster top entrant matches `totalEffectiveTimerSecAdded` leader at head block | | 5 | Streak buys under 15m | Defended Streak leader matches `bestDefendedStreak` at head block | | 6 | WarBow buys + steal | WarBow top-3 matches `battlePoints` for current `podiumEpoch[3]` | | 7 | `LastBuyEpochStarted` / hard reset | Last Buy board resets for new epoch; prior-epoch buys not in live top-3 | | 8 | `PodiumEpochRolled` for cat 1 | Live Time Booster board clears or switches to new epoch per documented API rule | | 9 | Reorg rollback | Live rows for reorged blocks removed; API consistent with surviving chain | | 10 | `chain_timer` unavailable | 503 `chain_timer_unavailable` (unchanged) | | 11 | Frontend with `VITE_INDEXER_URL` | Simple podium cards show same addresses as `cast call podium(cat)` at same block (manual or scripted) | **Commands:** `YIELDOMEGA_PG_TEST_URL=… cargo test --test integration_stage2`; local stack `bash scripts/start-local-anvil-stack.sh` + indexer; optional Playwright arena podium specs if present. --- ## Test plan — attack, hack, and abuse vectors | Vector | Risk | Mitigation / test | |--------|------|-------------------| | **SQL injection** via query params | Low (read-only route today) | Keep parameterized SQL; no raw address concatenation | | **Oversized / malicious addresses in stored rows** | Display confusion | Store lowercase `0x` + 40 hex; API validates format; ignore malformed rows in top-3 | | **Reorg double-spend of leaderboard** | Stale winners shown | `rollback_after` deletes live rows; integration reorg test | | **Indexer drift vs chain** | Wrong payout preview | Block-tagged snapshots at ingest; golden test: compare API vs `cast call` at `read_block_number` | | **RPC manipulation (indexer operator)** | False leaders | Document trust model; compare against user RPC on dispute; do not accept client-supplied scores | | **DoS via heavy `GET /v1/arena/podiums`** | DB CPU | Index `(category, epoch, score DESC)`; limit work to top-3 per cat; rate limit at reverse proxy (ops) | | **WarBow BP generation bump** | Stale BP from prior epoch | Tie snapshots to `warbowBpGeneration` / epoch; test epoch roll clears live WarBow table | | **Self-referral / sybil buys** | Game design, not indexer | No special casing; ensure buyCount not inflated by indexer bugs (one row per log) | | **Flash-buy ordering** | Last Buy slot gaming | Order by `buy_index` / block+log order from chain, not wall clock | | **Empty pool / zero address winners** | UI crash | Match onchain `address(0)` handling; frontend already pads zeros | --- ## Verification criteria (close checklist) - [ ] All **acceptance criteria** checked. - [ ] `cargo clippy --all-targets -- -D warnings` and `cargo test` green in `indexer/`. - [ ] `integration_stage2` passes with Postgres. - [ ] Manual: Anvil deploy → 3+ wallets buy → `curl $INDEXER/v1/arena/podiums` matches `cast call` podium for each `PODIUM_CONTRACT_CATEGORY_INDEX` at same block. - [ ] QA comment on [#254](https://gitlab.com/PlasticDigits/yieldomega/-/issues/254) linking this issue; close #254 when both pass. - [ ] No new TimeCurve-only routes or tables introduced.
PlasticDigits commented 2026-06-01 03:39:21 +00:00 (Migrated from gitlab.com)

mentioned in commit 5a5c8dc72f

mentioned in commit 5a5c8dc72f6b2b01b66689db2421070783cbe6d9
PlasticDigits commented 2026-06-01 03:39:28 +00:00 (Migrated from gitlab.com)

Summary

Implemented live podium predictions for GET /v1/arena/podiums (#273) — merged to main at 5a5c8dc.

What changed

  • Migration idx_arena_podium_live — top-3 slots per (category, epoch); included in ARENA_INDEX_TABLES / reorg rollback.
  • Ingest arena_podium_live.rs — block-tagged podium() snapshots on Buy, LastBuyEpochStarted, PodiumEpochRolled, and WarBow BP events; WarBow leaders rebuilt from idx_warbow_epoch_score.
  • API — UX order (Last Buy · WarBow · Defended Streak · Time Booster), head epoch from chain_timer (lastBuyEpoch / podiumEpoch[cat]), podium_prediction: true only when DB has entrants; RPC fallback otherwise.
  • chain_timer — polls lastBuyEpoch and podiumEpoch[0..3]; GET /v1/arena/timers exposes podium_epochs.
  • Schema 2.5.0 — types in indexerApi.ts; docs/invariants/skills cross-linked (INV-INDEXER-PODIUM-PREDICT-LIVE).

Verification checklist

  • Run migration on indexer DB: sqlx migrate run (new 20260601140000_idx_arena_podium_live_gl273).
  • cd indexer && cargo clippy --all-targets -- -D warnings && cargo test
  • With Postgres: YIELDOMEGA_PG_TEST_URL=… cargo test --test integration_stage2 (includes arena_podiums_live_predictions_smoke).
  • Local stack: deploy Anvil → several wallets buy → curl $INDEXER/v1/arena/podiums — four UX rows, WarBow non-empty when BP exists, podium_prediction: true.
  • Compare at same block: API winners vs cast call TimeArena.podium(cat) for cats 0,1,2; WarBow vs max battlePoints in epoch.
  • Frontend with VITE_INDEXER_URL: Simple/Arena podium cards match indexer rows.
  • After LastBuyEpochStarted / PodiumEpochRolled: live board resets for new epoch (no prior-epoch buyers in Last Buy top-3).
  • Reorg test: rollback removes idx_arena_podium_live rows for reorged blocks.

Follow-ups

  • Close #254 once QA confirms this slice plus remaining #254 acceptance items.
  • Optional later: GET /v1/arena/podiums?category=warbow filter (out of scope for v1).

QA agent team: please run the checklist above on a fresh Anvil + indexer deploy and report pass/fail on this issue (leave open until signed off).

## Summary Implemented live podium predictions for `GET /v1/arena/podiums` ([#273](https://gitlab.com/PlasticDigits/yieldomega/-/issues/273)) — merged to `main` at `5a5c8dc`. ### What changed - **Migration** `idx_arena_podium_live` — top-3 slots per `(category, epoch)`; included in `ARENA_INDEX_TABLES` / reorg rollback. - **Ingest** `arena_podium_live.rs` — block-tagged `podium()` snapshots on `Buy`, `LastBuyEpochStarted`, `PodiumEpochRolled`, and WarBow BP events; WarBow leaders rebuilt from `idx_warbow_epoch_score`. - **API** — UX order (Last Buy · WarBow · Defended Streak · Time Booster), head `epoch` from `chain_timer` (`lastBuyEpoch` / `podiumEpoch[cat]`), `podium_prediction: true` only when DB has entrants; RPC fallback otherwise. - **chain_timer** — polls `lastBuyEpoch` and `podiumEpoch[0..3]`; `GET /v1/arena/timers` exposes `podium_epochs`. - **Schema** `2.5.0` — types in `indexerApi.ts`; docs/invariants/skills cross-linked (`INV-INDEXER-PODIUM-PREDICT-LIVE`). ### Verification checklist - [ ] Run migration on indexer DB: `sqlx migrate run` (new `20260601140000_idx_arena_podium_live_gl273`). - [ ] `cd indexer && cargo clippy --all-targets -- -D warnings && cargo test` - [ ] With Postgres: `YIELDOMEGA_PG_TEST_URL=… cargo test --test integration_stage2` (includes `arena_podiums_live_predictions_smoke`). - [ ] Local stack: deploy Anvil → several wallets buy → `curl $INDEXER/v1/arena/podiums` — four UX rows, WarBow non-empty when BP exists, `podium_prediction: true`. - [ ] Compare at same block: API winners vs `cast call TimeArena.podium(cat)` for cats 0,1,2; WarBow vs max `battlePoints` in epoch. - [ ] Frontend with `VITE_INDEXER_URL`: Simple/Arena podium cards match indexer rows. - [ ] After `LastBuyEpochStarted` / `PodiumEpochRolled`: live board resets for new epoch (no prior-epoch buyers in Last Buy top-3). - [ ] Reorg test: rollback removes `idx_arena_podium_live` rows for reorged blocks. ### Follow-ups - Close [#254](https://gitlab.com/PlasticDigits/yieldomega/-/issues/254) once QA confirms this slice plus remaining #254 acceptance items. - Optional later: `GET /v1/arena/podiums?category=warbow` filter (out of scope for v1). --- **QA agent team:** please run the checklist above on a fresh Anvil + indexer deploy and report pass/fail on this issue (leave open until signed off).
PlasticDigits commented 2026-06-01 12:19:52 +00:00 (Migrated from gitlab.com)

mentioned in commit d902d7fff9

mentioned in commit d902d7fff92425cc88b2dd3c6946eb7c65e57303
PlasticDigits commented 2026-06-01 12:19:59 +00:00 (Migrated from gitlab.com)

QA verification complete (#273)

Verified live podium predictions on issue/273-verify worktree, fixed one blocker, merged to main (d902d7f).

What changed in this verification pass

  1. Bug fix — chain_timer / WarBow epoch reads: podiumDeadline and podiumEpoch are Solidity public array getters (uint256 index). The indexer used uint8 selectors that revert on TimeArena, so GET /v1/arena/podiums (and /v1/arena/timers) returned 503 chain_timer_unavailable. Fixed in indexer/src/chain_timer.rs and indexer/src/warbow_score.rs (INV-INDEXER-273-CHAIN-TIMER-SELECTORS).
  2. Anvil smoke: added bash scripts/verify-podium-live-anvil.sh (default ports 8546/3101 to avoid stack clashes).
  3. Docs: invariants §273, manual QA §273, guardrails + play skill + indexer/README.md cross-links.

Automated checks run (all green)

  • cargo clippy --all-targets -- -D warnings (indexer)
  • YIELDOMEGA_PG_TEST_URL=… cargo test --test integration_stage2 (includes arena_podiums_live_predictions_smoke + rollback)
  • bash scripts/verify-podium-live-anvil.sh — UX order, podium_prediction semantics, Last Buy / Time Booster vs block-tagged podium(), WarBow vs battlePoints, idx_arena_podium_live rows populated

Manual re-verify checklist

  • Postgres available (yieldomega-pg on 5433 or set DATABASE_URL / YIELDOMEGA_PG_TEST_URL)
  • bash scripts/verify-podium-live-anvil.sh exits 0
  • Optional full stack: bash scripts/start-local-anvil-stack.sh → 3+ wallet buys → curl $INDEXER/v1/arena/podiums matches onchain at read_block_number
  • Frontend with VITE_INDEXER_URL: /arena podium cards match indexer (four categories, UX order)
  • WarBow mid-epoch: leaders visible when BP exists even if head podium(3) empty
  • After PodiumEpochRolled / LastBuyEpochStarted: live board resets per documented API behavior
  • Reorg: live rows cleared via rollback_after (covered in integration_stage2)

Related: #254 slice for live podiums is satisfied by this work; QA may close #254 after spot-checking the same checklist there.

## QA verification complete (#273) Verified live podium predictions on `issue/273-verify` worktree, fixed one blocker, merged to `main` (`d902d7f`). ### What changed in this verification pass 1. **Bug fix — `chain_timer` / WarBow epoch reads:** `podiumDeadline` and `podiumEpoch` are Solidity **public array** getters (`uint256` index). The indexer used `uint8` selectors that revert on `TimeArena`, so `GET /v1/arena/podiums` (and `/v1/arena/timers`) returned **`503 chain_timer_unavailable`**. Fixed in `indexer/src/chain_timer.rs` and `indexer/src/warbow_score.rs` (`INV-INDEXER-273-CHAIN-TIMER-SELECTORS`). 2. **Anvil smoke:** added `bash scripts/verify-podium-live-anvil.sh` (default ports **8546/3101** to avoid stack clashes). 3. **Docs:** invariants §273, manual QA §273, guardrails + play skill + `indexer/README.md` cross-links. ### Automated checks run (all green) - [x] `cargo clippy --all-targets -- -D warnings` (indexer) - [x] `YIELDOMEGA_PG_TEST_URL=… cargo test --test integration_stage2` (includes `arena_podiums_live_predictions_smoke` + rollback) - [x] `bash scripts/verify-podium-live-anvil.sh` — UX order, `podium_prediction` semantics, Last Buy / Time Booster vs block-tagged `podium()`, WarBow vs `battlePoints`, `idx_arena_podium_live` rows populated ### Manual re-verify checklist - [ ] Postgres available (`yieldomega-pg` on **5433** or set `DATABASE_URL` / `YIELDOMEGA_PG_TEST_URL`) - [ ] `bash scripts/verify-podium-live-anvil.sh` exits 0 - [ ] Optional full stack: `bash scripts/start-local-anvil-stack.sh` → 3+ wallet buys → `curl $INDEXER/v1/arena/podiums` matches onchain at `read_block_number` - [ ] Frontend with `VITE_INDEXER_URL`: `/arena` podium cards match indexer (four categories, UX order) - [ ] WarBow mid-epoch: leaders visible when BP exists even if head `podium(3)` empty - [ ] After `PodiumEpochRolled` / `LastBuyEpochStarted`: live board resets per documented API behavior - [ ] Reorg: live rows cleared via `rollback_after` (covered in `integration_stage2`) Related: #254 slice for live podiums is satisfied by this work; QA may close #254 after spot-checking the same checklist there.
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-06-01 12:20:01 +00:00
PlasticDigits commented 2026-06-01 12:20:06 +00:00 (Migrated from gitlab.com)

mentioned in issue #254

mentioned in issue #254
ghost1 commented 2026-06-02 13:30:36 +00:00 (Migrated from gitlab.com)

mentioned in issue #278

mentioned in issue #278
PlasticDigits commented 2026-06-07 07:50:10 +00:00 (Migrated from gitlab.com)

mentioned in issue #300

mentioned in issue #300
PlasticDigits commented 2026-06-07 13:57:54 +00:00 (Migrated from gitlab.com)

mentioned in issue #302

mentioned in issue #302
PlasticDigits commented 2026-06-10 09:21:05 +00:00 (Migrated from gitlab.com)

mentioned in issue #306

mentioned in issue #306
PlasticDigits commented 2026-06-13 06:34:51 +00:00 (Migrated from gitlab.com)

mentioned in issue #307

mentioned in issue #307
PlasticDigits commented 2026-06-28 05:40:33 +00:00 (Migrated from gitlab.com)

mentioned in issue #356

mentioned in issue #356
PlasticDigits commented 2026-06-28 05:40:35 +00:00 (Migrated from gitlab.com)

marked as related to #356

marked as related to #356
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
code/yieldomega#273
No description provided.