Skip to content

Feat/loki log shipping - #3824

Closed
hubur wants to merge 62 commits into
latticexyz:mainfrom
Asphodel-OS:feat/loki-log-shipping
Closed

Feat/loki log shipping#3824
hubur wants to merge 62 commits into
latticexyz:mainfrom
Asphodel-OS:feat/loki-log-shipping

Conversation

@hubur

@hubur hubur commented Jun 13, 2026

Copy link
Copy Markdown

No description provided.

hubur and others added 30 commits March 12, 2026 00:36
- copy game2's leaderboard aggregation closure (buildAggregate + packUtils/tourneyMath/onyxConstants/sort/types + AFFINITIES) into src/leaderboard/; import paths flattened to local, logic identical
- self-contained parity test (inline packU32 fixtures, no sibling game2 dependency): 14/14 green, tsc --noEmit clean
- only external runtime dep is viem (already a store-indexer dep)
- README marks game2 as source of truth until a shared package exists
- checkpoint 1 of the postgres-frontend leaderboard cache (cache + endpoints next)
- aggregateCache.ts: query decoded app__* tables (schema = lowercased STORE_ADDRESS) -> buildAggregate (leaderboard) + roster-by-owner (incl zero-match tarus); cached in memory; built on startup, recomputed per block debounced to <=60s with failure-retry; subscribes to storedBlockLogs stream
- apiRoutes: GET /api/leaderboard + /api/trainer/:wallet; 503 until first build; bigint-safe json, byWallet/byTaruchi maps kept server-side
- postgres-frontend: STORE_ADDRESS (required hex) + TARUCHI_CDN_BASE (env-driven, not hardcoded); build + start cache; pass to apiRoutes
- server spriteFor (cdn-base param) + decodeName injected; fee bps imported from onyxConstants deliberately
- battles-by-participant (mine archive) deferred to checkpoint 2b (needs BattleResult schema + summary shape)
- tsc clean, eslint clean, leaderboard parity 14/14
…battles

- matches-by-owner index built in the same cache pass from tourneys/duels + results (placements + time) + cores/status/names; no BattleResult needed (full replay loads on click via keyed /api/logs by matchId)
- summary fields: matchId, type (duel/festival), bracket, time, placements (taru indices in order), participants (id/name/sprite); newest-first per owner
- GET /api/trainer/:wallet/battles (503-until-ready, bigint-safe json); routed by the existing /api/trainer/* alb wildcard so no further iac
- add time to the tourney_result query
- tsc + eslint clean, leaderboard parity 14/14
- MatchParticipant gains ownerAddress (lowercased) + computed placement (via positionToPlacement, same fn as buildAggregate) so the client can tell mine-vs-opponent and show trainer/result without cross-joining the roster
- participants sorted by placement (1st first); dropped the misleading top-level placements array (festival result array is contract slot order, not 1st..last)
- addresses engineer review on the new (unconsumed) /api/trainer/:wallet/battles endpoint
- tsc + eslint clean
hubur and others added 25 commits June 1, 2026 19:23
fix(leaderboard): exclude duel onyx from server aggregate
…-indexer

feat(indexer): sign ascension NFT battle records
- new GET /api/taruchi/:id on postgres-frontend: full per-taruchi detail for the fighter card
- returns onchain status (level, xp, training points, affinity, state, bud index), unpacked traits (5 slots + cdn trait code) and combat stats (int16x4 from the packed uint128), lifetime record reused from the leaderboard byTaruchi aggregate, per-tier (rookie/veteran/champion) w/l split, and an ascension flag
- buildTaruchiDetails: pure builder (no sql/io), mirrors buildAggregate's injection style; per-tier w/l uses the same placement math so tiers sum to the overall record
- aggregateCache: widen the taruchi_status select (xp, training_points, bud_index, stats) and precompute a detailById map in the existing per-block rebuild; getTaruchi is an O(1) in-memory lookup, no per-request db hit
- never-played tarus return zeroed record + never-placed sentinel; ids stay bigint-safe strings
- focused vitest coverage for stat/trait unpackers and the builder (record reuse, tier split, ascension, never-played)

note: recent-battles per taruchi intentionally left to the existing per-wallet /battles endpoint to keep this payload bounded
…ons (PR 3/4)

producer half of web-push notifications. one projector (sibling to
tourney-announcement, same createSupabasePushAdapter registration) that writes
notification_events rows when a player's onchain thing RESOLVES — mint reveal,
duel complete, festival complete — captured straight from block.logs. a
supabase DB webhook on insert fans each out via the send-push edge fn.

the wrinkle the other projectors don't have: every onchain signal is a taruchi
INDEX or ID, never a wallet, and the resolving rows carry no owner. so we keep
owner caches learned from TaruchiCore writes (ownerById for mint, ownerByIndex
for duel/festival players). training is intentionally NOT emitted — a
TaruchiStatus->IDLE write is mint-vs-training-ambiguous from the new state
alone, so mint fires only on the UNREVEALED->IDLE transition (prior state in
lastStateById). duel fires once on ->COMPLETED (lastDuelStatus guard); festival
mirrors the tourney-announcement TourneyResult signal, per entrant. isCaughtUp
gates publishing (caches still update during backfill) so historical replay
doesn't spam. idempotent via the notification_events
(type,recipient,taruchi_id,block) dedup constraint (onConflict ignore).

all byte offsets verified against the contract codegen decodeStatic
(Duel status@9, TaruchiCore owner@0/index@20, TaruchiStatus state@1).

constraint: capture from block.logs only, never a DB read (matches existing projectors)
constraint: training delivery deferred (mint/training indistinguishable from new state alone)
confidence: high (offsets codegen-verified; 8 unit tests green)
scope-risk: minimal (additive projector; disabled unless PUBLISH_RESULTS_TO_SUPABASE + creds set)
reversibility: clean
tested: 17/17 store-indexer projector tests (8 new + 9 existing); tsc --noEmit clean; eslint clean
not-tested: live indexer run against chain + real Supabase (full e2e after deploy)
…oth never fired

OMC code review caught two CRITICAL silent-drop bugs (mint + duel would NEVER
fire in prod) because I keyed off Store_SetRecord events the contracts don't
emit for those transitions. Verified against the contracts and fixed:

- MINT: the prior code required prev state == UNREVEALED(0). But UNREVEALED is
  never written — it's the unwritten default. Reveal does TaruchiStatus.set(IDLE)
  as the FIRST Status write for the id (TaruchiRevealSystem). So mint now fires
  on the first IDLE Status write (prev unseen). Already-seen ids returning to
  IDLE (training/duel) don't fire; reroll's new id fires once. Matches the
  working sqs-reveal-hook precedent (fires on IDLE directly).
- DUEL: prior code watched Duel.status==COMPLETED via SetRecord, but
  Duel.setStatus emits Store_SpliceStaticData (setRecordsFor ignores it) — never
  seen. FIXED by triggering off TourneyResult.set instead (LibDuel writes it on
  resolve, same full-SetRecord signal as festival). Duel player indices are
  cached from the Duel.set ENROLL SetRecord. Removed the dead lastDuelStatus +
  status-splice assumptions entirely. Simpler: one TourneyResult resolve signal
  branches duel (known duel id) vs festival (festival-bracket tourney id).

tests rewritten to model REAL events: mint = a single IDLE Status SetRecord with
no prior write; duel = Duel.set enroll + TourneyResult.set resolve. (The old
fixtures synthesized SetRecords the contracts never emit — false confidence.)

constraint: only full Store_SetRecord events are observable (setRecordsFor) — pick triggers accordingly
confidence: high (signals verified against LibDuel.sol:274, TaruchiRevealSystem, Duel.sol setStatus splice)
tested: 8/8 projector tests (modeling real events) + tsc clean + eslint clean
not-tested: live indexer run (full e2e after deploy)
- P1 mint-silenced-on-reorg: ReorgError recovers in-process (no restart), so the
  in-memory lastStateById survived and a re-revealed mint after a reorg would
  never re-fire (permanent silence). Detect the reorg via block regression
  (highWaterBlock) and clear the mint-seen gate so the reveal re-fires —
  same-block replay is absorbed by the notification_events dedup; a genuinely
  re-mined reveal re-notifying once beats silence. Owner/enroll caches kept
  (learned before the boundary; needed to resolve replayed recipients). New test.
- P2 silent skip on unknown TourneyResult id: added warnMiss("result-unknown-id")
  for ids in neither duel nor tourney caches (enroll not seen) — mirrors the
  sibling projector's warn so missed notifications aren't invisible. Known
  non-festival brackets still skip silently (intentional).
- P2 over-reported count: ignoreDuplicates returns no row count, so the log now
  says `attempted` (what we sent), not `inserted` (can't know what dedup dropped).

confidence: high
tested: 9/9 projector tests (incl. reorg-replay re-fire) + tsc clean + eslint clean
…layed block

greptile P1 on mud#9: the reorg fix used Math.max for highWaterBlock, so after a
reorg regressed the block once, EVERY subsequent replayed block stayed below the
frozen peak and re-cleared lastStateById — wiping prior-state accumulated during
the replay, so a training-return-to-IDLE on an already-revealed taru looked like
a first write → false mint once caught up. Track the PREVIOUS block (lastBlock)
instead: regression triggers the clear exactly once at the boundary, then blocks
climb forward without re-clearing. Added a cross-replayed-block test.

tested: 10/10 projector tests (incl. across-replay prior-state) + tsc + eslint clean
feat(store-indexer): notification-event projector for push notifications (PR 3/4)
@hubur
hubur requested a review from alvrs as a code owner June 13, 2026 09:35
@changeset-bot

changeset-bot Bot commented Jun 13, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: c5dfde1

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercel Bot commented Jun 13, 2026

Copy link
Copy Markdown

@hubur is attempting to deploy a commit to the Lattice Team on Vercel.

A member of the Team first needs to authorize it.

@hubur hubur closed this Jun 13, 2026
@frolic

frolic commented Jun 13, 2026

Copy link
Copy Markdown
Member

What are ya working on?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants