Skip to content

feat: index staking positions, nominations, validators and eras - #357

Draft
prashantasdeveloper wants to merge 4 commits into
redesign/08-coveragefrom
redesign/09-staking
Draft

prashantasdeveloper wants to merge 4 commits into
redesign/08-coveragefrom
redesign/09-staking

Conversation

@prashantasdeveloper

Copy link
Copy Markdown
Contributor

Summary

Plan 07 — Staking. Turns the staking pallet's raw event log into queryable position state on
top of what Phase 4 (POLYX ledger) already built:

  • StakingPosition — current bonded/unbonding/unlocking per stash, reward destination,
    validator/chilled status, and reward/slash totals. bonded/unbonding mirror the same
    staking.ledger chain read AccountBalance.bonded already uses — a second view onto that
    number, never an independently accumulated one that could drift.
  • Nomination — one row per identity→validator nomination, diffed against the target set on
    every nominate call (targets no longer nominated close, matching ones stay open, new ones
    open).
  • Validator — commission/blocked (ValidatorPrefsSet), permissioning
    (PermissionedIdentityAdded/Removed), and active-set membership (StakersElected).
  • Era — opened by StakersElected (which resolves the era index and elected validator set
    from chain storage, since the event itself carries neither) and closed by EraPaid
    (payout/remainder/total staked).
  • StakingEvent gains eraIndex/position, reusing the same PayoutStarted-fed cache
    PolyxEntry.eraIndex already relies on.

Also retires two defects in the existing mapStakingEvent.ts log path along the way: B1
(extract8xStakingAmount, a value-shape guessing heuristic) and B3 (an unhandled v8 staking
event silently dropped amount with no record — now an IndexerAnomaly).

Staking Fixes

  • handleNominated never persisted the StakingPosition it created — a dangling relation on any
    stash's first nomination.
  • A failed session.validators()/era-set read at StakersElected collapsed to "nobody elected,"
    deactivating every validator on a transient RPC error instead of leaving the prior set alone.
  • Era resolution was reading the wrong storage. Verified against pallet-staking's actual
    try_trigger_new_era source: StakersElected fires before ActiveEra/session.validators()
    update, so both were still reporting the outgoing era/set at that moment. Switched to
    staking.currentEra() (bumped in the same block) and staking.erasStakers(eraIndex) keys (the
    newly-elected set, populated in that same block).
  • project.ts ran handleStakingEvent before the handler that creates StakingPosition for
    Bonded/Nominated — so StakingEvent.position was null on a stash's very first bond or
    nomination. Reordered.
  • get8xStakingEventDetails read .stash unconditionally before its switch, which would throw
    (crash the handler) instead of recording the anomaly it advertises, for any future event without
    a stash field.
  • Chilled/Kicked had no pre-v8 decode shape — verified against Polymesh's actual v7.4.0 source
    (Chilled{stash}, Kicked{nominator,stash}); missing shapes meant a crash on the first pre-v8
    occurrence during a full resync.
  • Non-null Account relations (StakingPosition.stash/.controller, Validator.account,
    Nomination.validator) were written from raw addresses with no guarantee the Account row
    existed — fixed via ledgerAccount.
  • Minor: a stale JSDoc comment left behind by extract8xStakingAmount's deletion; isChilled
    never cleared on re-nomination/re-validation; a ledger-read-fallback delta that could go
    negative; a Nomination id collision risk within a batched block.

Consumer impact

Purely additive. StakingEvent is unchanged apart from two new nullable fields; the portal's
stakingEvents query (eventId: {in: [Reward, Rewarded]}) is untouched. New capability: current
bonded/unbonding/nominations per account, validator status, and per-era reward aggregation via
groupedAggregates(groupBy: [ERA_INDEX]) on StakingEvent — none expressible before.

Test plan

  • yarn codegen
  • yarn typecheck
  • yarn lint
  • yarn build
  • yarn test:unit (602 tests, 57 suites)
  • 10-index-per-entity cap checked on every new/changed entity

New `StakingPosition` entity tracking bonded/unbonding/unlocking per stash, sourced from
the same `staking.ledger` chain read `AccountBalance.bonded` already uses — a second view
onto that number, never an independently accumulated one.

Also migrates `mapStakingEvent.ts` off positional `params[i]` destructuring onto the
decode layer, fixing two defects along the way: B1 (`extract8xStakingAmount`, a value-shape
heuristic, retired along with its test) and B3 (an unhandled v8 staking event silently
dropped `amount` with no record — now recorded as an `IndexerAnomaly`).
New `Nomination` entity. `Nominated` (pre-v8 `staking`, v8+ `validators`) now opens a row
per target and closes rows for targets no longer nominated, diffing against the
currently-open set rather than replacing it wholesale. `Chilled`/`Kicked` close nominations
and mark `StakingPosition.isChilled`.
New `Validator` entity. `ValidatorPrefsSet` upserts commission/blocked (pre-v8 and v8+,
same shape both eras) and marks the corresponding `StakingPosition` as a validator.
`PermissionedIdentityAdded`/`Removed` name an identity rather than a stash, so
`isPermissioned` only updates `Validator` rows that already exist for that identity.
`SlashReported` — a validator-offence report, not yet a completed slash — is logged through
the existing `StakingEvent` path rather than a new entity field.
New `Era` entity. `StakersElected` carries no payload in either era, so the era index and
elected validator set are resolved from chain storage — `staking.currentEra()` and
`staking.erasStakers(eraIndex)` keys, not `activeEra()`/`session.validators()`, which are
still on the outgoing era/set at the moment this event fires. It marks exactly the resolved
validator set active, clearing validators no longer elected. `EraPaid` closes the era with
the payout figures and `staking.erasTotalStake`.

`StakingEvent` gains `eraIndex`/`position`, populated the same way `PolyxEntry.eraIndex`
already is — reusing `mapPolyxLedger.ts`'s `currentPayoutEra` cache rather than a second one
— and reward/slash events now roll up into `StakingPosition.totalRewarded`/`totalSlashed`.
@prashantasdeveloper
prashantasdeveloper requested a review from a team as a code owner September 15, 2026 08:23
@prashantasdeveloper
prashantasdeveloper added this pull request to stack #358 September 15, 2026 08:23
@sonarqubecloud

Copy link
Copy Markdown

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.

1 participant