RPC + indexer architecture for frontend reads (concept + design) #216
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:open_issues
agents
agent:security_audit
agent:verify
area
timecurve
arena
arena-v2
audit
block:feature_review
block:security
bug
contracts
design
devops
docs
documentation
enhancement
epic
feature
frontend
indexer
low priority
megaeth
operations
ops
performance
QA
qa checklist
referrals
removal
security
smart-contracts
tech-debt
testing
time-arena
timecurve
visual-qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
code/yieldomega#216
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Concept and design doc for the polling-reduction work this week. Filing this so you can review and modify before I touch any code.
Architecture
What I found mapping the codebase
Frontend is already calling indexer routes for most global state. The polling problem isn't that everything is on RPC — it's that for several reads we're running indexer AND a redundant RPC poll at the same time.
Routes that exist on indexer today and are already being called from frontend:
Three classes of polling work I see
1. Reads that should drop RPC entirely (run indexer-only)
For these we can stop running both. Indexer becomes the source, RPC drops out unless we need a verified read for a specific UX moment.
2. The one read with no indexer route
useTimeCurveSaleSession.ts:356 coreContracts — total raise, current min/max buy, charm price. Polls 10+ functions at 1s. No /v1/timecurve/sale-state or similar route exists today.
Options:
3. Wallet state — load once by default
useTimeCurveSaleSession.ts:378 polls charmWeight, charmsRedeemed, nextBuyAllowedAt, activeDefendedStreak at 1s.
Per the framing — default to load once on page load or wallet event. Cases:
Defensive fix that doesn't depend on any of the above
9 polling sites are using fixed refetchInterval that bypass getRpcBackoffPollMs — during a 429 storm they keep hammering while the rest of the app correctly backs off. Wiring them into useRpcQueryHealthForRefetch is cheap and lands before any of the migration work.
Sites: useTimeCurveSaleSession.ts:356,378 — warbowPodiumLive.ts:127 — usePodiumReads.ts:190,219 — useTimeCurveArenaModel.tsx:1938 — ReferralProgramEarningsSection.tsx:29 — ReferralRegisterSection.tsx:131,169
Most of these become irrelevant once the read drops RPC entirely, but the fix is independently correct.
Production config finding from this morning
rpc-megaeth-mainnet.globalstake.io in VITE_RPC_URL is dead — CORS preflight fails (no Access-Control-Allow-Origin header), some calls 503. Every fallback cascade through it is wasted. Pulling it from the env var (or replacing it) is config-only.
Suggested rollout
Open questions
mentioned in issue #210
warbow rpc reads are legacy and need to be removed, freshness is not critical on warbow leaderboards/steals/bp (indexer is fast enough for it)
for sale state - need new indexer route
realtime_sendRawTransaction - yes on mainnet.megaeth.com/rpc and other rpc as well
Also do wallet state load on account switch but not chain switch
nextBuyAllowedAt - We do not need to poll because when a user buys, the final epoch where the button should unlock (epoch+3 seconds for reducing failed tx due to outdated rpc data) doesnt change once its registered onchain
On (2) migrate to indexer fully and drop rpc local calls
Also for cl8y balance, we should allow user to manually update with one rpc call, with a refresh button they can click, inline with the cl8y balance
Plan additions (review feedback)
Before implementation, please fold the following into the written plan / acceptance criteria.
1. Doc / invariant / skills pass in the same epic (especially WarBow)
INV-FRONTEND-WARBOW-PODIUM-LIVE,docs/frontend/timecurve-views.md(live WarBow podium), yieldomega guardrails, andwarbowPodiumLivetests currently require the RPCbattlePointsoverlay and ~1s RPC poll for displayed BP.Removing that overlay is the right call for launch RPC load, but it is not a code-only delete: update invariants, timecurve-views, guardrails skill, and tests in the same workstream. Reframe acceptance as: trust indexer
WARBOW_BP_OBSERVATIONS_UNION+ existing event invalidation (#182); drop RPC overlay; verify steal → revenge → flag moves top-3 on indexer-only within normal indexer poll cadence.2.
GET /v1/timecurve/sale-state— short spec before builduseTimeCurveSaleSessioncoreContractsis a large ~1s multicall batch. Before indexer + frontend work, nail down in the issue or a linked note:saleStart,deadline,ended, bounds/price views,totalRaised, timer params, gate flags, router, etc.).as_of_block/as_of_timestamp(same head pattern aschain-timer).currentCharmBoundsWadaffect choice).VITE_INDEXER_URLunset, keep RPC likeusePodiumReadstoday.SCHEMA_VERSIONbump + invariants row; staleness/disclosure if we surface “indexed snapshot.”Display vs submit carve-out (updates
timecurve-viewsinvariant #1): display polling moves to indexer; submit paths keep fresh RPC (readFreshTimeCurveBuySizing, WarBow preflight at click) per #82 / #101.3. Arena steal preflight —
refresh-candidatesalone is insufficientThe 12s multicall at
useTimeCurveArenaModelloads per-candidatebattlePoints,stealsReceivedOnDay, andwarbowGuardUntil.GET /v1/timecurve/warbow/refresh-candidatesreturns addresses only (operator tooling).For RPC removal on steal hero rows, plan explicitly:
warbow/leaderboard(drop BP overlay).warbow/steals-by-victim-day(wire or batch).warbow/guard-latestper candidate or a batch route.describeStealPreflightat steal click (same pattern as buy sizing), not indexer-only at write time.Guardrails / invariants / skills — intentional guarantee shift
This design follows new operational reality at launch: public RPC overload from redundant polling, not a belief that indexer is “more authoritative” than chain state in the abstract.
We are weakening the previous doc stance (contract RPC = primary for global sale/WarBow display) to indexer-primary for display polling, with RPC retained only where safety requires it (wallet-scoped reads, manual refresh, submit-time sizing/preflight).
That is a deliberate trade: slightly weaker “digits always match
eth_callright now” on global UI, in exchange for sustainable RPC use. Guardrails,docs/testing/invariants-and-business-logic.md,docs/frontend/timecurve-views.md, and relevant skills must be updated in the same epic so agents and reviewers do not treat the old RPC-primary rules as blocking this work.mentioned in commit
3e42616af4Implementation summary (@Brouie — please verify)
Pushed to
mainin commit3e42616(feat(frontend,indexer): indexer-primary TimeCurve reads and RPC poll reduction (#216)).This closes the #216 architecture epic per your thread decisions: indexer-primary global display, RPC for wallet + submit safety, no redundant WarBow/podium BP overlays,
realtime_sendRawTransactionon writes.What shipped
GET /v1/timecurve/sale-state(schema 1.23.0), head-RPC batch at same block aschain-timercoreContractsfrom indexer whenVITE_INDEXER_URLset; RPC multicall fallback when unsetbattlePointsoverlays (warbowPodiumLive,usePodiumReads, arena leaderboard overlay)steals-by-victim-day+guard-latest; fresh RPC preflight at steal clicknextBuyAllowedAtpolleth_sendRawTransaction→realtime_sendRawTransaction;waitForWriteReceipt(10s fallback to normal receipt poll) on buy / WarBow / referrals / kumbaya / protocol finalizeglobalstake.iofallback; fixed-interval pollers wired touseRpcQueryHealthForRefetch+ backofftimecurve-views, guardrails,wallet-connection,megaeth,indexer/designKnown follow-up (non-blocking)
mergedArenaTc) instead of reusing indexersale-statelike Simple — separate optimization if RPC load is still high.Verification checklist
Environment: production-like stack with
VITE_INDEXER_URL, MegaETH RPC (mainnet.megaeth.com/rpcor your comma-separated list — notglobalstake.io).Indexer
GET /v1/timecurve/sale-statereturns 200 withread_block_number, bounds,total_raised, gate flags (not 503 when chain-timer is warm)GET /v1/statusshowsschema_version≥ 1.23.0Simple (
/timecurveor Simple view)eth_callstorm ontotalRaised,currentMinBuyAmount, etc. when indexer is on)balanceOfrefreshnextBuyAllowedAtpoll; after buy, cooldown wall updates from receipt + refetchreadFreshTimeCurveBuySizing)Arena
Podiums / WarBow live
Writes & RPC
realtime_sendRawTransactionon MegaETH RPC (or falls back to receipt polling within ~10s)finalizeWarbowPodiumcompleteNo-indexer fallback
VITE_INDEXER_URL→ Simple still loads sale via RPC multicall (no crash)Acceptance criteria
VITE_INDEXER_URLis set; redundant RPC display overlays are removed.GET /v1/timecurve/sale-stateexposes the same fields Simple/Arena consumed fromcoreContracts, anchored to chain-timer head block.nextBuyAllowedAtis not polled on an interval.realtime_sendRawTransactionwithwaitForWriteReceiptfallback.Please reply here with pass/fail per section (or file a follow-up issue for gaps). Thanks!
Follow-up (out of original #216 scope, same architecture)
@Brouie — follow-on work landed on
mainafter the core #216 sale-state / RPC-split PR. This was not in the original issue checklist but addresses the same “indexer + RPC duplicate polls” problem for surfaces called out during mapping.What shipped
useTimeCurveArenaModeluses sharedGET /v1/timecurve/sale-statefor global core (no ~1 HzmergedArenaTccore multicall whenVITE_INDEXER_URLis set). WarBow policy constants stay on a small load-once RPC bundle.fee_router,fee_router_sinks[5],linear_charm_base_price_wad/linear_charm_daily_increment_wad, plus Arena supplement fields (initial_timer_sec,prizes_distributed,owner).LinearCharmPricedisplay from sale-state when indexer is on./v1/fee-router/*).eth_getFilterChangescapability cache so event watches can prefer a capable endpoint (commite5fca5d).Commits:
e5fca5d(frontend + filter probe),a2228fb(indexer 1.24.0 + docs/invariants).Request: additional verification
Could you run through this checklist on a stack with
VITE_INDEXER_URL+ indexer ≥ 1.24.0 (restart indexer after deploy)?GET /v1/timecurve/sale-statereturnsfee_router_sinks(5 rows),linear_charm_*,fee_router,owner,initial_timer_sec,prizes_distributed./timecurve/arena): Network tab shows no repeating ~1 Hzeth_callbatches for the old 27-field TimeCurve core multicall; sale-state GET at ~2s backoff; supplement/warbow-immutable RPC at most once per session until refetch./timecurve): No ~1 HzLinearCharmPriceRPC while sale-state is healthy.sinks(i)RPC poll when indexer is on.timecurve-sale-stateentry (shared key).VITE_INDEXER_URLunset): Arena still loads via fullmergedArenaTc; Simple/Arena/FeeTransparency fall back to RPC.Reply here if anything on the checklist fails or if you want this split into a separate tracking issue.
mentioned in issue #219
mentioned in issue #221
Walked verification end to end on production. Mixed results — most of the new infrastructure is working, but found one regression class that I filed as #221 (Arena retry storm) plus a doc/deploy mismatch on the globalstake claim.
Indexer side
Simple side (/timecurve)
Arena side (/timecurve/arena) — FILED #221
Globalstake removed from VITE_RPC_URL claim — doc/deploy mismatch
Wallet behavior — not walked yet
Writes — not walked yet
429 backoff — not walked yet
No-indexer fallback — not walked
Summary
mentioned in issue #215
mentioned in issue #236
mentioned in issue #237
mentioned in issue #264
mentioned in issue #361