Skip to content

chore: 🤖 address the reviews of the holdings, identity, schema and post-resync branches - #361

Open
prashantasdeveloper wants to merge 20 commits into
redesign/11-holdings-reviewfrom
redesign/12-review-fixes
Open

prashantasdeveloper wants to merge 20 commits into
redesign/11-holdings-reviewfrom
redesign/12-review-fixes

Conversation

@prashantasdeveloper

@prashantasdeveloper prashantasdeveloper commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Addresses the review comments on the holdings (#352), identity/key (#353), schema-invariants (#354),
post-resync-defects (#355) and coverage (#356) branches. Based on #360 so the whole stack stays
linear.

Everything was verified against this branch's tip before being fixed — two threads were already
resolved upstream and are listed at the bottom.

Thread → change

Holdings (#352)

Thread Outcome
Should the allowance/holding id include the DID? Already fixed on #360 — a reused row is restamped with its holder's current identity, rather than widening the id.
identityId only set at creation, so it goes stale Same commit on #360.

Identity, keys and multisig (#353)

Thread Outcome
createAccount clobbers an existing row's provenance Account writes update in place; only a new address takes the current event as createdEvent. Moved to the account utils, as suggested.
Account.remove deletes the row the new history points at The row is kept and unlinked (keyRole = Unlinked), in SecondaryKeysRemoved, SignerLeft and the rotation path.
Unguarded Account.get crashes the block Guarded in all three places, each recording an anomaly instead.
handlePrimaryKeyUpdated is correct only because of its siblings The three-event rotation sequence is now a docstring on the handler.
rotateIdentityKey bails after the close has committed Records an anomaly instead of returning silently, and reopens one interval per interval it closed rather than only the first.
Signer keyRole written from a pending offer Taken from the chain's key record instead, which is what acceptance writes; re-read when a signer is removed. This gives resolveKeyRole its first caller.
The linkSignerAccount docstring's two claims don't hold Rewritten to describe what the code does.
MultiSigAdmin.admin points at an unchecked DID Resolved from the index, then from chain state, then recorded as an anomaly — no row rather than a broken relation. Covers the genesis scan, MultiSigAddedAdmin and the pre-7.x creator path.
resolveKeyRole unused, and its comment overstates the invariant It has a caller now, and the comment states the actual rule: an event that grants a role writes it; an event that only implies one reads the key record.
KeyRole / KeyRoleEnum names are confusing Renamed to IdentityKeyRole / AccountKeyRole with the shared values spelled the same way, as proposed.
The three derived caches have no stated standing Each says in its docstring that it summarises state held authoritatively elsewhere.
IdentityKey id format undocumented; no datetime Id format documented; datetime deliberately left off.
transactionGroups is no longer filterable inside a jsonField Confirmed nothing filtered on it; the loss of filterability is now stated on the json field and in the consumer notes.
AuthorizationRetryLimitReached declared but never emitted Kept, with a docstring saying no row is expected to hold that status and why.

Schema invariants (#354)

Thread Outcome
Restore EvmTransaction.block Restored, declared exactly as Event and Extrinsic declare theirs.
The datetime reasoning rules out one column but reads as ruling out all Answered the question that settles it: the schema sync does not revert a manual conversion (see below), so the objection is cost, and it is now recorded as cost. The epoch integer is recorded as declined rather than left open.
eventId as unknown as InstructionEventEnum Replaced with an explicit map; an unmapped event records an anomaly instead of writing an undeclared value.
InstructionEvent.event should be indexed Indexed.
Eight eventId columns with no stated rule Rule stated and applied: kept where consumers filter or group by event type (PolyxEntry, StakingEvent, AssetTransaction, AssetAgentAction), dropped from Account and DistributionPayment.
Drop updatedEvent from the append-only family Dropped from six entities. Two of the nine named turned out to be mutable — see below.
reconcileBlock early-returns on every invocation Already fixed on #359 and proven live on a full genesis resync (compared=1090 drifted=4).
modulo: 1 disables dictionary block-skipping Fixed properly: the block handler is gone. Both flushes it existed for now hang off event handlers, so the dictionary can skip empty heights again — see below.
mapStatistics passes a block id as an event id Fixed, all three call sites.
mapPolyxLedger / accountBalance likewise Fixed; the seeded rows now carry the seed event marker.
mapIdentities passes a block id into rotateIdentityKey Fixed.

Post-resync defects (#355)

The metadata-key patch is applied as sent, on top of this branch's LockedUntil fix — it applied
cleanly and keeps that field. The root cause, the three wrong-key cases, the None-detail lock
wipe, the silent unresolved-key returns and the toHex() test-mock gap are all in.

Coverage (#356)

Thread Outcome
AssetAgent unreachable from either side Asset.agents and Identity.agentOf added as derived fields — no column, no index.
Distribution/CorporateAction/CorporateBallot share a CAId but cannot traverse Distribution.corporateAction added, with CorporateAction.distribution / .ballot deriving back.
Ballot votes are append-only for a value the chain replaces One row per voter per ballot, upserted. Superseded votes stay readable as the VoteCast events.
Permill columns stored bare All three say parts per million in their docstrings.
totalDebited uses 0 for an unindexable past Nullable, null across the pre-v8 range.
pendingCheckpoints names something that stops being true Renamed scheduledCheckpoints, not decremented, with CheckpointSchedule.checkpoints deriving the live view.
relayer.RelayedTx unhandled Recorded as a deliberate decision with the reason.
AssetAgent.group / .permissions always null Dropped. Permissions belong to the AgentGroup; the timeline is AssetAgentHistory.
GroupChanged replaces AgentPermissionsChanged Stated in the consumer notes.
Checkpoint.schedule never set Paired from the index by (scheduleId, moment) ordering, which is exact where the timestamp alone is ambiguous. Unpairable → anomaly, left null.
CorporateAction.checkpoint never set Set for the Existing variant, which carries the id. The Scheduled variant links itself from CheckpointCreated.
Transfer-manager path half-kept Removed entirely, along with an id collision in the pre-v5 exemption path.
Ballot meta stores nested hex Motion titles, info links and choices all decoded; stored shape unchanged.
CAPTURED_MODULES misses four pallets Now tracks src/decode/shapes/; the sync added the enum members it was behind on and the spec-8001020 arity fixture.
Relayer shape field names wrong Corrected against testnet metadata at spec 8001020, and RemovedSubsidy.remaining is now kept.
mapCheckpoint falls back to block.timestamp Removed — a schedule only advances when balances change, so a checkpoint's moment can predate its block.
targetTreatment falls back to Exclude Left null instead; toEnum's signature now says an omitted fallback is a choice.
Eight silent returns One shared missing-row anomaly helper, which the relayer's own version folds into.
EventIdEnum and the agent-action table behind the runtime Enum caught up by the sync; the table gains the nft pallet, statistics.SetAssetTransferCompliance and capitaldistribution.Reclaimed.
Deleted snapshots with live toMatchSnapshot() calls Replaced with field-by-field assertions.

Four places this diverges from the review

Claim and ConfidentialLegAffirmation keep updatedEvent. Both are read back and mutated —
a revocation sets revokeDate, an approval sets status — so they fail the append-only test. The
claim revocation was in fact not stamping updatedEvent, which is fixed here. Also,
TickerExternalAgentHistory no longer exists in the schema, so the family is six entities, not
nine.

The block handler is removed outright, which is a bigger fix than the thread asked for. The
first version of this branch only dropped the modulo: 1 filter and documented why the handler had
to stay. Then the resync run against that version measured ~120 blocks/s and a ~2 day ETA, on a
box that was idle (load average 3 of 32 cores, Postgres at 0.12% CPU, 247 blocks sitting in
Awaiting process). The cause was exactly the one this thread identified: blocks 4,000,000–4,050,000
carry 1,175 events across 50,000 blocks, so ~98% of the chain was being fetched and processed for
nothing. An unconditional block handler produces no dictionary query conditions, so the node stops
skipping; modulo: 1 had identical coverage and additionally paid a dictionary query per batch.
master has no such handler, which is why resyncs were fast before this stack.

So both flushes the handler existed for were moved onto event handlers. The NftHolder rollup is
written by the block's last holdings event, identified from the block's own event list — no
extra chain read, and the write stays inside the block that made the change, which is what
historical mode requires. The POLYX reconcile queue flushes from the ledger's read path, before a
later block applies any movement of its own: a block the index skipped moved no balance, so the
snapshot stays comparable however many heights later the flush lands, and reconcileOne already
re-checked that against the row. Sampling is now decided once per block on first ask, since the
gap test moves its marker when it passes.

The metadata resolver does not depend on ItemCompleted everywhere — checked against the chain.
asset.SetAssetMetadataValue first appears at block 4,397,819 (spec 5000002);
utility.ItemCompleted / ItemFailed only at block 10,036,148 (spec 6000001). For the ~5.6M blocks
in between, the per-call event-count vector on BatchCompleted / BatchInterrupted /
BatchOptimisticFailed is the only call boundary — which is exactly the path the patch already
carries, so the assumption holds by a different route than the one assumed. That is now recorded in
the resolver.

The schema sync does not revert a timestamptz conversion. SchemaMigrationService compares
the previous GraphQL schema against the next one and emits DDL only for fields that changed in the
schema — it never inspects the live column type — and the only other DDL path is sequelize.sync()
with no options, which is CREATE TABLE IF NOT EXISTS. So the conversion was never unworkable; it
is declined on cost, and the recurring part of that cost (every newly added Date field needs the
ALTER extended, with nothing in CI to catch a miss) is now stated.

Three more places this diverges, on #356

Six missing EventIdEnum members, not seven. balances.Restored is in the runtime and
already declared in the schema, which is why the drift report does not list it. The other six are
exactly as described.

IssuedNFT cannot join the agent-action table. Pre-6.0 it is
(IdentityId, NFTCollectionId, NFTId) — it names the collection, never the asset or ticker — so it
cannot be resolved by parameter position, and the table has no shape for a collection lookup. Its
siblings NftCollectionCreated and RedeemedNFT do name the asset and are in.
The omission is written down rather than left looking like an oversight.

BenefitClaimed is removed rather than confirmed. It is emitted by an agent's push_benefit
and by any holder's claim, so recording it attributed holders' own claims to the agents — the
over-inclusion asset.Transfer is excluded for. Reclaimed, which only an agent can call, takes
its place.

One defect the resync found

A worker died on block 24,730,189 with staking.Rewarded has no field "dest", restarting the
container. Chain metadata says Polymesh keeps its own (identity, stash, amount) shape at spec
8000000 as well as 7004001 — there is no dest in either — so the v8 branch was asking for a field
that never exists. Reading it as required is what made that fatal rather than merely wrong: the node
can attribute a block near an upgrade to the later runtime, which is how a pre-v8 reward reached
that branch. It is optional now, with the payee resolved from staking.payee when absent.

Commits

Thirteen, each typechecked on its own tree. All chore, because everything corrected here was
introduced inside this unmerged stack — typing them fix/feat would make the release notes read
"added X" then "fixed X" for what ships as one change.

Verification

yarn codegen, yarn typecheck, yarn lint, yarn test:unit (708 tests), yarn build and
yarn check-handlers all pass. A clean genesis resync from this branch is still required for
sign-off — the schema changed again with the #356 fixes, so it cannot reuse the completed run.

That run did finish, against the first nine commits: caught up at 26,027,249, one restart (the
staking.Rewarded defect above, fixed here), 646 BalanceReconciliationDrift rows and 3
MissingReferencedEntity. All 646 are on free — the pre-v8 block-author fee share that has no
event, which remains an open question — and none on frozen, where the previous run had 10. The
metadata resolver produced no anomalies at all across the whole replay, including the spec-6000001
boundary where the per-call batch markers arrive. The reconciler compared ~29,000 balances by block
10M against 1,090 for the entire previous run, because sampling now lands only on blocks that
actually moved a balance.

`KeyRole` and `KeyRoleEnum` were told apart only by a suffix that carries no information, and
they spelled the same concept two different ways (`Primary`/`Secondary` against
`PrimaryKey`/`SecondaryKey`), which is why a mapping existed between them at all.

They are now `IdentityKeyRole` (the role held within one membership interval) and
`AccountKeyRole` (the key's current role, a superset that can also say the key sits outside the
identity system). Both spell the shared values the same way. Each enum's docstring now states
which it is and how the two relate.

Nothing downstream depends on the old names: both enums are new in this unmerged stack.
Eight entities carried an `eventId` column with no stated rule, and the reason for keeping any of
them was recorded in one place only. The column is not simply redundant — filters and aggregates
are generated over an entity's own columns, so `createdEvent.eventId` can be selected but not
filtered or grouped on — but that argument only holds where event type is a query axis.

Kept, each with the same docstring: `PolyxEntry`, `StakingEvent`, `AssetTransaction`,
`AssetAgentAction`. Dropped from `Account`, which restated `updatedEvent`, and from
`DistributionPayment`, which restated `reclaimed`.

The rule is recorded in the entity-provenance plan so the next column is decided rather than
copied.
`AssetTransaction`, `InstructionEvent`, `Funding`, `DistributionPayment`, `BridgeEvent` and
`StakingEvent` are never read back anywhere in `src/`, so no handler could ever update one and
`updatedEvent` always equalled `createdEvent` — a redundant non-null relation, plus its index, on
some of the largest tables in the database.

Two entities that the design table also listed as append-only turned out not to be: a claim's
revocation sets `revokeDate`, and a confidential leg affirmation's approval sets `status`. Both
keep `updatedEvent`, and the claim revocation now stamps it, which it had not been doing.
A metadata event says that a value was set on an asset but never which key, so the key has to come
from the originating call. The previous approach counted metadata events and filtered metadata
calls, then zipped the two lists by position — which only holds if every filtered call emits
exactly one counted event, and that failed both ways: a register-and-set call emits a counted
event without being in the call filter (two values in one batch came out swapped), and a call that
fails under a non-atomic batch emits nothing (every later value landed under the failed call's
key).

Matching is structural now. The extrinsic's calls are normalised into a tree and walked alongside
the extrinsic's own events, using the per-call markers the batch and multisig pallets emit as the
call boundaries. A failed call is explicit rather than inferred, and anything unrecognised aborts
the extrinsic to an anomaly rather than guessing a key. Both batch eras are handled: the per-call
markers, and the older per-call event-count vector — which is load-bearing, since metadata events
predate the markers by several million blocks.

Two further defects go with it: a `None` detail means "unchanged" on chain and was clearing real
locks, and a key that could not be resolved returned silently. The test codec mock gained
`toHex()`, without which code that writes an id one way and reads it back another passed every
test and would have failed on real data.

Based on a patch from the review of the post-resync defect branch.
Three call sites passed a bare block id into a parameter that goes straight into `createdEvent` /
`updatedEvent`. Both are strings, so nothing caught it, and the result was rows whose event
relation resolves to nothing: every `StatType`, the balance row created on a ledger miss, and
every genesis-seeded balance.

The seeded rows now carry the seed event marker, which is what the holdings seeder already used
and what tells a seeded row from an event-caused one.
The identity and multisig handlers had a cluster of related defects around an account's lifetime.

Removing a secondary key deleted the `Account` row outright, orphaning the membership interval
the same handler had just closed, the account's balance and every ledger entry — all of which
point at it through non-null relations. It also broke primary-key rotation, where the chain
announces the incoming key as removed immediately before promoting it. The row is kept and
unlinked instead.

Re-announcing a key that is already indexed rewrote its row from scratch and moved its provenance
forward; account writes now update in place and only a genuinely new address takes the current
event as its `createdEvent`. Three unguarded reads that would have killed the block are guarded and
record an anomaly instead, as does a rotation that finds no membership to carry forward — which
previously returned silently after the close had already been committed. A rotation also reopens
one interval per interval it closed, rather than closing all and reopening the first.

A multisig signer's role now comes from the chain's key record rather than from the event:
creating a multisig and authorising a signer are offers the chain only records on the key once
the signer accepts, so taking the role from the offer relabelled keys that were an identity's
primary or secondary key, with nothing to put them back. The role is re-read when a signer is
removed.

A multisig's admin DID is resolved before a relation points at it — from the index, then from
chain state, and recorded as an anomaly if neither knows it — rather than writing a reference that
only fails at query time.

The primary-key rotation handler now documents the three-event sequence it depends on, which reads
as a leaked interval without it.
…s block back

Writing an `EventIdEnum` into the narrow instruction-event enum through a double cast discarded the
only thing that enum is for. The handled events are mapped explicitly now, and an event that
reaches the writer without a mapping is recorded rather than written as a value the schema never
declared. The column is also indexed, since filtering without joining the events table is why it
is kept locally at all.

`EvmTransaction` regains `block`, matching how events and extrinsics keep theirs — there is no
other direct link from one to the block it was included in.
…ma's derived state

The block handler has to run on every block: under historical tracking a row's validity starts at
the block it is saved in, so the holder buffer is only safe because the very next block flushes it.
`modulo: 1` expressed that but was strictly worse than no filter — it kept issuing a dictionary
query per batch whose every height was then unioned straight back in. The filter is gone and the
handler documents why it is unfiltered, and what the lever would be if the cost has to come down.

Three fields that summarise state held authoritatively elsewhere now say so in their docstrings,
an identity key's id format is documented, and the permissions json field records that its
contents are not filterable. The status an authorization can never hold says that it cannot.

The timestamp decisions are recorded rather than left open. The schema sync does not revert a
manual column conversion — checked against the migration service and the sync path — so the
case against converting is cost, which is now stated as such; and the epoch-integer question
is declined rather than left to win by default, with the narrower version kept on the table.
@prashantasdeveloper
prashantasdeveloper marked this pull request as ready for review September 24, 2026 14:27
@prashantasdeveloper
prashantasdeveloper requested a review from a team as a code owner September 24, 2026 14:27
… handler

The `NftHolder` rollup buffer and the POLYX reconcile queue were both flushed from a block handler
on the following block. That handler had no filter, and an unfiltered block handler produces no
dictionary query conditions — so the node stops skipping and scans the whole chain instead of the
~2% of heights that carry a subscribed event. Measured on the range being indexed at the time:
1,175 events across blocks 4,000,000–4,050,000. A genesis resync ran at ~120 blocks/s with a
two-day estimate on an otherwise idle box, with the work queued behind block fetching rather than
the database, which sat at 0.12% CPU. `modulo: 1` had identical coverage and additionally paid a
dictionary query per batch whose every height was unioned back in.

Both flushes now hang off the handlers that fill them, and the block handler is gone.

The holder rollup is written by the block's last holdings event, found in the block's own event
list — no extra chain read, and the write stays inside the block that made the change, which is
what historical mode requires of it: a row's validity begins where it is saved, so writing from a
later block would date the change to that block and leave every query in between reading the old
array.

The reconcile queue flushes from the ledger's read path, before the current block applies any
movement of its own. Any later block will do, not only the next height: a block the index skipped
moved no balance, so the derived side has not left the queued block, and the comparison already
re-checked that against the row before trusting it. Sampling is decided once per block on first
ask, because the gap test moves its own marker when it passes and would otherwise answer "yes" and
then "no" within one block.
…state

Found by the genesis resync, which it killed. A worker crashed on block 24,730,189 with
`staking.Rewarded has no field "dest"`, the container restarted, and the block was re-indexed
correctly on the retry — so it cost a restart rather than data, but only by luck.

Two things were wrong. Upstream Substrate's `Rewarded` carries `dest`; Polymesh keeps its own
`(identity, stash, amount)` shape, and testnet metadata declares no `dest` at spec 8000000 any more
than at 7004001 — so the v8 branch was asking for a field that never exists. And reading it as a
required field made that fatal instead of merely wrong: the node can attribute a block near an
upgrade boundary to the later runtime, which is how a pre-v8 reward reached the v8 branch at all.

The payee is now read as optional and resolved from `staking.payee` when absent, which is where it
has always come from for the pre-v8 era. Correct in both eras, and no longer sensitive to which
runtime the node thinks a boundary block belongs to.

`optionalField` moves to the decode layer, where the rest of the field-resolution helpers live,
rather than staying private to the POLYX ledger — it is the primitive for "absence is the answer",
and this is its second caller.
The transfer-manager concept is gone from the chain, and what was left here was half of a
translation onto the statistics model that replaced it: `TransferManagerAdded` wrote a `StatType`
for a percentage restriction and nothing for a count one, `TransferManagerRemoved` was unregistered
so even that row was never cleared, and both exemption handlers ran for either kind — leaving a
pre-v5 asset able to hold an exemption against a restriction that was never written.

The exemption path also collided: pre-v5 it keyed rows on the exempted entity alone, where the v5+
path keys them on asset, operation, claim type and entity. Two assets exempting the same identity
shared one row, and a pre-v5 row could never match its v5+ equivalent.

So the events are recorded as unhandled rather than half-translated, and the handlers,
`getTransferManagerValue` and `TransferRestrictionTypeEnum` go with them. Neither consumer reads
this era — the SDK reads transfer restrictions from chain state — so nothing downstream loses a
source. The one thing it does close off is a future analytics view over pre-v5 restrictions, which
would now need its own resync.
Corporate actions, checkpoints, ballots, agents and the relayer.

**Relations that could not be traversed.** A distribution and a ballot are each a corporate action
plus extra data, keyed on the same `CAId`, but only the ballot could reach its action. Both
directions are wired now. `AssetAgent` was indexed on both sides and reachable from neither, so
"this asset's agents" and "the assets this identity is an agent of" each needed a top-level query;
both are derived fields, which cost no column and no index.

**Two relations that were declared and never written.** A corporate action's checkpoint is set when
the chain names one that already exists — the `Existing` variant carries the id, so it needs
nothing
but decoding. A checkpoint's schedule is paired from the index: the event names the moment but not
the schedule, and two schedules can fall due at the same moment, so the timestamp alone is
ambiguous. The chain emits them in ascending schedule order, which is what makes the pairing
exact —
the lowest-numbered schedule that declared this moment and has not yet been claimed. A pairing that
does not line up is recorded and left null rather than guessed at.

**A field whose name stopped being true.** `pendingCheckpoints` was captured when a schedule was
created and never decremented as its checkpoints fired, so "pending" described something that
stopped being the case after the first one. It is `scheduledCheckpoints` now, with the live view as
a derived relation — still-pending is the declared set minus the checkpoints that exist. Not
decrementing is deliberate: it is a JSON array, and under historical tracking every save rewrites
the whole thing.

**A log of a value the chain replaces.** Ballot votes were keyed per event, but on chain `Votes` is
keyed `(CAId, IdentityId)` and voting again replaces the previous vote — so a voter who changed
their mind got two rows with nothing marking which counted, and any tally double-counted them. One
row per voter now, upserted, with the superseded votes still readable as the events themselves.

**Content stored as hex.** A ballot's motions — the titles, the info links, the choices, which is
the readable content of a ballot — were passed through as `0x…` while only the outer title was
decoded. All of it is decoded now; the stored shape is unchanged.

**Eight silent returns.** A handler that could not find the row its event referred to returned
without a trace, which hides both of the things that cause it: the creating event was missed,
or the
two handlers disagree about the id. They record a missing-referenced-entity anomaly now,
through one
shared helper that the relayer's own version is folded into.

**Agent actions that were not recorded.** The whole `nft` pallet was absent, so an agent minting a
fungible token was recorded and one minting an NFT was not; `statistics.SetAssetTransferCompliance`
and `capitaldistribution.Reclaimed` were the remaining agent-only events in their pallets.
`BenefitClaimed` goes the other way — it is emitted by an agent's `push_benefit` *and* by any
holder's `claim`, so recording it attributed holders' claims to agents, which is the over-inclusion
`asset.Transfer` is excluded for. `IssuedNFT` stays out with its reason written down: pre-6.0 it
names the collection, never the asset, so it cannot be resolved by parameter position.

**Relayer field names that did not match the runtime.** Checked against testnet metadata at spec
8001020: `RemovedSubsidy` carries `remaining`, `RemovedPendingSubsidy` carries
`initial_polyx_limit`, and only `SubsidyDebited` carries `amount`. The shape table said
`amount` for
all three. It is the record of what an event looks like, so the runtime's names win over a
convenient shared one — and `RemovedSubsidy.remaining` is now kept, being the allowance left when
the subsidy ended.

**Values that stood in for the unknown.** `Subsidy.totalDebited` was zero across the pre-v8 range,
where the chain emitted nothing to accumulate, making "nothing was drawn" and "not knowable" the
same stored answer; it is nullable. An unrecognised `targetTreatment` fell back to `Exclude`, which
is not a catch-all but the opposite of `Include` — so an unknown value silently inverted who a
corporate action applied to. `toEnum` takes no fallback there, and its signature now says that
omitting one is a real choice.

**Units and derivations, stated.** The three `Permill` columns say they are parts per million. The
checkpoint timestamp no longer falls back to the block's: a schedule only advances when balances
change, so a checkpoint's moment can predate its block. `AssetAgent` loses `group` and
`permissions`, which were always null and would have gone stale the first time an agent changed
group — permissions belong to the `AgentGroup`, and the timeline to `AssetAgentHistory`.

`relayer.RelayedTx` is recorded as deliberately unhandled: the standing relationship and every fee
drawn against it are already on `Subsidy`, and the relayed call is an `Extrinsic` like any other.
`CAPTURED_MODULES` still listed the six pallets that mattered when the arity contract test was
written, so `checkpoint`, `corporateAction`, `corporateBallot` and `relayer` had their shapes
checked by nobody: an arity change in a future runtime would pass CI and surface as a worker
crash mid-index. The list tracks `src/decode/shapes/` now.

Running the sync against testnet at spec 8001020 with the wider list adds the six events and six
calls the schema enums were behind on — the 8.1.x per-account asset freezing (`FrozenBalanceSet`,
`SetAccountFreeze`), the split-off `ControllerTransferTo`, and the NFT approvals — plus the arity
fixture for that spec version. Until they were in the enum they could only decode as `Unknown` and
raise an anomaly per occurrence; the catch-all event handler resolves them properly now, whether or
not a domain handler is registered for them later.

The integration queries stop asserting against snapshots that are not there. Four snapshot files
were deleted with the entity rename while their `toMatchSnapshot()` calls stayed, and a missing
snapshot makes Jest write one instead of comparing — so those cases passed whatever came back, in
the suite meant to evidence the rename preserving the data. They assert field by field instead.
Regenerating the snapshots would not have been enough: two of those files still query columns the
provenance rework removed, so the wider suite needs its own pass against the redesigned schema.
Five smells and one duplication, all in code this branch added.

The two cognitive-complexity findings are in the metadata key resolver. `toCallNode` was one
if/else
chain over every section and method it knows; it dispatches per section now, with the three
wrappers
that differ only in which argument holds the call reduced to a table. `walkBatch` carried both
batch
eras inline; the pre-v7 path — find the closer whose per-call counts account for the events
seen, then
walk each call inside its own bound — is its own function, and so is the marker handling after each
call of a v7+ batch. Same logic throughout, no behaviour change, and the tests that pin all three
wrong-key cases still pass.

`sameAsset` reads as an explicit guard rather than a chain of `!== undefined` checks. Two
absent ids
still count as not the same asset, which is the point: nothing was compared, so nothing matched.

The nested ternary in `rotateIdentityKey` becomes two named steps.

The duplication was three cases in the metadata tests building the same registration and set events
from literals. Two builders replace them, which also makes what each case is actually varying
visible.
Found by the resync, which raised 23 of these between blocks 5.87M and 6.58M — all of them noise.

A pre-v6 `CheckpointSchedule` carries a period, a start and a count; it never enumerates the
moments
it will fire at, which is what the pairing matches against. So every scheduled checkpoint in
that era
failed to pair and recorded an anomaly for it, at one row per checkpoint for the whole pre-v6
range.

Unpairable by construction is not the same as unexpected. A checkpoint whose asset has no schedule
declaring moments at all is now left unlinked in silence, and the anomaly is kept for the case
it was
meant for: a schedule that does declare moments, none of which is this one.
The `!counts || counts.reduce(...) !== offset` guard the batch-walk refactor left behind reads as
two unrelated checks. It is one: an event with no count vector sums to nothing, and nothing never
equals a real offset — so the optional chain does both jobs, and a comment says which is which.
The metadata snapshot now captures thirteen pallets and records which events
the runtime names, with a fixture for every era from 3001 to 8001020. The
contract test asserts that each emitted tuple event has a decoder and that no
registered decoder is stale, so runtime drift in any pallet the decode layer
registers fails CI rather than a resync.

Pre-v7 multiSig shapes and the NFT shapes of every era are registered, and
ClassicTickerClaimed carries its third field.
Each block is indexed under the runtime that ran it. The dictionary starts
each runtime late, so about 2% of blocks carried the previous spec's label;
the spec is now read from the executing runtime once per block, before any
handler runs.

Fees follow the chain end to end. Every runtime pays fees to the block
author, found from the BABE digest. Before v5.4.0 no fee event exists, so the
fee is priced with payment.queryInfo at the parent block. Up to v5.4.0 the
treasury took 80% of each fee, announced as TreasuryReimbursement, so the
author is credited only the rest. Which path applies is decided by what the
extrinsic emitted, not by the spec label. Busy early blocks no longer crawl:
a block's fee quotes are requested together, and an extrinsic's events are
found through a per-block index.

New coverage: NFT approvals and operator approvals, balance freezes, legacy
IssuedNFT and RedeemedNFT, ControllerTransferTo, set_controller, and the PIP
lifecycle events. Checkpoints pair by ordinal instead of a store read, and a
value that cannot be read is recorded as an anomaly instead of skipped. A
paying key's re-offer no longer hides its live subsidy, so the fees it covers
are not charged to the user.

An index can start after genesis. The first block seeds balances, holdings,
multisigs and EVM mappings from chain state, and an IndexOrigin row records
where the index began and which domains were seeded. An asset created before
the start block is read from chain when first referenced. Block timestamps
are read through one helper that fails loudly when a block has none.
CI now counts strictNullChecks errors against a checked-in baseline and fails
if the count rises, so the codebase moves toward strict mode one file at a
time.

Comments no longer cite review or decision ids; each explains its reason in
place.
…call declared

Before v5.4.0 a fee had no event of its own, and it was priced with
payment.queryInfo. That prices the weight a call declared, but a call that
used less is refunded after it runs: staking.rebond and contracts.instantiate
routinely are, and sudo calls are refunded in full. Payers were charged for
weight they never paid for, and block authors were credited with it.

Every runtime in that range gave the treasury floor(80%) of each fee and
announced it just before the extrinsic closed, so the fee is now read back
from that cut. The cut names a single fee three times in four; otherwise the
quote picks between the two it allows. No cut means nothing was charged.
Across sampled testnet blocks, every quote for a call that was not refunded
falls inside the range the cut allows.
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
14 New Code Smells (required ≤ 0)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

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