feat: index staking positions, nominations, validators and eras - #357
Draft
prashantasdeveloper wants to merge 4 commits into
Draft
prashantasdeveloper wants to merge 4 commits into
prashantasdeveloper wants to merge 4 commits into
Conversation
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
added this pull request to stack #358
September 15, 2026 08:23
|
prashantasdeveloper
marked this pull request as draft
September 15, 2026 10:29
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
Plan 07 — Staking. Turns the
stakingpallet's raw event log into queryable position state ontop 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/unbondingmirror the samestaking.ledgerchain readAccountBalance.bondedalready uses — a second view onto thatnumber, never an independently accumulated one that could drift.
Nomination— one row per identity→validator nomination, diffed against the target set onevery
nominatecall (targets no longer nominated close, matching ones stay open, new onesopen).
Validator— commission/blocked (ValidatorPrefsSet), permissioning(
PermissionedIdentityAdded/Removed), and active-set membership (StakersElected).Era— opened byStakersElected(which resolves the era index and elected validator setfrom chain storage, since the event itself carries neither) and closed by
EraPaid(payout/remainder/total staked).
StakingEventgainseraIndex/position, reusing the samePayoutStarted-fed cachePolyxEntry.eraIndexalready relies on.Also retires two defects in the existing
mapStakingEvent.tslog path along the way: B1(
extract8xStakingAmount, a value-shape guessing heuristic) and B3 (an unhandled v8 stakingevent silently dropped
amountwith no record — now anIndexerAnomaly).Staking Fixes
handleNominatednever persisted theStakingPositionit created — a dangling relation on anystash's first nomination.
session.validators()/era-set read atStakersElectedcollapsed to "nobody elected,"deactivating every validator on a transient RPC error instead of leaving the prior set alone.
pallet-staking's actualtry_trigger_new_erasource:StakersElectedfires beforeActiveEra/session.validators()update, so both were still reporting the outgoing era/set at that moment. Switched to
staking.currentEra()(bumped in the same block) andstaking.erasStakers(eraIndex)keys (thenewly-elected set, populated in that same block).
project.tsranhandleStakingEventbefore the handler that createsStakingPositionforBonded/Nominated— soStakingEvent.positionwas null on a stash's very first bond ornomination. Reordered.
get8xStakingEventDetailsread.stashunconditionally before itsswitch, which would throw(crash the handler) instead of recording the anomaly it advertises, for any future event without
a
stashfield.Chilled/Kickedhad 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-v8occurrence during a full resync.
Accountrelations (StakingPosition.stash/.controller,Validator.account,Nomination.validator) were written from raw addresses with no guarantee theAccountrowexisted — fixed via
ledgerAccount.extract8xStakingAmount's deletion;isChillednever cleared on re-nomination/re-validation; a ledger-read-fallback delta that could go
negative; a
Nominationid collision risk within a batched block.Consumer impact
Purely additive.
StakingEventis unchanged apart from two new nullable fields; the portal'sstakingEventsquery (eventId: {in: [Reward, Rewarded]}) is untouched. New capability: currentbonded/unbonding/nominations per account, validator status, and per-era reward aggregation via
groupedAggregates(groupBy: [ERA_INDEX])onStakingEvent— none expressible before.Test plan
yarn codegenyarn typecheckyarn lintyarn buildyarn test:unit(602 tests, 57 suites)