TimeCurve: unify pre-open countdown to startSaleAt with indexer clock and "TimeCurve Opens In" copy #115
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#115
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?
Summary
Unify all TimeCurve-facing countdown UX so that before onchain sale goes live (
block.timestamp < TimeCurve.saleStart()afterstartSaleAt(epoch)/ #114), every prominent timer counts down tosaleStart, with explicit copy "TimeCurve Opens In", using the same indexer-anchored chain clock as the hero timer (useTimecurveHeroTimer,/v1/timecurve/chain-timer, #48).Once the sale is live, timers should continue to count down to
deadline()(round timer) with existing phase-specific narratives where appropriate.Problem statement (current behavior)
/timecurve(Simple) already switches the displayed countdown between pre-start and live usingpreStartCountdownSecvssaleCountdownSecinuseTimeCurveSaleSessionandTimeCurveSimplePage.useTimecurveHeroTimeralways derivessecondsRemainingasdeadline − chainNowfrom indexer fieldsdeadline_sec,block_timestamp_sec, … (indexer/src/chain_timer.rs).startSaleAt,deadline = saleStart + initialTimerSec(TimeCurve.sol). DuringsaleStartPending,deadline − chainNow= (saleStart − chainNow) + initialTimerSec — i.e. not “opens in” — but/timecurve/arenapasses this herosecondsRemainingstraight into the hero clock (TimeCurveArenaView,useTimeCurveArenaModel), so Arena’s digits disagree with Simple in the pre-open window.phaseNarrative, not a universal "TimeCurve Opens In" line.Inventory — where timers / countdowns appear
LaunchGate.tsx+LaunchCountdownPage.tsxVITE_LAUNCH_TIMESTAMP(build-time marketing gate)startSaleAt. Out of scope unless product wants copy alignment only. Eyebrow today: "DOUB launches in".TimeCurveSimplePage.tsx+TimeCurveTimerHero.tsxsaleStart − ledger; live:deadline − chainNow(via session)TimeCurveArenaView.tsxsecondsRemainingfromuseTimecurveHeroTimersaleStartPendinglike Simple.TimeCurveSections.tsx(Arena standings / raw accordion)deadlinerow +secondsRemainingsecondsTimeCurveLiveCharts.tsxsaleStartSec/deadlineSecTimeCurveProtocolPage.tsxdeadline(and related)saleStartrow if missing; informational, not a ticking hero.TimecurveBuyModals.tsxnew_deadlineformatCountdownin Simple / Arena)Data / information needed
Onchain (source of truth)
TimeCurve.saleStart()— epoch set bystartSaleAt(#114).TimeCurve.deadline()— round end; moves with buys after open.TimeCurve.ended().Indexer-anchored clock (UX consistency, #48)
block_timestamp_sec+ skew inuseTimecurveHeroTimer.sale_start_seconGET /v1/timecurve/chain-timerso the same snapshot carries both targets and avoids RPC/indexer drift for the target timestamps (deadline already comes from indexer poll).Phase
derivePhase/ledgerSecIntForPhaseso no page invents a separate “is it open?” rule.Proposed indexer changes (optional but recommended)
File:
indexer/src/chain_timer.rssaleStart()eth_callin the sameblock_idbatch asdeadline,timerCapSec,ended,podium.ChainTimerSnapshotJSON withsale_start_sec(decimal string;"0"when not scheduled — matches contract).x-schema-versioninindexer/src/api.rsper project convention; document indocs/indexer/design.mdif applicable.Frontend: extend
TimecurveChainTimertype +snapshotFromIndexerChainTimerto parsesale_start_sec.Alternative (minimal indexer): keep indexer payload as-is; read
saleStartonly via wagmi /useReadContractsand combine withchainNowSecfromuseTimecurveHeroTimer— satisfies [#48] clock alignment but two sources for target times; document tradeoff.Proposed frontend changes
Core logic (refactor)
Single derived “hero display seconds”
phase(or booleans),saleStartSec,deadlineSec,chainNowSec(from hero hook), fallbacks fromledgerSecIntForPhase.saleStartPending→max(0, saleStartSec − floor(chainNow))saleActive/saleExpiredAwaitingEnd→ existingmax(0, deadline − chainNow)(from indexer deadline snapshot)undefinedor0per existing UXCandidate homes (pick one in implementation):
useTimecurveHeroTimerto acceptsaleStartSec+ phase and returndisplaySecondsRemaining; ortimeCurveSimplePhase.ts/formatTimer.tsused by bothuseTimeCurveSaleSessionanduseTimeCurveArenaModel.Replace Arena’s raw
secondsRemainingfor the big digit display with the derived value above; keep internal uses (e.g. timer extension preview,timerCapSec - secondsRemaining) ondeadline − chainNowonly when sale is live — pre-start should not feed “live round” math.Copy
aria-livestring as appropriate).phaseNarrative/TimeCurveSubnavhints if they duplicate timer meaning.Files likely to touch
frontend/src/pages/timecurve/useTimecurveHeroTimer.tsfrontend/src/pages/timecurve/useTimeCurveSaleSession.tsfrontend/src/pages/TimeCurveSimplePage.tsxfrontend/src/pages/timeCurveArena/useTimeCurveArenaModel.tsxfrontend/src/pages/timeCurveArena/TimeCurveArenaView.tsxfrontend/src/pages/timecurve/TimeCurveTimerHero.tsx(optional: props for title / eyebrow)frontend/src/pages/timecurve/timeCurveSimplePhase.ts(+timeCurveSimplePhase.test.ts)frontend/src/lib/indexerApi.tsfrontend/src/pages/timecurve/TimeCurveSections.tsxfrontend/e2e/timecurve.spec.ts(if selectors / copy change)docs/frontend/timecurve-views.md(scheduled start / #114 section),docs/testing/invariants-and-business-logic.mdif new invariant IDIndexer (if extended):
indexer/src/chain_timer.rs,indexer/src/api.rs,indexer/tests/integration_stage2.rsAcceptance criteria
derivePhase===saleStartPending, Simple and Arena hero countdown digits both equalmax(0, saleStart − indexerAnchoredChainNow)(within 1s tolerance vscast callonsaleStart+ head block).saleStartPendingon both routes.deadlinewith no stale “opens in” label.VITE_INDEXER_URLis unset or chain-timer unreachable, behavior matches existing fallback policy (document any intentional difference).Verification criteria
timeCurveSimplePhase.test.tsor a dedicated helper test file; include edge:saleStartSec == 0(loading),ledgerSecIntjust before/aftersaleStart.YIELDOMEGA_DEV_SALE_START_DELAY_SEC/ stack script behavior from #114 sosaleStartis in the future; open/timecurveand/timecurve/arenaside-by-side — digits and "TimeCurve Opens In" must match.cast call(or read contract UI) forsaleStart,deadline, latestblock.timestampvs displayed remaining seconds.sale_start_secis added, assert JSON field present and matches RPC at sameread_block_number.Checklists
Implementation
formatLaunchCountdown,formatCountdown,secondsRemaininginfrontend/src/pages/timecurve/andtimeCurveArena/).deadline - nowduring pre-start.aria-livestrings.chain-timerwithsale_start_sec; updateindexerApi+ hook.npm run typecheck,npm test, targeted e2e if copy/selectors change.Code review
sale not livebranching outsidederivePhase/ledgerSecIntForPhase.timerCapSecmath uses live deadline semantics only.QA (manual)
saleStart.saleStartboundary: both flip to round countdown within ~1s.prefers-reduced-motion: sparks/animations still respect global rules.Related
startSaleAt/ scheduled saledocs/frontend/timecurve-views.md— Scheduled on-chain starts (repository path; adjust if using GitLab mirror)marked this issue as related to #48
marked this issue as related to #114
Implemented (
7ceb43a)Pre-open (
saleStartPending) hero countdown is unified on Simple + Arena: digits usemax(0, saleStart − floor(chainNow))with the same indexer-anchored clock as #48. IndexerGET /v1/timecurve/chain-timernow includessale_start_sec(same block asdeadline_sec); API schema 1.11.0. Live phases still usedeadline − chainNow; timer-cap / extension preview uses live deadline seconds only.Copy: prominent TimeCurve Opens In (Simple section title, Arena rail label, a11y).
@brouie Please verify when you have a moment (issue stays open).
Checklist for verification
saleStartin the future (e.g. local stack +YIELDOMEGA_DEV_SALE_START_DELAY_SEC), open/timecurveand/timecurve/arenaside-by-side — hero digits match within ~1s.curlchain-timer (or browser) showssale_start_secmatchingcast callsaleStart()at the sameread_block_number.nowcrossessaleStart(no full reload), both routes switch to round countdown + Time Remaining / live semantics; no stale Opens In label.#96).x-schema-version1.11.0 on indexer responses after deploy.Docs / invariants:
timecurve-views.md§115,invariants-and-business-logic.md§115,skills/README.md, guardrails #38.verified on
7ceb43aagainst fresh deploy walk (carried from #114 verification today, same stack). schema/indexer:x-schema-version: 1.11.0on /v1/timecurve/chain-timer;sale_start_sec=1777855603matchescast call saleStart()exactly +deadline_sec=1777942003(delta 86400s = initialTimerSec). pre-start hero on /timecurve: PRE-LAUNCH badge + "TimeCurve Opens In" copy + 9:46 countdown ticking down toward saleStart, Buy CHARM card disabled with "Coming soon". flip to live (anvil_increaseTime 600s): /timecurve transitions to SALE LIVE + "Time Left" + 23:51 deadline countdown; /timecurve/arena shows SALE LIVE + "TIME REMAINING" + 23:52 countdown — both within ~1s of each other reading the same indexer-anchored clock (drift = wall-clock between page loads). Arena explicitly shows "On-chain deadline(): … · read at block 8" and "Head block.timestamp: …" so the dual reference is right there in UI. no stale "Opens In" copy on either route post-flip. deferred 2 items: regression after a real buy (timer-cap / extension preview sanity) and offline-indexer fallback (#96) — neither is the meat of #115's countdown unification fix; happy to retest if you want, but the chain-timer + phase-swap + copy + parity surfaces are the core ones and they're clean. all 4 core items pass.mentioned in issue #96
mentioned in issue #124
mentioned in issue #227