Subgraph paging silently truncates at MAX_SKIP, overstating the net gain of the holders it drops
- Dominant language
- TypeScript
- Stars
- 1
- Forks
- 2
- Avg merge
- 6d 8h
- Merged PRs (30d)
- 10
Description
## Problem
`fetchOwnerStatsAtBlock` / the paging loop in `apps/web/src/lib/subgraph.ts:172` bounds itself with `for (let skip = 0; skip <= MAX_SKIP; skip += PAGE)` where `MAX_SKIP = 5000`. It exits either because a short page ended the data (correct) or because the cap bound (silent truncation) — and the two are indistinguishable from the outside. There are zero `logger` calls in the loop.
The admin's equivalent loops until a short page with no cap.
## Why the direction of the error is bad
Rows are ordered by `pixelCount` desc, so truncation drops the **smallest** holders. On the campaign board those rows are read at two pinned blocks and diffed. A wallet missing from the **start** read is treated as absent ⇒ 0, so its net gain is computed as `pixelCount(end) − 0` — **overstating** the gain of exactly the wallets that got dropped.
Harmless at today's holder counts. It becomes a wrong-ranking bug, silently, at the point the map gets popular enough to matter — and the failure is invisible because nothing records that the cap bound.
## Suggested fix
One line: when the loop exits on the cap rather than on a short page, `logger.warn('owner stats paging hit the cap', { mapId, block, rows: rows.length })`. That converts a silent wrong answer into a searchable signal.
Raising or removing `MAX_SKIP` is a separate call — the cap presumably exists to bound request cost — but whichever way that goes, it should be observable when it binds.
## Acceptance
A paging run that hits `MAX_SKIP` emits a warning naming the map and block. A run that ends on a short page does not.
Refs #224
Contributor guide
Assessment
This issue has not been assessed yet.