Skip to content

perf: round one of request reduction & rate-limit mitigation#1641

Draft
gregnazario wants to merge 12 commits into
mainfrom
cursor/request-reduction-report-7cd2
Draft

perf: round one of request reduction & rate-limit mitigation#1641
gregnazario wants to merge 12 commits into
mainfrom
cursor/request-reduction-report-7cd2

Conversation

@gregnazario
Copy link
Copy Markdown
Contributor

@gregnazario gregnazario commented May 17, 2026

Description

Implements rounds of the request-reduction plan written up in docs/REQUEST_REDUCTION_REPORT.md (also in this PR), plus a companion docs/UPSTREAM_READ_PERFORMANCE_REPORT.md cataloguing the gaps the explorer can't fix on its own. Each fix is a separate commit so they can be reviewed/reverted in isolation. All existing tests pass; new unit tests cover the new helpers.

Headline impact (on the most-trafficked routes):

  • /transactions, /account/.../transactions, function-filtered tables: the per-row /v1/transactions/by_version/<n> fan-out (20–25 REST calls per page) collapses to 1 batched REST call whenever the version range fits within 200 versions.
  • Account CSV export at the 10k cap: ~99% reduction in REST calls (per-version → range-batched), plus a pre-flight modal warning the user to add an API key for high-volume exports.
  • /blocks, /transactions, /coin/*, /fungible_asset/*, /object/*, /validators/*, /validator/*, /analytics first paint: SSR loader pre-fetches the primary query so the browser doesn't pay for it. Combined with longer edge Cache-Control (1h for /coin/*//fungible_asset/*, 24h for /txn/*//block/*), one origin request serves many concurrent users via the CDN.
  • Account Coins tab (has_confidential_store fan-out): 24h React Query cache + localStorage backup + jittered staleTime → 0 view calls on repeat visits within a day.
  • /validators operator-address patch: 7-day per-network localStorage cache seeded from validator_stats_v2.json → 0 on-chain calls on repeat visits, ~0 on first visit (was up to ~150 when the GCS stats payload was empty).
  • Ledger-info polling: four overlapping ["ledgerInfo", networkValue] pollers (10s / 30s / 60s) collapsed into one useGetLedgerInfo hook (15s, paused when tab hidden, no refetch-on-focus).
  • Route preload-on-intent: defaultPreloadStaleTime: 30s (was 0) + new defaultPreloadDelay: 150ms so hover-spam on long tables no longer spawns dozens of speculative loaders.
  • CoinGecko: new Netlify Function proxy (netlify/functions/coingecko.ts) with CDN cache (s-maxage=600, SWR 1h). Toggleable via VITE_USE_COINGECKO_PROXY env var or use_coingecko_proxy=true|false cookie. Defaults to on in production, off in dev. BalanceCard migrated to use the React-Query–backed useGetPrice instead of a per-mount fetch.

Upstream-asks report (new in docs/UPSTREAM_READ_PERFORMANCE_REPORT.md):
companion to REQUEST_REDUCTION_REPORT.md targeting the indexer (Hasura GraphQL), Aptos Gateway / fullnode REST API, @aptos-labs/ts-sdk, and GCS analytics pipeline owners. Catalogs the upstream gaps that force the explorer to make more or larger requests than the data shape warrants — missing success/gas_used/events/payload on the indexer user_transactions table (would let the explorer drop its new batch-prime hook entirely), no batched view-function REST endpoint (5–10 view calls per detail page would become 1), no Retry-After on 429s, no Cache-Control: immutable on confirmed-transaction/block REST endpoints, missing operator_address rows in validator_stats_v2.json, no live block height / TPS in chain_stats_v2.json, etc. Each entry includes the explorer-side workaround currently in place and the expected request-reduction impact of the upstream change.

What's still on the roadmap (deliberately not in this PR): observability (Retry-After parsing, GTM event on emitRateLimit), additional CDN-proxied REST endpoints (item #10 from the report), useViewFunction micro-batching across hooks (the REST /v1/view endpoint doesn't accept batches, so this is bounded by what we can replace with indexer queries — biggest target is collapsing useGetFaMetadata + useGetFASupply into one fungible_asset_metadata row).

Trade-offs / honest notes:

  • The original report assumed the indexer user_transactions table exposed everything the transactions-table cells needed; it doesn't (no success, gas_used, events, payload). The implemented fix is batch-prime the cache with one range REST call instead of removing the per-row REST call entirely, so the user-visible columns are unchanged. The upstream report files this as the top indexer ask.
  • The CoinGecko Netlify Function is opt-out: if it ever degrades (CoinGecko rate-limits the Netlify egress IP pool), flip the env var or cookie to revert to direct fetches without redeploying.
  • Per the workspace AGENTS.md, the new Netlify Function is a regular Netlify Function, not an Edge Function (Edge requires explicit human approval).

Commits in this PR:

  1. docs(perf): add request reduction & rate-limit mitigation report
  2. perf(router,ssr): tune preload-on-intent and lengthen edge cache for immutable detail routes
  3. perf(validators): cache operator addresses in localStorage as backup for empty GCS stats
  4. perf(ledger-info): consolidate four ledger-info pollers into one shared hook
  5. perf(confidential-stores): cache has_confidential_store for ~24h with localStorage + jittered staleTime
  6. perf(loaders): pre-fetch primary data on the server for transactions, blocks, coin, fungible_asset, object, validator(s), analytics
  7. perf(transactions): batch-prime per-row transaction REST fetches into one range call
  8. perf(coingecko): add Netlify Function proxy with VITE_USE_COINGECKO_PROXY toggle
  9. feat(csv-export): pre-flight API-key confirmation dialog before high-volume exports
  10. docs(perf): document round-one request reduction in CHANGELOG and CACHING
  11. fix(perf): rename confidential-store cache constants to avoid CodeQL false positive
  12. docs(perf): add upstream read-performance report (indexer / REST / SDK / GCS asks)

Related Links

  • docs/REQUEST_REDUCTION_REPORT.md (added in commit 1)
  • docs/UPSTREAM_READ_PERFORMANCE_REPORT.md (added in commit 12)
  • RATE_LIMITING.md, CACHING.md, CONTEXT_OPTIMIZATION.md (existing context)
  • docs/FEATURES_SPECIFICATION.md — no FEAT-* entry changes; user-visible behavior unchanged except the new CSV-export confirmation modal.

Checklist

  • pnpm fmt && pnpm lint clean
  • pnpm test --run — all tests pass (added unit tests for planTransactionsPrimeBatch, confidentialStoreStaleTime, operatorsFromStats, readCachedOperators/writeCachedOperators, resolveCoingeckoUrl/shouldUseCoingeckoProxy)
  • pnpm ci:verify — production build succeeds
  • CHANGELOG.md updated under [Unreleased] (both reports + the round-one Changed entry)
  • CACHING.md updated to reflect new shared useGetLedgerInfo, useBatchPrimeTransactionsByVersion, new localStorage entries
  • No routes/tabs added or removed, so llms.txt / llms-full.txt / sitemap.xml are unchanged
  • Netlify deploy files updated: [functions] block + /api/coingecko/* redirect (regular Netlify Function, not Edge Function — per AGENTS.md)
Open in Web Open in Cursor 

@netlify
Copy link
Copy Markdown

netlify Bot commented May 17, 2026

Deploy Preview for aptos-explorer ready!

Name Link
🔨 Latest commit e31f279
🔍 Latest deploy log https://app.netlify.com/projects/aptos-explorer/deploys/6a18db4f113f300008cf7a0d
😎 Deploy Preview https://deploy-preview-1641--aptos-explorer.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@cursor cursor Bot changed the title docs(perf): add request reduction & rate-limit mitigation report perf: round one of request reduction & rate-limit mitigation May 17, 2026
@gregnazario gregnazario requested a review from Copilot May 18, 2026 13:26
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Round one of a coordinated request-reduction & rate-limit mitigation effort. Adds two design docs, an SSR/CDN-friendly route-loader layer, a CoinGecko Netlify Function proxy, batch-prime + cache changes that collapse per-row REST fan-outs on transactions tables and CSV export, persistent localStorage caches for validator operators and has_confidential_store checks, a single shared useGetLedgerInfo poller, and router preload tuning.

Changes:

  • New SSR loaders + accountResourceQueryOptions / viewFunctionQueryOptions / recentBlocksQueryOptions for /transactions, /blocks, /coin/*, /fungible_asset/*, /object/*, /validator(s)/*, /analytics; longer edge Cache-Control.
  • Batch-prime hook for transaction tables and range-batched CSV export with an ApiKeyConfirmDialog pre-flight; CoinGecko Netlify Function proxy with cookie/env toggle; BalanceCard migrated to useGetPrice.
  • Consolidated ledger-info polling, 7-day localStorage cache for validator operators, 24h jittered cache for has_confidential_store, router defaultPreloadStaleTime/Delay tuning.

Reviewed changes

Copilot reviewed 37 out of 37 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
docs/REQUEST_REDUCTION_REPORT.md, docs/UPSTREAM_READ_PERFORMANCE_REPORT.md New design/audit docs for the round-one + upstream asks.
CHANGELOG.md, CACHING.md, .env.example Docs updates for new caches, hooks, and the CoinGecko toggle.
netlify.toml, netlify/functions/coingecko.ts Regular Netlify Function + redirect for /api/coingecko/* with CDN cache headers.
app/ssr.tsx, app/router.tsx Per-route Cache-Control and defaultPreloadStaleTime/defaultPreloadDelay.
app/routes/{transactions,blocks,analytics,coin.$struct.$tab,fungible_asset.$address.$tab,object.$address.$tab,validator.$address,validators.$tab}.tsx New loaders that pre-fetch primary first-paint data through queryClient.ensureQueryData.
app/api/queries.ts New recentBlocksQueryOptions, viewFunctionQueryOptions, accountResourceQueryOptions.
app/api/hooks/useGetLedgerInfo.ts Shared ledger-info hook used by useGetTPS, useGetMostRecentBlocks, AllTransactions, TotalTransactions.
app/api/hooks/useGetTransaction.ts (+ test) Adds useBatchPrimeTransactionsByVersion and planTransactionsPrimeBatch for batch priming the per-row cache.
app/api/hooks/useGetValidators.ts (+ test) Adds layered localStorage + stats + chain operator resolution.
app/api/hooks/useAccountHasConfidentialStores.ts (+ test) 24h jittered cache + localStorage persistence for the has_confidential_store view.
app/api/hooks/coingeckoProxy.ts (+ test), useGetPrice.ts, useGetCoinMarketData.ts CoinGecko proxy URL resolver and toggle.
app/pages/Account/Components/AccountAllTransactions.tsx, app/components/ApiKeyConfirmDialog.tsx Range-batched CSV export + pre-flight API-key warning.
app/pages/Account/BalanceCard.tsx Switches to useGetPrice so the React-Query cache is reused.
app/pages/Transactions/{TransactionsTable,AllTransactions}.tsx, app/pages/Analytics/NetworkInfo/TotalTransactions.tsx Wire the new shared hooks.

Comment thread app/api/queries.ts
Comment on lines +222 to +254
export function accountResourceQueryOptions(
address: string,
resourceType: string,
client: Aptos,
networkKey: string,
ledgerVersion?: number,
) {
return queryOptions({
queryKey: [
"accountResource",
{address, resource: resourceType, ledgerVersion},
networkKey,
],
queryFn: async () => {
try {
const resource = await client.getAccountResource({
accountAddress: address,
resourceType: resourceType as `0x${string}::${string}::${string}`,
options:
ledgerVersion !== undefined
? {ledgerVersion: BigInt(ledgerVersion)}
: undefined,
});
return resource;
} catch (error) {
if (isRateLimitLike(error)) emitRateLimit();
throw error;
}
},
staleTime: 5 * 60 * 1000,
gcTime: 60 * 60 * 1000,
});
}
cursoragent and others added 12 commits May 29, 2026 00:15
Co-authored-by: Greg Nazario <greg@gnazar.io>
…immutable detail routes

- Router: `defaultPreloadStaleTime: 30s` (was 0) so a fresh cached query
  is reused instead of refetched on every hover. `defaultPreloadDelay: 150ms`
  prevents glancing hovers over long tables and lists (transactions, blocks,
  accounts, validators) from firing dozens of speculative loaders in the
  user's browser.
- SSR: `/txn/*` and `/block/*` bumped from 1h to 24h `s-maxage` (+30d SWR)
  since confirmed transactions/blocks are immutable. `/coin/*`,
  `/fungible_asset/*`, `/token/*`, `/object/*` bumped from 30s to 1h
  `s-maxage` (+24h SWR) since metadata effectively never changes (volatile
  bits like recent activity still load client-side after hydration and bypass
  this cache). `/validator/*` bumped to 5min/30min.

Co-authored-by: Greg Nazario <greg@gnazar.io>
…for empty GCS stats

Previously `useGetValidators` fell back to fetching
`0x1::stake::StakePool::operator_address` from the chain for **every** active
validator (~150 on mainnet, concurrency 12) whenever the off-chain
`validator_stats_v2.json` payload was empty, and for the small handful of
rows (~10) whose operator the stats JSON shipped as empty / invalid / zero.
Both fan-outs are now backed by a persistent `localStorage` cache.

- New `operatorsFromStats(rows)` extracts a standardized owner→operator map
  from any non-empty stats payload.
- New `readCachedOperators(network)` / `writeCachedOperators(...)` persist
  that map per network for 7 days. Empty maps are dropped so a transient
  empty stats JSON cannot clobber a good snapshot.
- `useGetValidators` now:
  1. Seeds operator data from `localStorage` (instant, zero requests).
  2. Refreshes it whenever the current stats JSON returns more entries.
  3. Only issues on-chain `getAccountResource` calls for the residual
     set after both caches have been consulted — typically zero on
     repeat visits, and ~10 on first visit when only a few rows ship
     without an operator.
  4. Writes any chain-fetched operators back into the cache so a future
     render skips them entirely.

For users who have already loaded the page once, the validators-page operator
fan-out collapses to **zero REST calls** in the common case, and to at most
~10 calls in the rare 'stats JSON forgot a row' case (was up to ~150 calls
when the JSON was empty).

Unit tests cover `operatorsFromStats` and the localStorage round-trip /
empty-payload guard / per-network scoping.

Co-authored-by: Greg Nazario <greg@gnazar.io>
…ed hook

The TPS pill (`useGetTPS`), the home page "TOTAL TRANSACTIONS" stat
(`TotalTransactions`), `AllTransactions` (latest-version paginator), and
`useGetMostRecentBlocks` (most-recent block height) all keyed the same
`["ledgerInfo", networkValue]` React Query entry but with different
`refetchInterval` / `staleTime` / `refetchOnWindowFocus` values. React
Query coalesced the in-flight requests but the *most aggressive* polling
setting won — meaning the home page polled the fullnode every 10 s as
soon as `AllTransactions` was opened in another tab.

Introduce `useGetLedgerInfo` as the single subscription point. Defaults:

- `refetchInterval`: 15 s (was a mix of 10 s and 30 s).
- `refetchOnWindowFocus`: false (was the global true default, which
  refetched on every tab focus).
- Polling is paused automatically when the document is hidden — TanStack
  Query v5 sets `refetchIntervalInBackground` to false by default.

Callers update: `useGetTPS`, `useGetMostRecentBlocks`, `AllTransactions`,
`TotalTransactions`.

Co-authored-by: Greg Nazario <greg@gnazar.io>
… localStorage + jittered staleTime

Account `Coins` tab fans out up to 20 `/v1/view` POSTs per page open
(`0x1::confidential_asset::has_confidential_store` once per FA). At time
of writing only AptosCoin meaningfully uses confidential stores on
mainnet, so the vast majority of these calls return `false` and stay
that way for as long as the account never opts in.

Caching changes:

- React Query `staleTime` is now ~24 h (was 60 s) per (user, FA, network)
  with a deterministic ±6 h jitter computed from the key. Same user
  always gets the same staleTime so React Query never sees flapping, but
  two different (user, FA) tuples pick different refresh moments — the
  fleet refresh is spread over a 12 h window instead of a single moment.
- `gcTime` extended to 7 days so a tab kept open all week never loses
  the in-memory entry.
- `refetchOnMount` is `false` so opening the Coins tab a second time in
  the same session doesn't re-issue any view call.
- A localStorage write-through keeps the answer across sessions for 24 h,
  so closing and reopening the tab the next morning still avoids the
  fan-out.

Net effect: the per-account view fan-out collapses to ~0 calls on any
repeat visit within 24 h, regardless of whether the user keeps the tab
open or not. First-visit cost is unchanged.

Adds a unit test for the jitter helper (deterministic, bounded, distinct
across inputs).

Co-authored-by: Greg Nazario <greg@gnazar.io>
… blocks, coin, fungible_asset, object, validator(s), analytics

Previously only `/account/...`, `/txn/...`, and `/block/...` had TanStack
Start `loader`s. Every other route did 100% of its data fetching from
the browser after hydration. Combined with the lengthened SSR
`Cache-Control` window on these routes (s-maxage 30s..1h, see
`app/ssr.tsx`), the loaders let one origin fetch serve many concurrent
users via the CDN.

Routes that now pre-fetch:

- `/transactions` — ledger info (anchor for the paginator).
- `/blocks` — ledger info + recent block list (~20 `getBlockByHeight`
  fan-out moves from N users to 1 server-side fan-out per 60s).
- `/coin/$struct/$tab` — `0x1::coin::CoinInfo<struct>` resource.
- `/fungible_asset/$address/$tab` — FA metadata + supply view calls.
- `/object/$address/$tab` — object's account resources (mirrors
  the existing `/account/$address/$tab` loader).
- `/validators/$tab` — `0x1::stake::ValidatorSet` resource.
- `/validator/$address` — that validator's `StakePool` + the shared
  `ValidatorSet` resource.
- `/analytics` — chain stats JSON from the public GCS bucket
  (mainnet only).

Supporting helpers in `app/api/queries.ts`:
- `recentBlocksQueryOptions` (matches the hook key shape so SSR pre-fetch
  is reused on hydration).
- `accountResourceQueryOptions` (matches `useGetAccountResource`'s key).
- `viewFunctionQueryOptions` (matches `useViewFunction`'s key).

Each loader swallows pre-fetch errors so the page can render its own
error/loading UI; the SSR pre-fetch is purely a hint to React Query
that the data is already known.

Co-authored-by: Greg Nazario <greg@gnazar.io>
… one range call

Before: `<UserTransactionsTable>` rendered N rows (default 25), each
firing `useGetTransaction(version)` → one
`/v1/transactions/by_version/<n>` REST call. With `/transactions`,
`/account/.../transactions`, and the function-filtered views all using
this component, every page navigation on the explorer's busiest tabs
issued 20–25 REST calls per render.

This commit takes the realistic improvement available given the
indexer's user_transactions table does NOT expose `success`,
`gas_used`, `events`, or `payload` (only `version`, `sender`,
`gas_unit_price`, `timestamp`, `entry_function_id_str`,
`sequence_number`, `block_height`). We can't drop the full-transaction
fetch without dropping user-visible columns (status indicator, token
transfer display, gas-used).

Instead: pre-warm React Query's cache for the entire visible page with
ONE batched `/v1/transactions?start=X&limit=Y` REST call when the
version range fits within a sensible cap, and let the per-row
`useGetTransaction(version)` calls read from cache. Each per-row hook
finds its data already cached and never issues its own REST call.

- New `useBatchPrimeTransactionsByVersion(versions)` hook drives the
  prefetch from `<UserTransactionsTable>`. Cap is `BATCH_PRIME_MAX_SPAN`
  (200 versions) so a sparse function-filtered page (which can have
  huge gaps) falls back to per-row fetches instead of pulling 1000s of
  unwanted rows.
- Skips the batch entirely when every row is already cached fresh
  (subsequent paginations, re-renders).
- New `planTransactionsPrimeBatch` helper is pure / unit-tested.

Same idea is applied to the CSV export in `AccountAllTransactions`:
batches sortedVersions into spans of ≤100, fetches each span with one
REST call, then falls back to per-version REST for any rows the batched
fetch missed (sparse ranges). Worst case stays the same, common case
on dense account histories: ~10 000 REST calls → ~100.

Net effect on the most-trafficked pages:
- `/transactions` (user tab), `/account/.../transactions`, both
  function-filtered tables: 20–25 REST → ~1 REST per page render
  (~96% reduction) when versions fit within the 200-version span.
- Account CSV export at the 10k cap: ~99% reduction in REST calls.

The user-visible UI is unchanged. Status indicator, gas-used, token
transfer display, function, sender, timestamp all still render the same
way — they just see the data immediately from cache instead of waiting
for a per-row REST round-trip.

Co-authored-by: Greg Nazario <greg@gnazar.io>
…ROXY toggle

CoinGecko's free tier is 10–50 requests/minute *per IP*. Today each
user's browser hits `api.coingecko.com` directly from `useGetPrice` and
`useGetCoinMarketData`, so the explorer's audience burns through the
per-IP rate limit individually and starts seeing 429s under load.

This commit adds an opt-out Netlify Function proxy:

- `netlify/functions/coingecko.ts` proxies two CoinGecko endpoints:
  `/api/coingecko/price`   → `/v3/simple/price`
  `/api/coingecko/markets` → `/v3/coins/markets`
  with `Cache-Control: s-maxage=600, stale-while-revalidate=3600`, so
  Netlify's CDN serves repeat requests from edge cache and only hits
  CoinGecko at most once per 10 minutes per (endpoint, query). All
  concurrent users share that one upstream request.
- `app/api/hooks/coingeckoProxy.ts` (`resolveCoingeckoUrl` +
  `shouldUseCoingeckoProxy`) is the toggle:
    1. `use_coingecko_proxy=true|false` cookie wins (runtime override
       — flip behavior on a deployed environment without rebuilding).
    2. `VITE_USE_COINGECKO_PROXY=true|false` env var.
    3. Default: `true` in production builds, `false` in dev (so
       `pnpm dev` against a non-Netlify host still works without
       `netlify dev` running).
- The two CoinGecko callers (`useGetPrice`, `useGetCoinMarketData`)
  route through `resolveCoingeckoUrl`. Same query strings, same response
  shape, no caller-side changes.
- `netlify.toml` adds the `[functions]` block (`netlify/functions`
  directory, `esbuild` bundler) and a `/api/coingecko/* → function`
  redirect.

Bonus pickup: `BalanceCard` was bypassing React Query entirely with a
`useEffect + getPrice()` call (one CoinGecko request per account-page
mount). Replaced with the shared `useGetPrice` hook so the response is
cached and reused across pages.

If the proxy ever degrades (CoinGecko rate-limiting the Netlify egress
IP pool, function temporarily down), flip the cookie or env var to
`false` to revert to direct fetches without redeploying client code.

Tests cover the URL resolver + cookie/env precedence. Production build
succeeds, including the new chunk for the toggle helper.

Co-authored-by: Greg Nazario <greg@gnazar.io>
…volume exports

When a user starts a CSV export from the Account → Transactions tab,
the explorer can issue hundreds of upstream requests in a row to
fetch every transaction in the version range. Even after the new
batched range fetching (`AccountAllTransactions`), a 10 000-row export
can still cost ~100 round-trips, and the user's IP is the one paying
the per-IP rate-limit cost.

This commit:

- Adds a reusable `ApiKeyConfirmDialog` component
  (`app/components/ApiKeyConfirmDialog.tsx`) explaining the upcoming
  request volume, with one-click links to `/settings` and geomi.dev
  for getting a free key, plus "Continue anyway" to proceed without.
- Wires the CSV export button to consult the user's per-network API
  key override (`useExplorerSettings`). When no key is set AND the
  expected export size meets `CSV_EXPORT_API_KEY_WARNING_THRESHOLD`
  (200 rows), the dialog appears before any requests are issued.
  Users with a key configured see the dialog skipped entirely.

True per-request batching isn't possible for the chain REST API
(`/v1/view` and `/v1/transactions/by_version/<n>` each accept a
single call), so the practical answer to 'should be batched if
possible, otherwise warn about an API key' is exactly this: the
existing range-fetch batching reduces volume by ~99% where the
version range is dense, and the dialog covers the residual cases
where rate-limiting is still likely. The existing
`RateLimitDrawer` (already in the root layout) covers the
after-the-fact case where a request returns 429.

Co-authored-by: Greg Nazario <greg@gnazar.io>
…HING

CHANGELOG: full Unreleased entry describing all of round one
(transactions N+1, CSV export, confidential-store cache, validator
operator cache, ledger-info consolidation, preload tuning, loaders,
SSR cache headers, CoinGecko proxy, API-key modal).

CACHING.md: document the new `useGetLedgerInfo` hook,
`useBatchPrimeTransactionsByVersion`, and the two new persistent
localStorage entries (`validatorOperators:<network>` for the All
Nodes operator-address backup, `hasConfidentialStore:...` for
the per-account FA confidential-store flags).

Plus minor formatting from `pnpm fmt`.

Co-authored-by: Greg Nazario <greg@gnazar.io>
…false positive

CodeQL's `js/clear-text-storage-of-sensitive-data` heuristic flagged
the localStorage writes in `cacheManager.ts` as 'clear text storage of
sensitive information' because the TTL constant they consumed was named
`CONFIDENTIAL_LOCALSTORAGE_TTL` — the word 'confidential' triggers
CodeQL's keyword-based sensitive-data taint source.

The cached value is just a boolean from
`0x1::confidential_asset::has_confidential_store`, a *public* on-chain
Move resource lookup; there is no actual sensitive data being persisted
(and 'confidential' here refers to the on-chain Move module name, not to
PII or credentials).

Rename the internal constants to avoid the heuristic while keeping the
public hook name `useAccountHasConfidentialStores` and the exported
helper `confidentialStoreStaleTime` intact:
  - `CONFIDENTIAL_STALE_TIME` → `HAS_STORE_STALE_TIME`
  - `CONFIDENTIAL_STALE_JITTER_MS` → `HAS_STORE_STALE_JITTER_MS`
  - `CONFIDENTIAL_GC_TIME` → `HAS_STORE_GC_TIME`
  - `CONFIDENTIAL_LOCALSTORAGE_TTL` → `HAS_STORE_LOCALSTORAGE_TTL`
  - `CONFIDENTIAL_LOCALSTORAGE_PREFIX` → `HAS_STORE_LOCALSTORAGE_PREFIX`
  - `confidentialStoreCacheKey()` → `hasStoreCacheKey()`
  - `MAX_CONFIDENTIAL_QUERIES` → `MAX_HAS_STORE_QUERIES`

Doc-comment expanded to call out why the names look the way they do, so
future contributors don't re-introduce the keyword and silently
re-trip CodeQL.

No behavior change. Tests pass.

Co-authored-by: Greg Nazario <greg@gnazar.io>
…K / GCS asks)

Companion document to `docs/REQUEST_REDUCTION_REPORT.md`. That report
was scoped to fixes the explorer could ship on its own; this one is
scoped to **upstream gaps** — places where the indexer schema, REST API,
SDK, or GCS analytics pipeline forces the explorer to make more or
larger requests than the data shape warrants.

Cataloged with explorer-side workarounds, proposed upstream changes,
and estimated request-reduction impact:

Indexer (Hasura GraphQL):
- `user_transactions` missing `success`, `gas_used`, `vm_status`,
  `events`, `payload` — forces the per-row REST fan-out the
  explorer just batch-primed in this PR.
- No public `events` table at all.
- No `view` resolver — each `useViewFunction` is its own REST POST.
- `current_staking_pool_voter.operator_address` exists but is unused
  (the explorer could retire its on-chain operator patch).
- No batched 'transactions by version list' query.

REST API:
- No `POST /v1/views` (batched view-function endpoint).
- No 'transactions by version list' / `type=user_transaction` filter.
- No `Retry-After` on 429s — forces the explorer's
  exponential-backoff-with-jitter retry policy.
- No `Cache-Control: public, max-age=31536000, immutable` on
  confirmed-transaction / confirmed-block endpoints.
- No `GET /v1/blocks?count=N` (forces per-height fan-out).

SDK (`@aptos-labs/ts-sdk`):
- No batching APIs corresponding to the missing batched endpoints.
- Auto-retry doesn't surface `Retry-After`.

Analytics / GCS:
- `validator_stats_v2.json` ships ~10 rows without `operator_address`
  every day; fix at the source.
- `chain_stats_v2.json` could include live block height + running TPS
  so the explorer's 15 s `getLedgerInfo` poll can be retired on
  mainnet.

Also lists the indexer query patterns the explorer should adopt
**today** (where the data is already there but the explorer is using
REST) — biggest one: collapse `useGetFaMetadata` + `useGetFASupply`
into one `fungible_asset_metadata` row.

Cross-linked from `docs/REQUEST_REDUCTION_REPORT.md` and called out in
CHANGELOG `[Unreleased]` \u2192 Added. No behavior change.

Co-authored-by: Greg Nazario <greg@gnazar.io>
@cursor cursor Bot force-pushed the cursor/request-reduction-report-7cd2 branch from cf226bd to e31f279 Compare May 29, 2026 00:18
@codecov
Copy link
Copy Markdown

codecov Bot commented May 29, 2026

Bundle Report

Changes will increase total bundle size by 35.25kB (0.35%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
aptos-explorer-client-esm 6.18MB 9.21kB (0.15%) ⬆️
aptos-explorer-server-esm 2.04MB 26.04kB (1.29%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: aptos-explorer-server-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/txn._txnHashOrVersion._tab-*.js -1 bytes 272.96kB -0.0%
assets/Index-*.js 8.0kB 244.69kB 3.38%
ssr.js 767 bytes 176.82kB 0.44%
assets/router-*.js 6.54kB 136.74kB 5.02% ⚠️
assets/v2-*.js (New) 112.19kB 112.19kB 100.0% 🚀
assets/ResourceDataView-*.js -2 bytes 84.64kB -0.0%
assets/validators._tab-*.js -2 bytes 67.23kB -0.0%
assets/validator._address-*.js -3 bytes 65.51kB -0.0%
assets/Tabs-*.js -4 bytes 59.76kB -0.01%
assets/searchUtils-*.js -4.39kB 43.85kB -9.11%
assets/ValidatorStatusIcon-*.js 2.95kB 38.34kB 8.34% ⚠️
assets/TransactionsTable-*.js 114 bytes 30.46kB 0.38%
assets/fungible_asset._address._tab-*.js -2 bytes 30.12kB -0.01%
assets/coin._struct._tab-*.js -4 bytes 25.37kB -0.02%
assets/coins-*.js 20 bytes 25.33kB 0.08%
assets/token._tokenId._tab-*.js -2 bytes 20.95kB -0.01%
assets/utils-*.js -3 bytes 18.4kB -0.02%
assets/transactions-*.js -186 bytes 18.35kB -1.0%
assets/analytics-*.js 48 bytes 17.08kB 0.28%
assets/GeneralTableRow-*.js -1 bytes 14.95kB -0.01%
assets/PageHeader-*.js -4 bytes 14.83kB -0.03%
assets/routes-*.js -4 bytes 14.56kB -0.03%
assets/_tanstack-*.js 126 bytes 14.21kB 0.89%
assets/settings-*.js -3 bytes 13.81kB -0.02%
assets/blocks-*.js -172 bytes 12.46kB -1.36%
assets/Card-*.js -1.19kB 11.22kB -9.56%
assets/TransactionFunction-*.js -3 bytes 10.45kB -0.03%
assets/block._height._tab-*.js -4 bytes 10.02kB -0.04%
assets/FunctionFilter-*.js -4 bytes 9.86kB -0.04%
assets/useGetConfidentialFASupply-*.js -4 bytes 9.47kB -0.04%
assets/useSubmitTransaction-*.js -4 bytes 8.27kB -0.05%
assets/useGetAccountTokens-*.js -3 bytes 5.58kB -0.05%
assets/createClient-*.js -4 bytes 5.45kB -0.07%
assets/useGetTransaction-*.js 3.6kB 4.29kB 515.78% ⚠️
assets/coingeckoProxy-*.js (New) 3.83kB 3.83kB 100.0% 🚀
assets/Error-*.js -4 bytes 3.24kB -0.12%
assets/useGetAccountResource-*.js -4 bytes 2.48kB -0.16%
assets/TimestampValue-*.js -4 bytes 1.73kB -0.23%
assets/useGetModulePublishHistory-*.js -3 bytes 1.43kB -0.21%
assets/useGetCoinSupplyLimit-*.js -4 bytes 1.2kB -0.33%
assets/useGetAnalyticsData-*.js (New) 1.16kB 1.16kB 100.0% 🚀
assets/useGetBlock-*.js -48 bytes 1.15kB -4.0%
assets/useGetValidatorSet-*.js -3 bytes 1.01kB -0.29%
assets/useGraphqlClient-*.js -4 bytes 867 bytes -0.46%
assets/useViewFunction-*.js -5 bytes 794 bytes -0.63%
assets/useGetAccountResources-*.js -4 bytes 767 bytes -0.52%
assets/TotalTransactions-*.js -361 bytes 748 bytes -32.55%
assets/useGetLedgerInfo-*.js (New) 736 bytes 736 bytes 100.0% 🚀
assets/useGetInMainnet-*.js -4 bytes 270 bytes -1.46%
assets/validators-*._tab-BiSSb_vI.js (New) 218 bytes 218 bytes 100.0% 🚀
assets/client-*.js (Deleted) -107.6kB 0 bytes -100.0% 🗑️
assets/validators-*._tab-CKSRjm30.js (Deleted) -218 bytes 0 bytes -100.0% 🗑️

Files in assets/Index-*.js:

  • ./app/pages/Account/Components/AccountAllTransactions.tsx → Total Size: 15.18kB

  • ./app/components/ApiKeyConfirmDialog.tsx → Total Size: 2.66kB

  • ./app/api/hooks/useAccountHasConfidentialStores.ts → Total Size: 4.54kB

  • ./app/api/hooks/useGetPrice.ts → Total Size: 1.48kB

  • ./app/pages/Account/BalanceCard.tsx → Total Size: 3.89kB

Files in assets/router-*.js:

  • ./app/routes/transactions.tsx → Total Size: 683 bytes

  • ./app/routes/analytics.tsx → Total Size: 895 bytes

  • ./app/routes/object.$address.$tab.tsx → Total Size: 772 bytes

  • ./app/api/queries.ts → Total Size: 2.73kB

  • ./app/routes/coin.$struct.$tab.tsx → Total Size: 844 bytes

  • ./app/router.tsx → Total Size: 1.29kB

  • ./app/routes/blocks.tsx → Total Size: 1.03kB

  • ./app/routes/fungible_asset.$address.$tab.tsx → Total Size: 1.09kB

Files in assets/ValidatorStatusIcon-*.js:

  • ./app/api/hooks/useGetValidators.ts → Total Size: 9.56kB

Files in assets/TransactionsTable-*.js:

  • ./app/pages/Transactions/TransactionsTable.tsx → Total Size: 19.45kB

Files in assets/coins-*.js:

  • ./app/api/hooks/useGetCoinMarketData.ts → Total Size: 2.76kB

Files in assets/transactions-*.js:

  • ./app/pages/Transactions/AllTransactions.tsx → Total Size: 4.93kB

Files in assets/blocks-*.js:

  • ./app/api/hooks/useGetMostRecentBlocks.ts → Total Size: 1.61kB

Files in assets/Card-*.js:

  • ./app/api/hooks/useGetTPS.ts → Total Size: 639 bytes

Files in assets/useGetTransaction-*.js:

  • ./app/api/hooks/useGetTransaction.ts → Total Size: 3.88kB

Files in assets/coingeckoProxy-*.js:

  • ./app/api/hooks/coingeckoProxy.ts → Total Size: 3.71kB

Files in assets/TotalTransactions-*.js:

  • ./app/pages/Analytics/NetworkInfo/TotalTransactions.tsx → Total Size: 494 bytes

Files in assets/useGetLedgerInfo-*.js:

  • ./app/api/hooks/useGetLedgerInfo.ts → Total Size: 1.91kB
view changes for bundle: aptos-explorer-client-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/vendor-*.js 513 bytes 441.88kB 0.12%
assets/index-*.js 3.75kB 238.89kB 1.6%
assets/Index-*.js 2.49kB 166.53kB 1.52%
assets/txn._txnHashOrVersion._tab-*.js 6 bytes 146.7kB 0.0%
assets/v2-*.js (New) 145.16kB 145.16kB 100.0% 🚀
assets/ResourceDataView-*.js -2 bytes 65.11kB -0.0%
assets/validator._address-*.js -6 bytes 35.68kB -0.02%
assets/validators._tab-*.js -5 bytes 34.63kB -0.01%
assets/searchUtils-*.js -2.07kB 22.3kB -8.51%
assets/ValidatorStatusIcon-*.js 733 bytes 16.36kB 4.69%
assets/fungible_asset._address._tab-*.js 3 bytes 15.9kB 0.02%
assets/TransactionsTable-*.js 25 bytes 15.2kB 0.16%
assets/coin._struct._tab-*.js -9 bytes 13.27kB -0.07%
assets/coins-*.js 25 bytes 13.21kB 0.19%
assets/token._tokenId._tab-*.js -2 bytes 10.78kB -0.02%
assets/transactions-*.js -77 bytes 9.87kB -0.77%
assets/utils-*.js -3 bytes 9.79kB -0.03%
assets/analytics-*.js -4 bytes 8.96kB -0.04%
assets/GeneralTableRow-*.js -6 bytes 8.25kB -0.07%
assets/routes-*.js 6 bytes 7.5kB 0.08%
assets/settings-*.js -4 bytes 7.26kB -0.06%
assets/PageHeader-*.js 1 bytes 6.62kB 0.02%
assets/blocks-*.js -59 bytes 6.62kB -0.88%
assets/Card-*.js -614 bytes 6.06kB -9.21%
assets/TransactionFunction-*.js -3 bytes 5.86kB -0.05%
assets/useGetConfidentialFASupply-*.js -9 bytes 5.66kB -0.16%
assets/block._height._tab-*.js -4 bytes 5.36kB -0.07%
assets/FunctionFilter-*.js -4 bytes 4.8kB -0.08%
assets/useSubmitTransaction-*.js -4 bytes 4.08kB -0.1%
assets/useGetAccountTokens-*.js -3 bytes 3.68kB -0.08%
assets/Error-*.js -4 bytes 1.97kB -0.2%
assets/useGetTransaction-*.js 978 bytes 1.35kB 261.5% ⚠️
assets/useGetAccountResource-*.js -4 bytes 1.28kB -0.31%
assets/coingeckoProxy-*.js (New) 1.25kB 1.25kB 100.0% 🚀
assets/releases._tab-*.js -5 bytes 1.22kB -0.41%
assets/TimestampValue-*.js -4 bytes 1.0kB -0.4%
assets/useGetModulePublishHistory-*.js -3 bytes 910 bytes -0.33%
assets/useGetBlock-*.js -38 bytes 570 bytes -6.25%
assets/useGetCoinSupplyLimit-*.js -4 bytes 551 bytes -0.72%
assets/useGetValidatorSet-*.js -3 bytes 477 bytes -0.62%
assets/TotalTransactions-*.js -197 bytes 466 bytes -29.71%
assets/useGetLedgerInfo-*.js (New) 437 bytes 437 bytes 100.0% 🚀
assets/useViewFunction-*.js -5 bytes 408 bytes -1.21%
assets/useGetAccountResources-*.js -4 bytes 398 bytes -1.0%
assets/useParams-*.js -4 bytes 300 bytes -1.32%
assets/validators-*._tab-bdB6OjH_.js (New) 100 bytes 100 bytes 100.0% 🚀
assets/client-*.js (Deleted) -143.0kB 0 bytes -100.0% 🗑️
assets/validators-*._tab-C51q6KGd.js (Deleted) -100 bytes 0 bytes -100.0% 🗑️

Files in assets/index-*.js:

  • ./app/routes/coin.$struct.$tab.tsx → Total Size: 883 bytes

  • ./app/routes/transactions.tsx → Total Size: 722 bytes

  • ./app/api/queries.ts → Total Size: 2.73kB

  • ./app/routes/analytics.tsx → Total Size: 934 bytes

  • ./app/routes/blocks.tsx → Total Size: 1.07kB

  • ./app/routes/fungible_asset.$address.$tab.tsx → Total Size: 1.13kB

  • ./app/router.tsx → Total Size: 1.29kB

  • ./app/routes/object.$address.$tab.tsx → Total Size: 811 bytes

Files in assets/Index-*.js:

  • ./app/components/ApiKeyConfirmDialog.tsx → Total Size: 2.43kB

  • ./app/api/hooks/useGetPrice.ts → Total Size: 715 bytes

  • ./app/pages/Account/Components/AccountAllTransactions.tsx → Total Size: 15.78kB

  • ./app/api/hooks/useAccountHasConfidentialStores.ts → Total Size: 3.03kB

  • ./app/pages/Account/BalanceCard.tsx → Total Size: 4.39kB

Files in assets/ValidatorStatusIcon-*.js:

  • ./app/api/hooks/useGetValidators.ts → Total Size: 7.64kB

Files in assets/TransactionsTable-*.js:

  • ./app/pages/Transactions/TransactionsTable.tsx → Total Size: 22.69kB

Files in assets/coins-*.js:

  • ./app/api/hooks/useGetCoinMarketData.ts → Total Size: 2.36kB

Files in assets/transactions-*.js:

  • ./app/pages/Transactions/AllTransactions.tsx → Total Size: 5.41kB

Files in assets/blocks-*.js:

  • ./app/api/hooks/useGetMostRecentBlocks.ts → Total Size: 1.43kB

Files in assets/Card-*.js:

  • ./app/api/hooks/useGetTPS.ts → Total Size: 639 bytes

Files in assets/useGetTransaction-*.js:

  • ./app/api/hooks/useGetTransaction.ts → Total Size: 2.6kB

Files in assets/coingeckoProxy-*.js:

  • ./app/api/hooks/coingeckoProxy.ts → Total Size: 2.01kB

Files in assets/TotalTransactions-*.js:

  • ./app/pages/Analytics/NetworkInfo/TotalTransactions.tsx → Total Size: 590 bytes

Files in assets/useGetLedgerInfo-*.js:

  • ./app/api/hooks/useGetLedgerInfo.ts → Total Size: 632 bytes

@codecov
Copy link
Copy Markdown

codecov Bot commented May 29, 2026

Codecov Report

❌ Patch coverage is 52.63158% with 72 lines in your changes missing coverage. Please review.
✅ Project coverage is 32.20%. Comparing base (600ff53) to head (e31f279).

Files with missing lines Patch % Lines
app/api/hooks/useGetTransaction.ts 30.43% 32 Missing ⚠️
app/api/hooks/useGetValidators.ts 40.81% 29 Missing ⚠️
app/api/hooks/useAccountHasConfidentialStores.ts 52.17% 11 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1641      +/-   ##
==========================================
+ Coverage   32.04%   32.20%   +0.16%     
==========================================
  Files         188      192       +4     
  Lines        8666     8976     +310     
  Branches     3251     3345      +94     
==========================================
+ Hits         2777     2891     +114     
- Misses       5885     6081     +196     
  Partials        4        4              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.

3 participants