This module is ESM, so `require` is not defined at runtime. `node --check`
only parses, so it passed while both helpers would have thrown: the write
was swallowed by its catch, and the read fell through to `Infinity`, which
the watchdog reads as a permanently stale heartbeat — it would have exited
the process a minute after every scan and left the bot restarting in a loop.
`fs` is already imported at the top of the file; use it.
The bot pointed at the block-producing nodes' RPC ports. Those bind to
localhost, so once they stopped being exposed to the internet the bot lost
every chain at once and has submitted nothing since. `staking_score` gates
the entire trust score, so a noter that cannot reach the chains does not
degrade the score — it zeroes it for every tracked account.
Point it at the public endpoints instead, which is what an external client
should use and what the code already defaulted to. They are TLS-terminated
rather than plaintext ws:// across the internet, and this drops a hardcoded
node address from the repository.
Reaching the chains again is not enough on its own: the outage lasted three
weeks because reconnecting forever looks identical to working. The bot now
writes a heartbeat when a scan *completes*, a container healthcheck reads it,
and a watchdog exits once it goes stale so the restart policy turns a silent
stall into a visibly crash-looping container.
hasDataChanged() only compared fresh data against CachedStakingDetails -
which stays empty until a submission matures and gets finalized. For any
account whose real stake was genuinely stable, every 5-minute scan still
saw "changed from empty cache", resubmitted, and reset submitted_at to the
current block. Confirmed live: Serok's and QaziM's real, correctly-computed
stake (490,000 / 100,000 HEZ) never once reached CachedStakingDetails/
TrustScores despite multiple successful submissions - each new scan arrived
before the previous one's 300-block dispute window elapsed and overwrote it
with a fresh timer, a self-perpetuating loop.
getPendingData() now reads PendingStakingDetails first; hasDataChanged()
compares against that when present, only falling back to the cache once
nothing is pending. Applied to both the AssetHub and legacy RelayChain-clear
comparisons. Also reordered main()'s cycle to finalizeMaturedPending()
before fullScan(), so anything that matured this cycle gets promoted before
a new scan pass runs at all.
The staking-score pallet's audit-fix (commit 9afd99ae) added a dispute-window
model: receive_staking_details() only writes to PendingStakingDetails, and a
separate, permissionless finalize_staking_details() call promotes it to
CachedStakingDetails once DisputeWindow blocks have passed. No caller of that
promotion step existed anywhere in this bot (or anywhere else in this repo) -
confirmed live: Serok's and QaziM's real stake was correctly submitted to
Pending after the earlier NotRegisteredNoter/nonce fixes, but sat there
indefinitely with no path to ever reaching CachedStakingDetails/TrustScores.
Adds finalizeMaturedPending(), called once per periodic scan cycle (and once
at startup): scans PendingStakingDetails.entries(), finalizes whichever have
matured, batched into one utility.batchAll like the existing submission path,
routed through the same submitQueue so it can never race with a concurrent
submitStakingDetails call.
fullScan() processes accounts in concurrent batches of 10, and each account's
own data-gathering takes variable time, so multiple accounts' submitStakingDetails
calls landed close enough together to race on nonce assignment - the pool then
rejected most of them as duplicate-priority ("Priority is too low ... already in
the pool"). Observed live: 37 of 50 tracked accounts silently dropped in a single
scan cycle, including real accounts with genuine stake (Serok, QaziM) whose trust
scores stayed stuck at 0 as a result even after the separate NotRegisteredNoter
bond issue was fixed.
Routes every submission (both fullScan's batches and the live event listener)
through a single queue so exactly one signAndSend is ever in flight at a time,
in call order - data-gathering stays concurrent (unaffected), only the actual
chain submission is serialized.
processAccount() was comparing direct staking against cache BEFORE
checking pool data. This caused a flip-flop: when cache=121K and
direct=100K, the bot submitted 100K (missing pool). Next scan it
detected the difference and submitted 121K. Every 5 minutes, forever.
Worse, when direct staking went to 0, the bot submitted 0 which
caused the pallet to delete StakingStartBlock — permanently removing
the user from tracking and zeroing their trust score.
Fix: collect ALL staking data (direct + pool) first, combine into
a single total, then compare against cache. Also adds queryFailed
flag to prevent downgrading stake when pool RPC query fails.