Frontend: CRED as buy payment option (balance, burn check, buyWithCred)

Summary

Add Play CRED as a payment option in the arena buy charm UI alongside CL8Y/ETH/USDM: read wallet CRED balance, compute required burn from selected CHARM, gate submit when insufficient, and call TimeArena.buyWithCred(charmWad) on the correct chain.

Bundled with onchain work in #268 (closed) (100 CRED per CHARM burn + first-buy bonus). This issue is frontend-only but must not ship burn math that disagrees with deployed TimeArena.

Parent: Arena v2 #238 (closed), arena mount #256 (closed), buy router #251 (closed) / #264 (closed).


Current codebase

Area Behavior
Pay asset type PayWithAsset = "cl8y" | "eth" | "usdm" only (frontend/src/lib/kumbayaRoutes.ts).
Buy UI TimeCurveSimplePage / useTimeCurveSaleSession — token picker SIMPLE_AMOUNT_PAY_TOKEN_OPTIONS (CL8Y, ETH, USDM).
Arena v2 submit When isArenaV2, only payWith === "cl8y" allowed; approves DOUB (acceptedAsset) and calls buy().
buyWithCred In timeArenaReadAbi but never called from frontend.
CRED UX today ArenaCharmCredCard — epoch CHARM, pendingCred, claimCred only (read/claim, not pay).
PlayCred address Not in frontend/src/lib/addresses.ts; available onchain as TimeArena.playCred().
Wallet profile Indexer field cred_claimed in WalletProfileModal; no live CRED balance.

Relevant files

  • frontend/src/lib/kumbayaRoutes.ts — extend pay type or parallel payMode
  • frontend/src/pages/TimeCurveSimplePage.tsx — token picker options, labels, slider copy
  • frontend/src/pages/timecurve/useTimeCurveSaleSession.ts — submit path, gates, sizing
  • frontend/src/lib/abis.tsbuyWithCred, ERC20 balanceOf on PlayCred
  • frontend/src/lib/addresses.ts — optional playCred from env or derived read
  • frontend/src/pages/arena/ArenaCharmCredCard.tsx — may share CRED balance hook
  • frontend/src/components/ChainMismatchWriteBarrier.tsx — wrap writes
  • frontend/e2e/arenaE2eHelpers.tsselectPayWith helper
  • docs/frontend/arena-views.md (if present) — UX spec
  • docs/testing/manual-qa-checklists.md — Arena v2 QA section

Why this is needed

  • buyWithCred exists onchain but is unreachable for normal users without wallet/script calls.
  • Participants who earned CRED via DOUB epoch claims cannot spend it in-product.
  • Burn is CHARM-scaled (100 CRED per 1e18 CHARM after onchain issue); UI must show exact required CRED before sign.

Constraints and guardrails

  • No offchain pricing authority — Burn formula must mirror TimeArena (read CRED_PER_CHARM_WAD constant or credBurnForCharm(charmWad) view if added onchain).
  • Wrong-network gating — Use existing ChainMismatchWriteBarrier / chainMismatchWriteMessage (#95 (closed)).
  • Single-chain wagmi (#81 (closed)).
  • Arena v2 only — CRED pay applies when TimeArena / isArenaV2; do not break legacy TimeCurve paths if still mounted (#266 (closed)).
  • CRED non-transferable — No approve step; only balanceOf + buyWithCred (no ERC20 allowance UX).
  • Cooldown — Same nextBuyAllowedAt / buy cooldown UX as DOUB buys.
  • CHARM band — Reuse existing min/max CHARM validation before submit.
  • Referral / WarBow flags — CRED path onchain does not take referral today; UI must not send referral args on CRED buys (or file follow-up if product wants parity).
  • Accessibility — CRED option in same picker pattern as other tokens; aria-label includes CRED.

  1. Resolve PlayCred addressuseReadContract timeArena.playCred() when env unset; cache in session context.
  2. Extend pay model — Add "cred" to pay union (or payWith + payAsset split). Picker shows CRED with distinct label/logo.
  3. Balance readbalanceOf(user) on PlayCred; poll/refetch after buy and on claim.
  4. Burn previewrequiredCred = charmWad * 100n / WAD (or read onchain helper post-deploy). Show in checkout summary and insufficient-CRED gate (mirror CL8Y insufficient pattern in TimeCurveSimplePage).
  5. SubmitwriteContract buyWithCred(charmWad) with gas buffer helper used elsewhere; no DOUB approve.
  6. Slider / amount input — When CRED selected, slider still targets CHARM band; suffix shows “CRED cost” not DOUB/CL8Y.
  7. E2E — Anvil dev deploy mints CRED to test wallet; one spec selects CRED and buys (or mocks read if fork unavailable).
  8. First-buy bonus — No extra UI required if onchain schedules; optional tooltip “First buy grants 150 CRED next epoch” once onchain lands.

Acceptance criteria

  • Connected wallet on MegaETH (configured chain) sees CRED in buy payment picker on /arena (or unified simple buy surface).
  • UI displays wallet CRED balance and required burn for selected CHARM.
  • Buy button disabled with clear message when balance < requiredCred or CHARM out of band.
  • Successful submit calls buyWithCred with correct charmWad (fresh sizing read before tx).
  • Wrong chain: writes blocked; message matches existing pattern.
  • After tx, balance and ArenaCharmCredCard data refresh.
  • ETH/USDM modes unchanged; CRED mode does not require DOUB allowance.
  • Unit/integration tests for burn math helper (if extracted).
  • E2E or component test covers CRED selection path (Anvil/dev).

Test plan — functional paths

# Scenario Expected
1 Select CRED, sufficient balance buyWithCred tx succeeds
2 Select CRED, insufficient balance Disabled submit + error copy
3 Switch CRED → CL8Y DOUB approve path restored
4 CHARM at min/max band Burn preview matches onchain
5 Cooldown active Same error as DOUB buy
6 Not connected Connect prompt
7 Wrong chain Write barrier
8 playCred == address(0) CRED option hidden or disabled with copy
9 Post-buy CRED balance decreases by burn amount
10 Arena paused Buy disabled consistent with DOUB

Test plan — attack vectors / UX failure modes

Vector Mitigation / test
Stale balance Re-read balanceOf immediately before submit; show revert message on failure
Stale CHARM bounds Reuse readFreshTimeCurveBuySizing or arena equivalent
Wrong burn constant in UI Prefer onchain view/constant read; single shared credBurnForCharmWad() util tested against Forge expectations
User selects CRED but session sends buy() E2E asserts function name / calldata
Phishing wrong PlayCred Address only from TimeArena.playCred() on configured arena proxy
Double-submit Disable button while isPending
Scientific notation input Parse errors don’t submit tx

Verification criteria

  • npm test / vitest for any new burn helper.
  • npx playwright test arena spec with CRED path (5 workers per repo convention).
  • Manual QA checklist updated under “Arena v2 — CRED pay”.
  • No new secrets in env; optional VITE_PLAY_CRED_ADDRESS only as override if read fails.
  • Lighthouse/a11y: picker keyboard navigable.

Depends on

  • #268 (closed): 100 CRED per CHARM (and ideally public view for burn). Merge order: contracts first, then frontend (or feature-flag until deployed).

Out of scope

  • Implementing first-buy 150 CRED bonus (onchain issue).
  • Indexer API for CRED balance (wallet can read RPC).
  • Kumbaya / ETH / USDM → CRED swaps.