feat: /protocol global USD stats + USTC/LUNC/vFDUSD oracle in one card #550

Closed
opened 2026-08-17 10:35:55 +00:00 by PlasticDigits · 47 comments
PlasticDigits commented 2026-08-17 10:35:55 +00:00 (Migrated from gitlab.com)

Summary

Rebuild /protocol so it is a DEX stats + oracle page: a global USD stats card on top, then one oracle card where the user can select USTC, LUNC, and vFDUSD. Bundle these into one issue. Do not split “ticker chips” vs “merge history” vs “overview 7d/30d fields” vs “new tokens/pairs.”

Related: #515 (ticker-scoped USTC/LUNC oracle), #378 (factory/router audit rows stay on /protocol only), #281 / #333 (/overview 24h rollup + 60s cache), #489 (anti-cognitive-overload), #522 (volume_usd uses USTC feed), #506 (on-chain UST1↔vFDUSD window — different oracle), #520 (Bitget source investigation — do not block).

Current codebase

/protocol is operator-oriented today: hardcoded USTC/USD, two oracle panels, no DEX-wide USD dashboard. Charts already shows a thin 24h overview strip; Protocol does not.

Layer Behavior today
Page ProtocolPage.tsx: title Protocol & Oracle, lecture paragraph about USTC vs TWAP, then On-chain contracts (audit) (protocol-contract-addresses), then USTC / USD (indexer oracle) snapshot card, then Recent USTC/USD history as a separate shell-panel-strong, then Hook events.
Oracle queries getOraclePrice() and getOracleHistory({ limit: 48 }) with no ticker. Client defaults both to ustc. Query keys are ['indexer-oracle-price'] / ['indexer-oracle-history'] — not ticker-scoped. Copy, aria-label, and heading are hardcoded USTC.
Indexer oracle (#515) GET /api/v1/oracle/price and /history are catalogs { metadata, tickers: ["ustc","lunc"] } (X1). Snapshots need /price/{ticker} and /history/{ticker} (X2). OracleTicker is only Ustc | Lunc (indexer/src/indexer/oracle.rs). Frontend already has getOraclePrice(ticker?) / getOracleHistory({ ticker? }) / getOraclePriceCatalog() — Protocol never uses them.
vFDUSD On-chain wrapped First Digital USD (VITE_VFDUSD_TOKEN_ADDRESS, mainnet terra1mnl9azefrqpmu888ar2u6zrcwr80hxlt3avf4300r576cw5ar7esvxsvj3). Not an external oracle ticker. /ust1 uses the ust1-window contract rate (vFDUSD↔UST1), not CEX FDUSD/USD.
Overview API GET /api/v1/overview (indexer/src/api/overview.rs): total_volume_24h, total_volume_24h_usd, total_trades_24h, pair_count, token_count, ustc_price_usd. 60s whole-response cache. Cache miss reads global_stats_24h (24h only). token_count is get_all_assets().len() (loads every row). No 7d/30d volume, no tokens/pairs added in 30d, no actively-traded pair count.
Volume semantics total_volume_24h is SUM(offer_amount) across all offer assets — mixed decimals (6 vs 18). Not a human USD number. USD is SUM(volume_usd) using the USTC oracle only (X4 / #522). Token windows already exist (token_volume_stats 24h/7d/30d); global rollup does not.
New listings assets.created_at / pairs.created_at are indexer first-seen (schema default NOW()). After a DB rebuild they all look “new.” pairs.created_at_block exists; no dedicated 30d COUNT indexes.
Charts overlap ChartsPage.tsx already renders 24h volume (raw + USD), USTC/USD, 24h trades, pairs, tokens from getOverview(). Protocol shows none of this.
Tests ProtocolPage.test.tsx only asserts factory/router/LCD/RPC (#378). e2e/protocol-page.spec.ts smoke: heading + audit rows. Indexer: api_oracle.rs, api_overview.rs, indexer_overview_global_stats.rs.
Docs docs/frontend.md § Protocol: USTC-only. docs/runbooks/indexer-external-oracle.md + skills/AGENTS_INDEXER_EXTERNAL_ORACLE.md: tickers ustc | lunc. Overview runbook: docs/runbooks/overview-global-stats-brin.md.

Why this is needed

  1. LUNC and vFDUSD are first-class TerraClassic / CL8Y assets, but Protocol only shows USTC. Operators and retail who open Protocol to check “the oracle” cannot switch to LUNC/USD or vFDUSD/USD even though #515 already serves LUNC.
  2. Oracle is split across two cards (snapshot vs history) with duplicated USTC headings. One card + ticker control is the actual job.
  3. No DEX-wide USD dashboard. Charts shows mixed-unit 24h total_volume_24h next to USD. Protocol should be the place for USD 24h / 7d / 30d volume, token/pair census, 30d listings, and actively traded pairs — numbers people actually ask for.
  4. Raw total_volume_24h is misleading if used as a headline (sum of incomparable offer amounts). The new card must lead with USD.
  5. vFDUSD has no CEX ticker in the indexer. Selecting it in the UI without a feed would be a fake $1 peg. Need a real FDUSD/USD poll, labeled vFDUSD in the dApp.

Constraints / guardrails

  1. #515 invariants X1–X6 stay. Bare /api/v1/oracle/price and /history remain catalogs. Unknown ticker → 400. Do not restore a numeric body on the catalog path.
  2. X4 unchanged. volume_usd / overview USD conversion stay on the USTC handle. Do not convert DEX volume with LUNC or FDUSD/vFDUSD.
  3. Advisory only (X5). External feeds are not settlement. Do not imply they price swaps, limits, or UST1 mint/redeem. Keep a short “reference” label; do not paste TWAP vs CEX essays (#489).
  4. Do not confuse oracles. Protocol ticker card = indexer CEX/aggregator USD. Per-pair TWAP stays on Charts. UST1 window stays on /ust1. vFDUSD/USD here is wrapped FDUSD’s CEX reference, not the window rate.
  5. vFDUSD ticker mapping. Add OracleTicker::Vfdusd with path segment vfdusd. Poll FDUSD CEX/aggregator symbols (not USTC, not LUNC): CoinGecko id first-digital-usd; MEXC FDUSDUSDT. KuCoin may lack FDUSD — soft-fail that source (same pattern as CoinGecko 429). Do not hardcode $1. UI label vFDUSD / USD.
  6. Allowlist tickers in the UI. Chips/tabs only ustc | lunc | vfdusd. URL ?ticker= (optional) must be allowlisted; javascript:, HTML, ../, unknown → default ustc. pathSegment() already encodes; still never interpolate raw user input into fetch URLs beyond the allowlist.
  7. One oracle card. Snapshot (price, sources table) + history table live in the same shell-panel. Ticker control at the top of that card. Switching ticker refetches both price and history (query keys include ticker).
  8. Stats card above oracle. Page order: short title → Global stats → Oracle (unified) → On-chain contracts (audit) → Hook events. Do not merge factory/router addresses into stats. Do not clone AddressRow onto other pages (#378).
  9. USD headlines. Stats card shows total_volume_*_usd with $ + formatNum(..., 2) (or compact USD). Do not headline mixed-unit total_volume_24h. Optional: keep 24h trade count (dimensionless).
  10. Rollups, not live 30d scans. 7d/30d volume, active-pair count, unique traders must be materialized (extend global_stats_24h or add global_stats keyed by window 24h/7d/30d, refreshed by volume_aggregator ~5 min). Keep 60s /overview response cache (V5). Cache-miss must stay O(1) / cheap COUNT(*) — no SUM/COUNT(DISTINCT) over 30d swap_events per request (DoS; #281/#333).
  11. Define “actively traded pairs.” Count of distinct pair_id with ≥1 swap_events row in the last 24h (materialized on the 24h rollup). Document in the runbook. Do not invent TVL from CG liquidity_in_usd (that field is currently 24h volume USD mislabeled).
  12. “New in 30 days” = indexer first-seen. COUNT(*) where created_at >= now() - 30 days on assets / pairs. Document that a reindex/rebuild makes everything look new. Add supporting indexes if EXPLAIN needs them. token_count should be SELECT COUNT(*) FROM assets, not load-all.
  13. Backward-compatible overview JSON. Add fields; do not rename/remove total_volume_24h, total_volume_24h_usd, total_trades_24h, pair_count, token_count, ustc_price_usd. Charts strip may keep current fields; expanding Charts is optional, not required.
  14. #489 copy. Labels ≤ ~5 words (24h volume, 7d volume, 30d volume, Tokens, New tokens (30d), New pairs (30d), Active pairs). No architecture essays. Keep market-data outage banner (#215) — no VITE_INDEXER_URL leak.
  15. Do not change factory/router display, hook-event filter, TWAP, UST1 window, or swap settlement.
  16. LocalTerra / Postgres. Indexer tests: make setup-indexer-postgres. Frontend smoke can mock indexer; do not skip LocalTerra when asserting the page against a live stack.

Relevant files

File Role
frontend-dapp/src/pages/ProtocolPage.tsx Layout: stats card, unified oracle + ticker, existing audit + hooks
frontend-dapp/src/pages/ProtocolPage.test.tsx RTL: ticker switch, one oracle card, stats fields, #378 regression
frontend-dapp/e2e/protocol-page.spec.ts Playwright smoke
frontend-dapp/src/services/indexer/client.ts getOverview, getOraclePrice / History / catalog; allowlisted ticker
frontend-dapp/src/types/index.ts IndexerOverview, oracle types
frontend-dapp/src/components/ui/StatBox.tsx Stat cells
indexer/src/api/overview.rs Overview response + 60s cache
indexer/src/db/queries/volume.rs Global rollup refresh/read
indexer/src/indexer/volume_aggregator.rs ~5 min refresh
indexer/src/api/oracle.rs Catalog + ticker routes
indexer/src/indexer/oracle.rs Poll loop + CEX symbols
indexer/src/db/queries/oracle.rs oracle_prices by ticker
indexer/tests/api_oracle.rs Catalog / unknown 400 / per-ticker
indexer/tests/api_overview.rs Overview shape
indexer/tests/indexer_overview_global_stats.rs Rollup vs live; EXPLAIN no 30d scan
docs/runbooks/indexer-external-oracle.md Add vfdusd
docs/runbooks/overview-global-stats-brin.md 7d/30d + census fields
docs/indexer-invariants.md X2 tickers + overview row
skills/AGENTS_INDEXER_EXTERNAL_ORACLE.md Playbook
docs/frontend.md Protocol section
scripts/qa/verify-issue-515.sh Pattern for verify-issue-<iid>

Indexer — oracle vfdusd

  1. Extend OracleTicker + OraclePriceHandles with Vfdusd (as_str() = "vfdusd").
  2. Symbols: CoinGecko first-digital-usd; MEXC FDUSDUSDT; KuCoin: use a documented symbol or skip if unlisted (soft-fail, do not abort the tick).
  3. Catalog metadata lists ustc, lunc, vfdusd. Storage already ticker-scoped (oracle_prices).
  4. Unit tests: vFDUSD symbols never equal USTC/LUNC; unknown still 400; fdusd (without v) is 400 unless you also alias it (prefer no alias — one path).
  5. Update #515 skill/runbook/invariants: X2 becomes ustc | lunc | vfdusd. X4 still USTC-only for volume.

Indexer — overview census

Additive OverviewResponse fields (names may match this set):

Field Meaning
total_volume_7d_usd Sum volume_usd over 7d (rollup)
total_volume_30d_usd Sum volume_usd over 30d (rollup)
tokens_added_30d COUNT(*) assets with created_at in last 30d
pairs_added_30d COUNT(*) pairs with created_at in last 30d
active_pairs_24h Distinct pairs with ≥1 swap in last 24h (rollup)

Also useful (include unless EXPLAIN says they blow the budget — then materialize):

Field Meaning
total_trades_7d / total_trades_30d Swap counts
unique_traders_24h Distinct sender in 24h (rollup; not per-request COUNT DISTINCT)

Keep existing 24h USD + trade count + pair_count + token_count. Prefer a global_stats table keyed by window (24h/7d/30d) mirroring token_volume_stats, or extra columns on the single-row table. Refresh in the same aggregator loop. Integration tests: window cutoffs (swap older than 7d not in 7d), empty DB zeros, EXPLAIN on cache-miss path does not scan swap_events for 30d.

Frontend — /protocol

  1. Global stats card (data-testid="protocol-global-stats") above oracle: 24h / 7d / 30d USD volume, tokens, new tokens 30d, new pairs 30d, active pairs 24h, 24h trades. StatBox grid; skeleton + RetryError + existing outage banner (include overview in detectMarketDataOutage).
  2. Unified oracle card (data-testid="protocol-oracle"): role="tablist" chips USTC / LUNC / vFDUSD (same pattern as Wrap/UST1 tabs). Selected ticker drives price + history. Sources table + history table inside this card. Heading like {symbol} / USD.
  3. Optional shareable ?ticker=lunc (allowlist). Default ustc.
  4. Shorten the page lead (#489). Keep audit contract card.
  5. Light + dark; mobile grid wrap.

Acceptance criteria

  • AC1 — Ticker select. /protocol oracle card can select USTC, LUNC, and vFDUSD. Selection loads that ticker’s price and history (/price/{ticker}, /history/{ticker}). Default USTC.
  • AC2 — One oracle card. Snapshot + sources + history are a single card. No second “Recent USTC/USD history” panel. Heading/aria follow the selected ticker.
  • AC3 — vFDUSD is a real feed. Catalog includes vfdusd. GET /price/vfdusd returns FDUSD/USD from CEX/CG (not USTC, not LUNC, not hardcoded 1). Soft-fail missing KuCoin.
  • AC4 — Stats card above oracle. Global USD stats card is above the oracle card and shows 24h, 7d, 30d USD volume, token count, new tokens 30d, new pairs 30d, actively traded pairs (24h definition), plus 24h trades.
  • AC5 — Overview additive API. New fields on GET /api/v1/overview without breaking existing keys. Charts still works.
  • AC6 — No mixed-unit headline. Protocol stats do not present total_volume_24h as USD or as “the” volume.
  • AC7 — Cheap reads. 7d/30d/active-pair/unique-trader figures come from rollups + 60s cache; tests prove cache-miss does not scan 30d swap_events.
  • AC8 — X1–X6. Catalog paths stay catalogs; unknown ticker 400; volume USD still USTC-only; feeds labeled reference.
  • AC9 — Audit + hooks unchanged. Factory/router/LCD/RPC and hook filter still work (#378).
  • AC10 — Copy / a11y. Short labels; tabs keyboard-accessible; outage banner with no env leak; light + dark.
  • AC11 — Verify. make verify-issue-<iid> + indexer oracle/overview tests + RTL + Playwright (5 workers).

Test plan (all paths)

Oracle UI

# Path Assert
O1 Default /protocol USTC selected; price query /price/ustc; history /history/ustc; one oracle card
O2 Click LUNC Query keys/ticker lunc; heading LUNC/USD; history rows are LUNC scale (~1e-5) not USTC (~1e-3) when both mocked
O3 Click vFDUSD vfdusd; heading vFDUSD/USD; ~1.00 scale in mocks, not USTC
O4 Keyboard Tabs are role="tab"; Arrow/Enter/Space move selection
O5 Empty history “No history” inside the same card
O6 Price error Retry on oracle card; stats card independent if overview OK
O7 ?ticker=lunc Opens on LUNC; ?ticker=btc / ?ticker=../ustc / HTML → USTC
O8 Catalog unused tickers UI never offers a chip not in {ustc,lunc,vfdusd} even if catalog grows later without a label map (or map-only known three)

Global stats UI

# Path Assert
S1 Overview success 24h/7d/30d USD formatted; tokens; new tokens 30d; new pairs 30d; active pairs; 24h trades
S2 Loading Skeletons; no undefined / NaN
S3 Overview 502 Outage banner or RetryError; no host:port
S4 Partial/missing new fields Em-dash, not crash (until indexer ships; tests may pin both together)
S5 Order Stats card above oracle in DOM
S6 Raw volume total_volume_24h not shown as the USD headline

Indexer oracle

# Path Assert
I1 GET /price catalog tickers includes ustc, lunc, vfdusd; no price_usd on catalog
I2 /price/vfdusd ticker=vfdusd; empty → price_usd null
I3 /price/fdusd 400 (no silent alias)
I4 /price/btc, /price/USTC, path ..%2f 400 / not a file read
I5 Symbol wiring vFDUSD CG/MEXC ids ≠ USTC/LUNC (lib tests)
I6 History /history/vfdusd Same ticker scoping as #515
I7 make verify-issue-515 Still green (extend catalog assertions)

Indexer overview

# Path Assert
V1 Existing fields still strings/ints Regression api_overview.rs
V2 New USD fields present 7d/30d
V3 Window cutoff Swap 8d old in 30d not 7d; 25h old not in 24h
V4 New tokens/pairs Seed created_at inside/outside 30d
V5 Active pairs Pair with 24h swap counted; idle pair not
V6 Empty DB Zeros, not null crash
V7 EXPLAIN cache-miss No 30d swap_events aggregate on /overview
V8 token_count Matches COUNT(*) not a truncated page

Unit / RTL

  1. Extend ProtocolPage.test.tsx: ticker clicks call getOraclePrice('lunc'|'vfdusd') + matching history; stats testids; factory/router still present; no second history heading.
  2. Client tests: overview type; ticker path encoding of allowlisted values only.
  3. Charts overview mock still type-checks with extra optional fields.

Playwright (5 workers, e2e-smoke)

# Path Assert
P1 /protocol Stats card + oracle card + audit rows
P2 Ticker chips USTC/LUNC/vFDUSD visible; click LUNC updates heading
P3 One history table No duplicate “Recent USTC/USD history” heading
P4 Tablet 820×1180 Cards stack; tabs usable
P5 Phone 390×844 Same; Protocol nav still works
P6 Light + dark Stats + oracle readable (design-tokens-visual protocol shot still exists)

Manual QA

  1. Production or LocalTerra with indexer: USTC ~0.00x, LUNC ~0.0000x, vFDUSD ~1.00 — not 100× swapped (#515 class).
  2. 24h USD on Protocol vs Charts 24h Volume (USD) match within rollup lag (~5 min).
  3. Confirm factory/router still match Coolify env.

Test plan — attack, hack, and abuse vectors

# Vector What to prove
A1 Ticker confusion / 100× lie vFDUSD poll must not use USTC or LUNC CEX ids. UI must not label LUNC as USTC. Regression of #515: catalog is not a USTC price.
A2 Fake $1 peg vFDUSD is polled, not hardcoded 1 / 1.0. Depeg (mock 0.87) must display.
A3 Path injection ?ticker= and /price/{ticker} only allowlist. ../, %2e%2e, ustc/../../, ticker[], SQL fragments → default or 400. No string-paste into SQL (enum parse only).
A4 XSS Ticker/source names from indexer rendered as text, not innerHTML. javascript: query ignored.
A5 Open redirect / SSRF Oracle poll URLs stay hardcoded CEX/CG hosts. Do not take user-supplied oracle URLs. Stats/oracle UI does not window.open indexer logo_url.
A6 DoS via overview 7d/30d/active/unique must not live-scan swap_events per GET. Burst still served from 60s cache + rollup. Add a test that EXPLAIN avoids seq-scan aggregates.
A7 DoS via history limit Existing history limit cap (1000) unchanged; Protocol uses 48.
A8 Cache poisoning / mixed ticker In-memory handles per ticker; writing vFDUSD must not overwrite USTC used for volume_usd.
A9 Integer overflow / NaN X6: non-finite f64 → safe BigDecimal. UI: non-finite USD → em-dash, not Infinity.
A10 Listing inflation “New tokens/pairs 30d” is indexer created_at, not a claim of on-chain genesis. After --fresh DB, UI may show all as new — copy must not say “launched on chain this month.”
A11 Active-pair gaming Dust swaps can inflate active_pairs_24h. Document; do not add a USD floor in this issue unless cheap. Do not treat it as unique traders.
A12 Volume USD manipulation Still USTC-only conversion. Thin USTC CEX print can skew USD; keep “reference” labeling; do not switch conversion to vFDUSD because it is ~$1.
A13 Look-alike ticker Reject ustc , lunc%00, VFDUSD path variants except documented case-insensitive parse (today to_ascii_lowercase — keep; do not accept uѕtc homoglyphs as USTC beyond ASCII).
A14 Outage copy leak Failed overview/oracle: retail banner; no LCD/indexer host, tokens, or .env names (#215).
A15 Clickjacking / overlay No position:fixed stats overlay on wallet/clickwrap.
A16 Hook filter Unrelated but same page: hook contract filter stays a search string to indexed events, not an LCD query amplification.

Verification criteria

  1. make verify-issue-<iid> fails if: Protocol still calls getOraclePrice() with no ticker; two oracle history headings remain; OracleTicker::ALL length is 2; overview lacks 7d/30d USD + 30d listing + active-pair fields; get_all_assets().len() still implements token_count.
  2. Indexer: cargo test --lib oracle; cargo test --test api_oracle --test api_overview --test indexer_overview_global_stats -- --test-threads=1.
  3. RTL Protocol tests cover ticker switch + stats + #378 addresses.
  4. Playwright P1–P6 green (5 workers, e2e-smoke).
  5. Docs: docs/frontend.md Protocol section, oracle runbook + skill (X2 includes vfdusd), overview runbook + docs/indexer-invariants.md, AGENTS.md link to make verify-issue-<iid>.
  6. make verify-issue-515 still passes.
  7. Light + dark eyeball: stats above oracle; three ticker chips; audit card still present.

Out of scope

  • Bitget as a fourth oracle source (#520).
  • Using vFDUSD/LUNC for volume_usd conversion.
  • On-chain TWAP UI on Protocol; UST1 window mint/redeem.
  • True TVL / reserves USD (book snapshot liquidity is not ready as TVL).
  • Changing Charts overview strip (optional follow-up).
  • Hook-event UX redesign.
  • Moving factory/router off /protocol or onto Swap confirmation.
## Summary Rebuild `/protocol` so it is a **DEX stats + oracle** page: a **global USD stats** card on top, then **one** oracle card where the user can select **USTC**, **LUNC**, and **vFDUSD**. Bundle these into **one** issue. Do **not** split “ticker chips” vs “merge history” vs “overview 7d/30d fields” vs “new tokens/pairs.” Related: [#515](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/515) (ticker-scoped USTC/LUNC oracle), [#378](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/378) (factory/router audit rows stay on `/protocol` only), [#281](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/281) / [#333](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/333) (`/overview` 24h rollup + 60s cache), [#489](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/489) (anti-cognitive-overload), [#522](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/522) (`volume_usd` uses **USTC** feed), [#506](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/506) (on-chain UST1↔vFDUSD **window** — different oracle), [#520](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/520) (Bitget source investigation — **do not block**). ## Current codebase `/protocol` is operator-oriented today: hardcoded **USTC/USD**, two oracle panels, no DEX-wide USD dashboard. Charts already shows a thin 24h overview strip; Protocol does not. | Layer | Behavior today | |-------|----------------| | **Page** | [`ProtocolPage.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/pages/ProtocolPage.tsx): title **Protocol & Oracle**, lecture paragraph about USTC vs TWAP, then **On-chain contracts (audit)** (`protocol-contract-addresses`), then **USTC / USD (indexer oracle)** snapshot card, then **Recent USTC/USD history** as a **separate** `shell-panel-strong`, then **Hook events**. | | **Oracle queries** | `getOraclePrice()` and `getOracleHistory({ limit: 48 })` with **no ticker**. Client defaults both to **`ustc`**. Query keys are `['indexer-oracle-price']` / `['indexer-oracle-history']` — not ticker-scoped. Copy, `aria-label`, and heading are hardcoded USTC. | | **Indexer oracle (#515)** | `GET /api/v1/oracle/price` and `/history` are **catalogs** `{ metadata, tickers: ["ustc","lunc"] }` (**X1**). Snapshots need `/price/{ticker}` and `/history/{ticker}` (**X2**). `OracleTicker` is **only** `Ustc` \| `Lunc` ([`indexer/src/indexer/oracle.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/indexer/oracle.rs)). Frontend already has `getOraclePrice(ticker?)` / `getOracleHistory({ ticker? })` / `getOraclePriceCatalog()` — Protocol never uses them. | | **vFDUSD** | On-chain wrapped First Digital USD (`VITE_VFDUSD_TOKEN_ADDRESS`, mainnet `terra1mnl9azefrqpmu888ar2u6zrcwr80hxlt3avf4300r576cw5ar7esvxsvj3`). **Not** an external oracle ticker. `/ust1` uses the **ust1-window** contract rate (vFDUSD↔UST1), not CEX FDUSD/USD. | | **Overview API** | `GET /api/v1/overview` ([`indexer/src/api/overview.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/api/overview.rs)): `total_volume_24h`, `total_volume_24h_usd`, `total_trades_24h`, `pair_count`, `token_count`, `ustc_price_usd`. 60s whole-response cache. Cache miss reads **`global_stats_24h`** (24h only). `token_count` is `get_all_assets().len()` (loads every row). **No** 7d/30d volume, **no** tokens/pairs added in 30d, **no** actively-traded pair count. | | **Volume semantics** | `total_volume_24h` is `SUM(offer_amount)` across **all** offer assets — mixed decimals (6 vs 18). **Not** a human USD number. USD is `SUM(volume_usd)` using the **USTC** oracle only (**X4** / [#522](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/522)). Token windows already exist (`token_volume_stats` 24h/7d/30d); global rollup does not. | | **New listings** | `assets.created_at` / `pairs.created_at` are **indexer first-seen** (schema default `NOW()`). After a DB rebuild they all look “new.” `pairs.created_at_block` exists; no dedicated 30d COUNT indexes. | | **Charts overlap** | [`ChartsPage.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/pages/ChartsPage.tsx) already renders 24h volume (raw + USD), USTC/USD, 24h trades, pairs, tokens from `getOverview()`. Protocol shows none of this. | | **Tests** | [`ProtocolPage.test.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/pages/ProtocolPage.test.tsx) only asserts factory/router/LCD/RPC (#378). [`e2e/protocol-page.spec.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/e2e/protocol-page.spec.ts) smoke: heading + audit rows. Indexer: `api_oracle.rs`, `api_overview.rs`, `indexer_overview_global_stats.rs`. | | **Docs** | [`docs/frontend.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/frontend.md) § Protocol: USTC-only. [`docs/runbooks/indexer-external-oracle.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/runbooks/indexer-external-oracle.md) + [`skills/AGENTS_INDEXER_EXTERNAL_ORACLE.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/skills/AGENTS_INDEXER_EXTERNAL_ORACLE.md): tickers `ustc` \| `lunc`. Overview runbook: [`docs/runbooks/overview-global-stats-brin.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/runbooks/overview-global-stats-brin.md). | ## Why this is needed 1. **LUNC and vFDUSD are first-class TerraClassic / CL8Y assets**, but Protocol only shows USTC. Operators and retail who open Protocol to check “the oracle” cannot switch to LUNC/USD or vFDUSD/USD even though #515 already serves LUNC. 2. **Oracle is split across two cards** (snapshot vs history) with duplicated USTC headings. One card + ticker control is the actual job. 3. **No DEX-wide USD dashboard.** Charts shows mixed-unit 24h `total_volume_24h` next to USD. Protocol should be the place for **USD** 24h / 7d / 30d volume, token/pair census, 30d listings, and actively traded pairs — numbers people actually ask for. 4. **Raw `total_volume_24h` is misleading** if used as a headline (sum of incomparable offer amounts). The new card must lead with **USD**. 5. **vFDUSD has no CEX ticker in the indexer.** Selecting it in the UI without a feed would be a fake `$1` peg. Need a real **FDUSD/USD** poll, labeled vFDUSD in the dApp. ## Constraints / guardrails 1. **#515 invariants X1–X6 stay.** Bare `/api/v1/oracle/price` and `/history` remain catalogs. Unknown ticker → **400**. Do not restore a numeric body on the catalog path. 2. **X4 unchanged.** `volume_usd` / overview USD conversion stay on the **USTC** handle. Do **not** convert DEX volume with LUNC or FDUSD/vFDUSD. 3. **Advisory only (X5).** External feeds are **not** settlement. Do not imply they price swaps, limits, or UST1 mint/redeem. Keep a short “reference” label; do not paste TWAP vs CEX essays (#489). 4. **Do not confuse oracles.** Protocol ticker card = indexer **CEX/aggregator** USD. Per-pair **TWAP** stays on Charts. **UST1 window** stays on `/ust1`. vFDUSD/USD here is **wrapped FDUSD’s CEX reference**, not the window rate. 5. **vFDUSD ticker mapping.** Add `OracleTicker::Vfdusd` with path segment **`vfdusd`**. Poll **FDUSD** CEX/aggregator symbols (not USTC, not LUNC): CoinGecko id `first-digital-usd`; MEXC `FDUSDUSDT`. KuCoin may lack FDUSD — **soft-fail** that source (same pattern as CoinGecko 429). Do **not** hardcode `$1`. UI label **vFDUSD / USD**. 6. **Allowlist tickers in the UI.** Chips/tabs only `ustc` \| `lunc` \| `vfdusd`. URL `?ticker=` (optional) must be allowlisted; `javascript:`, HTML, `../`, unknown → default `ustc`. `pathSegment()` already encodes; still never interpolate raw user input into fetch URLs beyond the allowlist. 7. **One oracle card.** Snapshot (price, sources table) + history table live in the **same** `shell-panel`. Ticker control at the top of that card. Switching ticker refetches **both** price and history (query keys include ticker). 8. **Stats card above oracle.** Page order: short title → **Global stats** → **Oracle** (unified) → **On-chain contracts (audit)** → **Hook events**. Do not merge factory/router addresses into stats. Do not clone `AddressRow` onto other pages (#378). 9. **USD headlines.** Stats card shows `total_volume_*_usd` with `$` + `formatNum(..., 2)` (or compact USD). Do **not** headline mixed-unit `total_volume_24h`. Optional: keep 24h **trade count** (dimensionless). 10. **Rollups, not live 30d scans.** 7d/30d volume, active-pair count, unique traders must be **materialized** (extend `global_stats_24h` or add `global_stats` keyed by window `24h`/`7d`/`30d`, refreshed by [`volume_aggregator`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/indexer/volume_aggregator.rs) ~5 min). Keep **60s** `/overview` response cache (**V5**). Cache-miss must stay O(1) / cheap `COUNT(*)` — **no** `SUM`/`COUNT(DISTINCT)` over 30d `swap_events` per request (DoS; #281/#333). 11. **Define “actively traded pairs.”** Count of distinct `pair_id` with ≥1 `swap_events` row in the **last 24h** (materialized on the 24h rollup). Document in the runbook. Do not invent TVL from CG `liquidity_in_usd` (that field is currently 24h volume USD mislabeled). 12. **“New in 30 days” = indexer first-seen.** `COUNT(*)` where `created_at >= now() - 30 days` on `assets` / `pairs`. Document that a **reindex/rebuild** makes everything look new. Add supporting indexes if EXPLAIN needs them. `token_count` should be `SELECT COUNT(*) FROM assets`, not load-all. 13. **Backward-compatible overview JSON.** Add fields; do not rename/remove `total_volume_24h`, `total_volume_24h_usd`, `total_trades_24h`, `pair_count`, `token_count`, `ustc_price_usd`. Charts strip may keep current fields; expanding Charts is **optional**, not required. 14. **#489 copy.** Labels ≤ ~5 words (`24h volume`, `7d volume`, `30d volume`, `Tokens`, `New tokens (30d)`, `New pairs (30d)`, `Active pairs`). No architecture essays. Keep market-data outage banner (#215) — no `VITE_INDEXER_URL` leak. 15. **Do not change** factory/router display, hook-event filter, TWAP, UST1 window, or swap settlement. 16. **LocalTerra / Postgres.** Indexer tests: `make setup-indexer-postgres`. Frontend smoke can mock indexer; do not skip LocalTerra when asserting the page against a live stack. ## Relevant files | File | Role | |------|------| | [`frontend-dapp/src/pages/ProtocolPage.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/pages/ProtocolPage.tsx) | Layout: stats card, unified oracle + ticker, existing audit + hooks | | [`frontend-dapp/src/pages/ProtocolPage.test.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/pages/ProtocolPage.test.tsx) | RTL: ticker switch, one oracle card, stats fields, #378 regression | | [`frontend-dapp/e2e/protocol-page.spec.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/e2e/protocol-page.spec.ts) | Playwright smoke | | [`frontend-dapp/src/services/indexer/client.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/services/indexer/client.ts) | `getOverview`, `getOraclePrice` / `History` / catalog; allowlisted ticker | | [`frontend-dapp/src/types/index.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/types/index.ts) | `IndexerOverview`, oracle types | | [`frontend-dapp/src/components/ui/StatBox.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/components/ui/StatBox.tsx) | Stat cells | | [`indexer/src/api/overview.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/api/overview.rs) | Overview response + 60s cache | | [`indexer/src/db/queries/volume.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/db/queries/volume.rs) | Global rollup refresh/read | | [`indexer/src/indexer/volume_aggregator.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/indexer/volume_aggregator.rs) | ~5 min refresh | | [`indexer/src/api/oracle.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/api/oracle.rs) | Catalog + ticker routes | | [`indexer/src/indexer/oracle.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/indexer/oracle.rs) | Poll loop + CEX symbols | | [`indexer/src/db/queries/oracle.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/db/queries/oracle.rs) | `oracle_prices` by ticker | | [`indexer/tests/api_oracle.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/tests/api_oracle.rs) | Catalog / unknown 400 / per-ticker | | [`indexer/tests/api_overview.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/tests/api_overview.rs) | Overview shape | | [`indexer/tests/indexer_overview_global_stats.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/tests/indexer_overview_global_stats.rs) | Rollup vs live; EXPLAIN no 30d scan | | [`docs/runbooks/indexer-external-oracle.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/runbooks/indexer-external-oracle.md) | Add `vfdusd` | | [`docs/runbooks/overview-global-stats-brin.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/runbooks/overview-global-stats-brin.md) | 7d/30d + census fields | | [`docs/indexer-invariants.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/indexer-invariants.md) | X2 tickers + overview row | | [`skills/AGENTS_INDEXER_EXTERNAL_ORACLE.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/skills/AGENTS_INDEXER_EXTERNAL_ORACLE.md) | Playbook | | [`docs/frontend.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/frontend.md) | Protocol section | | [`scripts/qa/verify-issue-515.sh`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/scripts/qa/verify-issue-515.sh) | Pattern for `verify-issue-<iid>` | ## Recommended direction ### Indexer — oracle `vfdusd` 1. Extend `OracleTicker` + `OraclePriceHandles` with `Vfdusd` (`as_str() = "vfdusd"`). 2. Symbols: CoinGecko `first-digital-usd`; MEXC `FDUSDUSDT`; KuCoin: use a documented symbol **or skip** if unlisted (soft-fail, do not abort the tick). 3. Catalog metadata lists `ustc`, `lunc`, `vfdusd`. Storage already ticker-scoped (`oracle_prices`). 4. Unit tests: vFDUSD symbols never equal USTC/LUNC; unknown still 400; `fdusd` (without `v`) is **400** unless you also alias it (prefer **no alias** — one path). 5. Update #515 skill/runbook/invariants: X2 becomes `ustc` \| `lunc` \| `vfdusd`. X4 still USTC-only for volume. ### Indexer — overview census Additive `OverviewResponse` fields (names may match this set): | Field | Meaning | |-------|---------| | `total_volume_7d_usd` | Sum `volume_usd` over 7d (rollup) | | `total_volume_30d_usd` | Sum `volume_usd` over 30d (rollup) | | `tokens_added_30d` | `COUNT(*)` assets with `created_at` in last 30d | | `pairs_added_30d` | `COUNT(*)` pairs with `created_at` in last 30d | | `active_pairs_24h` | Distinct pairs with ≥1 swap in last 24h (rollup) | Also useful (include unless EXPLAIN says they blow the budget — then materialize): | Field | Meaning | |-------|---------| | `total_trades_7d` / `total_trades_30d` | Swap counts | | `unique_traders_24h` | Distinct `sender` in 24h (rollup; **not** per-request `COUNT DISTINCT`) | Keep existing 24h USD + trade count + `pair_count` + `token_count`. Prefer a `global_stats` table keyed by `window` (`24h`/`7d`/`30d`) mirroring `token_volume_stats`, or extra columns on the single-row table. Refresh in the same aggregator loop. Integration tests: window cutoffs (swap older than 7d not in 7d), empty DB zeros, EXPLAIN on cache-miss path does **not** scan `swap_events` for 30d. ### Frontend — `/protocol` 1. **Global stats** card (`data-testid="protocol-global-stats"`) **above** oracle: 24h / 7d / 30d **USD** volume, tokens, new tokens 30d, new pairs 30d, active pairs 24h, 24h trades. `StatBox` grid; skeleton + RetryError + existing outage banner (include overview in `detectMarketDataOutage`). 2. **Unified oracle** card (`data-testid="protocol-oracle"`): `role="tablist"` chips **USTC** / **LUNC** / **vFDUSD** (same pattern as Wrap/UST1 tabs). Selected ticker drives price + history. Sources table + history table **inside this card**. Heading like `{symbol} / USD`. 3. Optional shareable `?ticker=lunc` (allowlist). Default `ustc`. 4. Shorten the page lead (#489). Keep audit contract card. 5. Light + dark; mobile grid wrap. ## Acceptance criteria - [ ] **AC1 — Ticker select.** `/protocol` oracle card can select **USTC**, **LUNC**, and **vFDUSD**. Selection loads that ticker’s price **and** history (`/price/{ticker}`, `/history/{ticker}`). Default USTC. - [ ] **AC2 — One oracle card.** Snapshot + sources + history are a **single** card. No second “Recent USTC/USD history” panel. Heading/aria follow the selected ticker. - [ ] **AC3 — vFDUSD is a real feed.** Catalog includes `vfdusd`. `GET /price/vfdusd` returns FDUSD/USD from CEX/CG (not USTC, not LUNC, not hardcoded `1`). Soft-fail missing KuCoin. - [ ] **AC4 — Stats card above oracle.** Global USD stats card is **above** the oracle card and shows 24h, 7d, 30d **USD** volume, token count, new tokens 30d, new pairs 30d, actively traded pairs (24h definition), plus 24h trades. - [ ] **AC5 — Overview additive API.** New fields on `GET /api/v1/overview` without breaking existing keys. Charts still works. - [ ] **AC6 — No mixed-unit headline.** Protocol stats do not present `total_volume_24h` as USD or as “the” volume. - [ ] **AC7 — Cheap reads.** 7d/30d/active-pair/unique-trader figures come from rollups + 60s cache; tests prove cache-miss does not scan 30d `swap_events`. - [ ] **AC8 — X1–X6.** Catalog paths stay catalogs; unknown ticker 400; volume USD still USTC-only; feeds labeled reference. - [ ] **AC9 — Audit + hooks unchanged.** Factory/router/LCD/RPC and hook filter still work (#378). - [ ] **AC10 — Copy / a11y.** Short labels; tabs keyboard-accessible; outage banner with no env leak; light + dark. - [ ] **AC11 — Verify.** `make verify-issue-<iid>` + indexer oracle/overview tests + RTL + Playwright (5 workers). ## Test plan (all paths) ### Oracle UI | # | Path | Assert | |---|------|--------| | O1 | Default `/protocol` | USTC selected; price query `/price/ustc`; history `/history/ustc`; one oracle card | | O2 | Click LUNC | Query keys/ticker `lunc`; heading LUNC/USD; history rows are LUNC scale (~1e-5) not USTC (~1e-3) when both mocked | | O3 | Click vFDUSD | `vfdusd`; heading vFDUSD/USD; ~1.00 scale in mocks, not USTC | | O4 | Keyboard | Tabs are `role="tab"`; Arrow/Enter/Space move selection | | O5 | Empty history | “No history” inside the **same** card | | O6 | Price error | Retry on oracle card; stats card independent if overview OK | | O7 | `?ticker=lunc` | Opens on LUNC; `?ticker=btc` / `?ticker=../ustc` / HTML → USTC | | O8 | Catalog unused tickers | UI never offers a chip not in `{ustc,lunc,vfdusd}` even if catalog grows later without a label map (or map-only known three) | ### Global stats UI | # | Path | Assert | |---|------|--------| | S1 | Overview success | 24h/7d/30d USD formatted; tokens; new tokens 30d; new pairs 30d; active pairs; 24h trades | | S2 | Loading | Skeletons; no `undefined` / `NaN` | | S3 | Overview 502 | Outage banner or RetryError; no host:port | | S4 | Partial/missing new fields | Em-dash, not crash (until indexer ships; tests may pin both together) | | S5 | Order | Stats card **above** oracle in DOM | | S6 | Raw volume | `total_volume_24h` not shown as the USD headline | ### Indexer oracle | # | Path | Assert | |---|------|--------| | I1 | `GET /price` catalog | `tickers` includes `ustc`, `lunc`, `vfdusd`; no `price_usd` on catalog | | I2 | `/price/vfdusd` | `ticker=vfdusd`; empty → `price_usd` null | | I3 | `/price/fdusd` | **400** (no silent alias) | | I4 | `/price/btc`, `/price/USTC`, path `..%2f` | 400 / not a file read | | I5 | Symbol wiring | vFDUSD CG/MEXC ids ≠ USTC/LUNC (lib tests) | | I6 | History `/history/vfdusd` | Same ticker scoping as #515 | | I7 | `make verify-issue-515` | Still green (extend catalog assertions) | ### Indexer overview | # | Path | Assert | |---|------|--------| | V1 | Existing fields still strings/ints | Regression `api_overview.rs` | | V2 | New USD fields present | 7d/30d | | V3 | Window cutoff | Swap 8d old in 30d not 7d; 25h old not in 24h | | V4 | New tokens/pairs | Seed `created_at` inside/outside 30d | | V5 | Active pairs | Pair with 24h swap counted; idle pair not | | V6 | Empty DB | Zeros, not null crash | | V7 | EXPLAIN cache-miss | No 30d `swap_events` aggregate on `/overview` | | V8 | `token_count` | Matches `COUNT(*)` not a truncated page | ### Unit / RTL 1. Extend `ProtocolPage.test.tsx`: ticker clicks call `getOraclePrice('lunc'|'vfdusd')` + matching history; stats testids; factory/router still present; no second history heading. 2. Client tests: overview type; ticker path encoding of allowlisted values only. 3. Charts overview mock still type-checks with extra optional fields. ### Playwright (5 workers, `e2e-smoke`) | # | Path | Assert | |---|------|--------| | P1 | `/protocol` | Stats card + oracle card + audit rows | | P2 | Ticker chips | USTC/LUNC/vFDUSD visible; click LUNC updates heading | | P3 | One history table | No duplicate “Recent USTC/USD history” heading | | P4 | Tablet 820×1180 | Cards stack; tabs usable | | P5 | Phone 390×844 | Same; Protocol nav still works | | P6 | Light + dark | Stats + oracle readable (`design-tokens-visual` protocol shot still exists) | ### Manual QA 1. Production or LocalTerra with indexer: USTC ~0.00x, LUNC ~0.0000x, vFDUSD ~1.00 — **not** 100× swapped (#515 class). 2. 24h USD on Protocol vs Charts 24h Volume (USD) match within rollup lag (~5 min). 3. Confirm factory/router still match Coolify env. ## Test plan — attack, hack, and abuse vectors | # | Vector | What to prove | |---|--------|----------------| | A1 | **Ticker confusion / 100× lie** | vFDUSD poll must not use USTC or LUNC CEX ids. UI must not label LUNC as USTC. Regression of #515: catalog is not a USTC price. | | A2 | **Fake $1 peg** | vFDUSD is polled, not hardcoded `1` / `1.0`. Depeg (mock `0.87`) must display. | | A3 | **Path injection** | `?ticker=` and `/price/{ticker}` only allowlist. `../`, `%2e%2e`, `ustc/../../`, `ticker[]`, SQL fragments → default or 400. No string-paste into SQL (enum parse only). | | A4 | **XSS** | Ticker/source names from indexer rendered as text, not `innerHTML`. `javascript:` query ignored. | | A5 | **Open redirect / SSRF** | Oracle poll URLs stay hardcoded CEX/CG hosts. Do not take user-supplied oracle URLs. Stats/oracle UI does not `window.open` indexer `logo_url`. | | A6 | **DoS via overview** | 7d/30d/active/unique **must not** live-scan `swap_events` per GET. Burst still served from 60s cache + rollup. Add a test that EXPLAIN avoids seq-scan aggregates. | | A7 | **DoS via history limit** | Existing history `limit` cap (1000) unchanged; Protocol uses 48. | | A8 | **Cache poisoning / mixed ticker** | In-memory handles per ticker; writing vFDUSD must not overwrite USTC used for `volume_usd`. | | A9 | **Integer overflow / NaN** | X6: non-finite f64 → safe BigDecimal. UI: non-finite USD → em-dash, not `Infinity`. | | A10 | **Listing inflation** | “New tokens/pairs 30d” is indexer `created_at`, not a claim of on-chain genesis. After `--fresh` DB, UI may show all as new — copy must not say “launched on chain this month.” | | A11 | **Active-pair gaming** | Dust swaps can inflate `active_pairs_24h`. Document; do not add a USD floor in this issue unless cheap. Do not treat it as unique traders. | | A12 | **Volume USD manipulation** | Still USTC-only conversion. Thin USTC CEX print can skew USD; keep “reference” labeling; do not switch conversion to vFDUSD because it is ~$1. | | A13 | **Look-alike ticker** | Reject `ustc `, `lunc%00`, `VFDUSD` path variants except documented case-insensitive parse (today `to_ascii_lowercase` — keep; do not accept `uѕtc` homoglyphs as USTC beyond ASCII). | | A14 | **Outage copy leak** | Failed overview/oracle: retail banner; no LCD/indexer host, tokens, or `.env` names (#215). | | A15 | **Clickjacking / overlay** | No `position:fixed` stats overlay on wallet/clickwrap. | | A16 | **Hook filter** | Unrelated but same page: hook contract filter stays a search string to indexed events, not an LCD query amplification. | ## Verification criteria 1. `make verify-issue-<iid>` fails if: Protocol still calls `getOraclePrice()` with no ticker; two oracle history headings remain; `OracleTicker::ALL` length is 2; overview lacks 7d/30d USD + 30d listing + active-pair fields; `get_all_assets().len()` still implements `token_count`. 2. Indexer: `cargo test --lib oracle`; `cargo test --test api_oracle --test api_overview --test indexer_overview_global_stats -- --test-threads=1`. 3. RTL Protocol tests cover ticker switch + stats + #378 addresses. 4. Playwright P1–P6 green (5 workers, `e2e-smoke`). 5. Docs: `docs/frontend.md` Protocol section, oracle runbook + skill (X2 includes `vfdusd`), overview runbook + `docs/indexer-invariants.md`, `AGENTS.md` link to `make verify-issue-<iid>`. 6. `make verify-issue-515` still passes. 7. Light + dark eyeball: stats above oracle; three ticker chips; audit card still present. ## Out of scope - Bitget as a fourth oracle **source** ([#520](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/520)). - Using vFDUSD/LUNC for `volume_usd` conversion. - On-chain TWAP UI on Protocol; UST1 window mint/redeem. - True TVL / reserves USD (book snapshot liquidity is not ready as TVL). - Changing Charts overview strip (optional follow-up). - Hook-event UX redesign. - Moving factory/router off `/protocol` or onto Swap confirmation.
PlasticDigits commented 2026-08-17 10:35:57 +00:00 (Migrated from gitlab.com)

marked as related to #515

marked as related to #515
PlasticDigits commented 2026-08-17 10:35:58 +00:00 (Migrated from gitlab.com)

marked as related to #378

marked as related to #378
PlasticDigits commented 2026-08-17 10:35:58 +00:00 (Migrated from gitlab.com)

marked as related to #333

marked as related to #333
PlasticDigits commented 2026-08-17 10:35:59 +00:00 (Migrated from gitlab.com)

marked as related to #281

marked as related to #281
PlasticDigits commented 2026-08-17 10:36:00 +00:00 (Migrated from gitlab.com)

marked as related to #489

marked as related to #489
PlasticDigits commented 2026-08-17 10:36:01 +00:00 (Migrated from gitlab.com)

marked as related to #522

marked as related to #522
PlasticDigits commented 2026-08-17 10:36:03 +00:00 (Migrated from gitlab.com)

marked as related to #506

marked as related to #506
PlasticDigits commented 2026-08-17 12:29:47 +00:00 (Migrated from gitlab.com)

mentioned in commit 388bd558c9

mentioned in commit 388bd558c94daf95a8a6340a32390413d8cb9e76
PlasticDigits commented 2026-08-17 12:29:50 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1078

mentioned in merge request !1078
PlasticDigits commented 2026-08-17 12:32:07 +00:00 (Migrated from gitlab.com)

mentioned in commit 2f5b4ad14e

mentioned in commit 2f5b4ad14e6c6934820705781f6c7cad6d53e2d8
PlasticDigits commented 2026-08-17 13:46:07 +00:00 (Migrated from gitlab.com)

mentioned in commit 81b403d55e

mentioned in commit 81b403d55ee8e08f8249e4d109e364a3a0415c52
PlasticDigits commented 2026-08-17 13:46:19 +00:00 (Migrated from gitlab.com)

mentioned in commit 9481c758e9

mentioned in commit 9481c758e9634751c8533861f18741686ac2a4f2
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-17 13:46:20 +00:00
PlasticDigits commented 2026-08-17 13:48:58 +00:00 (Migrated from gitlab.com)

mentioned in issue #548

mentioned in issue #548
PlasticDigits commented 2026-08-17 13:49:01 +00:00 (Migrated from gitlab.com)

Merged as !1078 onto main (9481c758) after conflict resolution with !1076 (#548) and !1077 (#547).

Product conflict resolved in the merge (not as originally written in this issue):

  • X4 / volume_usd: kept #548 P522-Q catalog (UST1 / USTC / LUNC / USTR). vFDUSD/FDUSD is not used for DEX volume. Overview ustc_price_usd stays the USTC ticker. Docs/skills/migration comments were updated; original P550-10 “USTC-only volume” was not kept.
  • token_count: kept #548 C6 unique pair-leg assets (count_pair_leg_assets), not COUNT(*) FROM assets. tokens_added_30d remains the assets first-seen census.

Remaining / post-merge:

  • Deploy: run 20260818120000_global_stats_windows_and_census.sql after the #548 backfill. The #548 migration’s rollup INSERT is 24h-only (it runs first); 7d/30d/active_pairs_24h stay 0 until refresh_global_stats (aggregator ~5 min or indexer restart). Do not re-run a 24h-only INSERT after the census columns exist.
  • P6 light+dark Protocol eyeball is not a dedicated protocol-page e2e (dark shell exists in design-tokens-visual).
  • O4 Enter/Space on oracle tabs is not asserted (arrow keys are).
  • Manual QA from the issue (ticker prices not 100× swapped; Protocol 24h USD vs Charts within rollup lag; light+dark stats/oracle) is still open.

Tracking: new post-merge issue for deploy + remaining manual QA.

Merged as !1078 onto `main` (`9481c758`) after conflict resolution with !1076 (#548) and !1077 (#547). **Product conflict resolved in the merge (not as originally written in this issue):** - **X4 / `volume_usd`:** kept #548 P522-Q catalog (UST1 / USTC / LUNC / USTR). vFDUSD/FDUSD is **not** used for DEX volume. Overview `ustc_price_usd` stays the USTC ticker. Docs/skills/migration comments were updated; original P550-10 “USTC-only volume” was **not** kept. - **`token_count`:** kept #548 **C6** unique pair-leg assets (`count_pair_leg_assets`), not `COUNT(*) FROM assets`. `tokens_added_30d` remains the assets first-seen census. Remaining / post-merge: - **Deploy:** run `20260818120000_global_stats_windows_and_census.sql` after the #548 backfill. The #548 migration’s rollup `INSERT` is 24h-only (it runs first); 7d/30d/`active_pairs_24h` stay 0 until `refresh_global_stats` (aggregator ~5 min or indexer restart). Do not re-run a 24h-only `INSERT` after the census columns exist. - **P6** light+dark Protocol eyeball is not a dedicated `protocol-page` e2e (dark shell exists in `design-tokens-visual`). - **O4** Enter/Space on oracle tabs is not asserted (arrow keys are). - **Manual QA** from the issue (ticker prices not 100× swapped; Protocol 24h USD vs Charts within rollup lag; light+dark stats/oracle) is still open. Tracking: new post-merge issue for deploy + remaining manual QA.
PlasticDigits commented 2026-08-17 13:49:47 +00:00 (Migrated from gitlab.com)

mentioned in issue #552

mentioned in issue #552
PlasticDigits commented 2026-08-17 13:49:48 +00:00 (Migrated from gitlab.com)

marked as related to #552

marked as related to #552
PlasticDigits commented 2026-08-18 00:28:53 +00:00 (Migrated from gitlab.com)

mentioned in issue #556

mentioned in issue #556
PlasticDigits commented 2026-08-19 01:02:31 +00:00 (Migrated from gitlab.com)

mentioned in issue #569

mentioned in issue #569
PlasticDigits commented 2026-08-19 01:03:03 +00:00 (Migrated from gitlab.com)

marked as related to #570

marked as related to #570
PlasticDigits commented 2026-08-19 01:03:03 +00:00 (Migrated from gitlab.com)

mentioned in issue #570

mentioned in issue #570
PlasticDigits commented 2026-08-19 01:04:57 +00:00 (Migrated from gitlab.com)

mentioned in issue #571

mentioned in issue #571
PlasticDigits commented 2026-08-19 01:04:58 +00:00 (Migrated from gitlab.com)

marked as related to #571

marked as related to #571
PlasticDigits commented 2026-08-19 01:05:02 +00:00 (Migrated from gitlab.com)

mentioned in issue #572

mentioned in issue #572
PlasticDigits commented 2026-08-19 01:05:02 +00:00 (Migrated from gitlab.com)

marked as related to #572

marked as related to #572
PlasticDigits commented 2026-08-19 11:49:49 +00:00 (Migrated from gitlab.com)

mentioned in issue #574

mentioned in issue #574
PlasticDigits commented 2026-08-19 11:49:53 +00:00 (Migrated from gitlab.com)

marked as related to #574

marked as related to #574
PlasticDigits commented 2026-08-19 11:49:57 +00:00 (Migrated from gitlab.com)

mentioned in issue #575

mentioned in issue #575
PlasticDigits commented 2026-08-19 11:50:00 +00:00 (Migrated from gitlab.com)

marked as related to #575

marked as related to #575
PlasticDigits commented 2026-08-19 11:52:57 +00:00 (Migrated from gitlab.com)

mentioned in issue #576

mentioned in issue #576
PlasticDigits commented 2026-08-19 12:11:17 +00:00 (Migrated from gitlab.com)

mentioned in issue #579

mentioned in issue #579
PlasticDigits commented 2026-08-19 12:11:18 +00:00 (Migrated from gitlab.com)

marked as related to #579

marked as related to #579
PlasticDigits commented 2026-08-19 12:11:28 +00:00 (Migrated from gitlab.com)

mentioned in issue #580

mentioned in issue #580
PlasticDigits commented 2026-08-19 12:11:31 +00:00 (Migrated from gitlab.com)

marked as related to #580

marked as related to #580
PlasticDigits commented 2026-08-20 02:19:42 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1103

mentioned in merge request !1103
PlasticDigits commented 2026-08-20 02:21:34 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1104

mentioned in merge request !1104
PlasticDigits commented 2026-08-21 00:21:02 +00:00 (Migrated from gitlab.com)

mentioned in issue #586

mentioned in issue #586
PlasticDigits commented 2026-08-24 00:30:13 +00:00 (Migrated from gitlab.com)

mentioned in issue #613

mentioned in issue #613
PlasticDigits commented 2026-08-24 00:30:20 +00:00 (Migrated from gitlab.com)

mentioned in issue #614

mentioned in issue #614
PlasticDigits commented 2026-08-25 01:55:34 +00:00 (Migrated from gitlab.com)

mentioned in issue #631

mentioned in issue #631
PlasticDigits commented 2026-08-25 01:55:39 +00:00 (Migrated from gitlab.com)

marked as related to #631

marked as related to #631
PlasticDigits commented 2026-08-26 01:10:54 +00:00 (Migrated from gitlab.com)

mentioned in issue #652

mentioned in issue #652
PlasticDigits commented 2026-08-26 01:11:06 +00:00 (Migrated from gitlab.com)

mentioned in issue #653

mentioned in issue #653
PlasticDigits commented 2026-08-26 03:06:34 +00:00 (Migrated from gitlab.com)

mentioned in issue #655

mentioned in issue #655
PlasticDigits commented 2026-08-26 04:17:22 +00:00 (Migrated from gitlab.com)

mentioned in issue #666

mentioned in issue #666
PlasticDigits commented 2026-08-26 04:18:46 +00:00 (Migrated from gitlab.com)

mentioned in issue #667

mentioned in issue #667
PlasticDigits commented 2026-08-26 04:18:50 +00:00 (Migrated from gitlab.com)

marked as related to #667

marked as related to #667
PlasticDigits commented 2026-08-26 04:19:22 +00:00 (Migrated from gitlab.com)

mentioned in issue #668

mentioned in issue #668
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-dex-terraclassic#550
No description provided.