Skip to content

fix(frontend): paginate factory event fetch instead of silently capping the token explorer#962

Open
Macnelson9 wants to merge 1 commit into
Favourorg:mainfrom
Macnelson9:fix/token-explorer-pagination-35
Open

fix(frontend): paginate factory event fetch instead of silently capping the token explorer#962
Macnelson9 wants to merge 1 commit into
Favourorg:mainfrom
Macnelson9:fix/token-explorer-pagination-35

Conversation

@Macnelson9

Copy link
Copy Markdown
Contributor

Closes #930

Summary

fetchAllTokens() in useTokens.ts (the no-creator path intended for the global token explorer) called stellarService.getContractEvents(contractId, 100) once and filtered for created events — a hardcoded, undocumented cap on the entire explorer's dataset, with no pagination and no indication to the user that results could be partial.

Investigation: what actually gets dropped?

Traced getContractEvents's implementation in stellar-impl.ts: it calls Soroban's getEvents RPC, which returns events in ascending ledger order (oldest first) starting from the earliest retained ledger when no cursor is supplied. A single capped call therefore drops the newest events once the factory's event history exceeds one page — the opposite of the usual "old data goes missing" assumption. Concretely: once the factory has emitted more than 100 created events, brand-new tokens silently stop appearing in "All Tokens" while old ones keep showing up, with no pagination control and no way to reach the missing tokens through this code path. This is documented at the call site now (fetchAllContractEvents.ts).

getContractEvents already supports cursor-based pagination (it's used this way by getTokenEvents), so full pagination to fetch the complete history was feasible — no need to fall back to an honest-but-partial UI indicator.

Fix

  • Added frontend/src/utils/fetchAllContractEvents.ts: pages through getContractEvents's cursor until a short page signals end-of-data, mirroring the exact end-of-data pattern fetchAllTokensByCreator already uses for the contract's paginated get_tokens_by_creator view call.
  • useTokens.ts's fetchAllTokens() now uses this helper instead of one fixed-size call.
  • Discovered while investigating: TokenExplorer.tsx — the component actually rendered as the Token Explorer page — has its own, entirely independent duplicate of this same bug class (getContractEvents(contractId, 1000), same silent-cap failure mode, just a higher ceiling, used in both its "browse all tokens" effect and its "search by index" handler). Since the acceptance criteria is about what the explorer view actually shows, not just this one hook, I switched both of TokenExplorer.tsx's call sites to the same fetchAllContractEvents helper — one implementation of "fetch the complete factory event history" instead of two that could independently drift, and the page users actually see now shows the complete set.

Scaling note (per the issue)

This re-walks the full factory event history on every load, which won't scale indefinitely as the platform grows. Per the issue's own framing, the durable long-term fix is an off-chain indexer (#35) rather than the frontend re-deriving state from raw contract events on each request — noted as a comment in fetchAllContractEvents.ts so this doesn't get relitigated from scratch later.

Tests

  • fetchAllContractEvents.test.ts (new): single short page (no pagination needed), multi-page pagination across 3 pages (230 events), a full-page-with-null-cursor edge case, and the empty-history case.
  • useTokens.test.tsx: added a regression test with 120 mocked created events split across two pages (100 + 20), asserting all 120 resolve to TokenInfo — not just the first 100 — and that the second getContractEvents call is made with the cursor returned by the first.

Test plan

  • npx vitest run — all 47 test files / 466 tests pass.
  • npx tsc --noEmit — clean.
  • npx eslint on changed files — clean.

…ng "all tokens"

fetchAllTokens() in useTokens.ts (the no-creator path used for the global
token explorer) called stellarService.getContractEvents(contractId, 100)
once and filtered for 'created' events, with no pagination and no
indication to the user that the result could be partial.

Traced getContractEvents's implementation (stellar-impl.ts): it calls
Soroban's getEvents RPC, which returns events in ascending ledger order
(oldest first) from the earliest retained ledger when no cursor is given.
A single capped call therefore drops the *newest* events once history
exceeds one page — the opposite of the usual "old data missing" assumption.
Concretely: once the factory has emitted more than 100 `created` events,
brand-new tokens silently stop appearing in "All Tokens" while old ones
keep showing up, with no pagination control and no way to reach them
through this code path.

getContractEvents already supports cursor-based pagination (used by
getTokenEvents), so full pagination is feasible today — no need to settle
for an honest-but-partial indicator. Added fetchAllContractEvents (frontend/
src/utils/), a small helper that pages through the cursor using the same
short-page end-of-data signal fetchAllTokensByCreator already uses for the
contract's paginated view call, and documents the ordering/truncation
behavior at the call site as required.

TokenExplorer.tsx — the component actually rendered as the Token Explorer —
turned out to have its own, independent duplicate of this same bug class
(getContractEvents(contractId, 1000), same silent-cap problem, just a
higher ceiling). Switched both of its call sites to the same
fetchAllContractEvents helper so there's one implementation of "fetch the
complete factory event history" instead of two that can independently
drift, and the explorer view itself (not just the otherwise-unused
useTokens() no-creator path) now shows the complete set.

This re-walks full event history per load, which won't scale indefinitely —
per the issue, the durable fix is an off-chain indexer (Favourorg#35); noted as a
comment in fetchAllContractEvents.

Added tests: fetchAllContractEvents.test.ts covers multi-page pagination,
a null-cursor-on-a-full-page edge case, and the empty-history case.
useTokens.test.tsx adds a regression test with 120 mocked 'created' events
across two pages, asserting all 120 resolve rather than only the first 100.
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.

fetchAllTokens silently truncates the "all tokens" view to the last 100 events

1 participant