Skip to content

feat(indexer): sync LRN mint/burn events into scholar_balances - #1096

Merged
Anuoluwapo25 merged 1 commit into
bakeronchain:mainfrom
Arowolokehinde:feat/issue-1018-scholar-balance-indexer
Aug 30, 2026
Merged

feat(indexer): sync LRN mint/burn events into scholar_balances #1096
Anuoluwapo25 merged 1 commit into
bakeronchain:mainfrom
Arowolokehinde:feat/issue-1018-scholar-balance-indexer

Conversation

@Arowolokehinde

Copy link
Copy Markdown
Contributor

Summary

GET /api/scholars/leaderboard reads scholar_balances, but nothing wrote to it. LRN balances are now projected from learn_token's on-chain events.

Balance updates are a non-idempotent read-modify-write, so deltas go through an append-only journal (lrn_balance_events) keyed by the Soroban event id and applied in the same statement that records them. Replays -- poller restarts, overlapping backfills, re-scanned ledger ranges -- collapse to a no-op instead of double-counting, and scholar_balances can be rebuilt exactly from SUM(delta) without re-reading the chain (Soroban RPC only retains a short window of events).

  • migration 035: journal table; widens lrn_balance to NUMERIC(40, 0), since 005's NUMERIC(30, 0) cannot hold every i128 the contract can emit and an overflow would stall the indexer on the offending ledger
  • db/scholar-balance-store: batch apply in one round trip, touching the balance table once per distinct address rather than once per event
  • lib/lrn-events: pure decoder for the topics the contract actually emits, lrn_mint (amount as the payload) and lrn_burned (map of from/amount). The issue text described GOVMinted/GOVBurned with value.fields, which does not match contracts/learn_token/src/lib.rs
  • services/scholar-balance-indexer: paginated sync holding O(page) memory, own checkpoint in indexer_state, clamps to the oldest retained ledger when a range has aged out of RPC
  • scripts/backfill-scholar-balances.ts: documented one-time replay, with --rebuild to recompute balances from the journal alone

Also fixes two pre-existing bugs that made the poller a no-op: getLatestLedger returns Api.GetLatestLedgerResponse rather than a number, so Number(...) gave NaN and the batch loop never ran an iteration; the event-indexer test mocked the buggy shape and is corrected to the real one.

Closes #1018

Type of Change

  • Bug fix
  • New feature
  • Smart contract change
  • Documentation
  • Breaking change
  • Other (please describe)

…onchain#1018)

GET /api/scholars/leaderboard reads scholar_balances, but nothing wrote to
it. LRN balances are now projected from learn_token's on-chain events.

Balance updates are a non-idempotent read-modify-write, so deltas go through
an append-only journal (lrn_balance_events) keyed by the Soroban event id and
applied in the same statement that records them. Replays -- poller restarts,
overlapping backfills, re-scanned ledger ranges -- collapse to a no-op instead
of double-counting, and scholar_balances can be rebuilt exactly from
SUM(delta) without re-reading the chain (Soroban RPC only retains a short
window of events).

- migration 035: journal table; widens lrn_balance to NUMERIC(40, 0), since
  005's NUMERIC(30, 0) cannot hold every i128 the contract can emit and an
  overflow would stall the indexer on the offending ledger
- db/scholar-balance-store: batch apply in one round trip, touching the
  balance table once per distinct address rather than once per event
- lib/lrn-events: pure decoder for the topics the contract actually emits,
  `lrn_mint` (amount as the payload) and `lrn_burned` (map of from/amount).
  The issue text described GOVMinted/GOVBurned with value.fields, which does
  not match contracts/learn_token/src/lib.rs
- services/scholar-balance-indexer: paginated sync holding O(page) memory,
  own checkpoint in indexer_state, clamps to the oldest retained ledger when
  a range has aged out of RPC
- scripts/backfill-scholar-balances.ts: documented one-time replay, with
  --rebuild to recompute balances from the journal alone

Also fixes two pre-existing bugs that made the poller a no-op: getLatestLedger
returns Api.GetLatestLedgerResponse rather than a number, so Number(...) gave
NaN and the batch loop never ran an iteration; the event-indexer test mocked
the buggy shape and is corrected to the real one.
@Anuoluwapo25
Anuoluwapo25 merged commit 7c550c2 into bakeronchain:main Aug 30, 2026
3 checks passed
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.

feat: Scholar Balance Indexer — sync LRN mint/burn events to scholar_balances table

2 participants