feat: /protocol global stats — total USD pair liquidity + 24h/30d % change #569

Closed
opened 2026-08-19 01:02:30 +00:00 by PlasticDigits · 26 comments
PlasticDigits commented 2026-08-19 01:02:30 +00:00 (Migrated from gitlab.com)

Summary

Add total USD liquidity in factory pairs (pool TVL) to /protocol Global stats, plus % change vs 24 hours ago and % change vs 30 days ago. Bundle into one issue. Do not split “current TVL” vs “24h Δ%” vs “30d Δ%” vs “snapshot table” vs “overview JSON”.

Related: #550 (Protocol USD census + overview rollup), #556 (DEX hub USD + humanized pair TVL for ranking), #548 (catalog volume_usd), #522 (P522-Q quote USD), #515 (CEX USTC/LUNC; not vFDUSD for DEX conversion), #281 / #333 (/overview 60s cache + O(1) rollup), #489 (short retail copy).

Current codebase

/protocol is the DEX USD census page after #550 / #556. Global stats show volume (flow) and listing census. There is no protocol-wide pool liquidity (stock / TVL) headline, and no historical liquidity series to compute 24h / 30d % change.

Layer Behavior today
Global stats UI ProtocolGlobalStats.tsx: 24h / 7d / 30d USD volume, tokens, new tokens 30d, new pairs 30d, active pairs, 24h trades. formatProtocolUsd / formatProtocolCount → em-dash on missing / non-finite. Copy: “USD volume uses the USTC reference feed.” No liquidity / TVL / Δ% boxes.
Page order (P550-1) ProtocolPage.tsx: title → Global stats → DEX hub prices → one CEX oracle card → audit contracts → hooks.
Overview API GET /api/v1/overview (indexer/src/api/overview.rs): 60s whole-response cache. Cache miss reads global_stats_24h (volume windows, active_pairs_24h, unique_traders_24h) + cheap COUNT(*) census + O(1) hub_prices. No total_liquidity_usd, no liquidity Δ%.
Volume vs liquidity Volume is SUM(volume_usd) over swaps (flow), P522-Q catalog, not vFDUSD (P550-4, X4). P550-8 already states active_pairs_24h is not TVL. unique_traders_24h is on the JSON for DoS safety (AC7) but is not a Protocol headline (dust-swap gaming; #489).
Pair reserves pair_reserves is a current-state upsert (one row per pair; reserve_0 / reserve_1 raw NUMERIC(38,0)). Written by book_snapshot.rs (~10s). No history. Swaps and LP add/remove both move reserves, so liquidity_events alone cannot reconstruct TVL.
Hub TVL (local only) hub_usd.rs already computes humanized USD TVL = human(reserve_0)×usd_0 + human(reserve_1)×usd_1 to rank cUSTC/UST1/USTR source pools (H1–H8). Dust floor, stale/reserve_*=0, symbol-spoof natives skipped. That TVL is stored per hub ticker on hub_prices.tvl_usd, not summed across all factory pairs.
Asset USD catalog pair_price_usd.rs P522-Q: uusd/USTC/cUSTC → USTC oracle; uluna/LUNC/cLUNC → LUNC; UST1/USTR → hub_prices (not $1 / 2.5×). Unknown / spoof native → None. humanize_raw_amount + fits_numeric_38_18.
CG mislabel indexer/src/api/cg.rs liquidity_in_usd is currently 24h volume_usd, not pool TVL. #550 already forbade inventing Protocol TVL from that field.
Rollup refresh volume_aggregator.rs ~5 min + startup. refresh_global_stats must not be a 24h-only INSERT that leaves extra columns stale (runbook).
Tests / verify ProtocolPage.test.tsx, formatProtocolStats.test.ts, e2e/protocol-page.spec.ts, indexer/tests/api_overview.rs, indexer/tests/indexer_overview_global_stats.rs, make verify-issue-550.
Docs / skills skills/AGENTS_FRONTEND_PROTOCOL_STATS.md (P550-1–P550-12), docs/indexer-invariants.md row Protocol global stats (#550), docs/frontend.md § Protocol, docs/runbooks/overview-global-stats-brin.md.

Why this is needed

  1. Volume ≠ liquidity. 24h/7d/30d USD volume answers “how much traded.” Operators and retail who open /protocol also ask how much is locked in pools and whether that stock is growing or shrinking.
  2. % change needs a clock. Current pair_reserves cannot answer “vs yesterday / vs last month.” Without snapshots, the UI would either lie (compute Δ from nothing) or omit the only numbers that make TVL useful.
  3. Protocol is the USD census page (#550). Charts already has a thin 24h volume strip. Liquidity belongs next to Protocol volume, not as a second oracle essay or a CoinGecko-shaped field that is actually volume.
  4. Hub TVL is not protocol TVL. #556 ranks one pool per hub ticker. A GEM/cUSTC pool can hold more USD than the UST1 hub pair and still be invisible on Protocol today.

Constraints / guardrails

  1. Keep P550 / H1–H10 / X4. Cache-miss /overview stays rollup + cheap COUNT(*) + O(1) hub read (P550-5). Do not SUM live pair TVL, scan swap_events, or walk 30d snapshots on the GET path. 60s whole-response cache stays (V5).
  2. Do not invent TVL from CG liquidity_in_usd. That field is mislabeled 24h volume. Do not use total_volume_* as liquidity. Do not use LP token supply as TVL.
  3. AMM pool reserves only. Protocol “liquidity in pairs” = humanized USD of pair_reserves (constant-product legs). Do not add resting limit-order escrow, parked dust, or wallet balances. Document that book depth is not in this number.
  4. Same USD catalog as volume (X4 / P522-Q / #556). USTC/cUSTC/uusd → USTC oracle; LUNC/cLUNC/uluna → LUNC; UST1/USTR → hub_prices. Never convert with vFDUSD/FDUSD. Never $1 UST1 or 2.5× USTR. Oracle down → that handle is unpriced (omit / do not peg).
  5. Identity is contract/denom (A1 / H8). Symbol-spoof natives (symbol=USTR on ugem) must not price. Hub CW20s need a contract address.
  6. Humanize decimals. TVL is human(reserve)×usd, never raw 18-dec vs 6-dec integers. Reuse humanize_raw_amount + fits_numeric_38_18. Overflow / non-positive → skip that pair (do not wrap or Inf).
  7. Skip unusable rows. Stale snapshot_at (same TTL as hub / book mirror), reserve_*=0, missing reserves, same-asset legs, unlisted pairs: omit from the sum. Omitted ≠ $0 (zeros would fake a drain).
  8. One-sided catalog (CPAMM). If both legs have catalog/hub USD → h0×usd0 + h1×usd1. If exactly one leg is catalogued → 2 × that leg’s USD (constant-product). If neither → omit. Do not invent gem/gem USD.
  9. Double-count across pools is correct. The same USTC in two pairs is locked twice. Do not “unique-token TVL.”
  10. % change math. pct = (now − then) / then × 100 when then > 0. If no snapshot in the window, then = 0, or overflow → JSON null, UI em-dash. Never Infinity / NaN / "∞%". Cold start, --fresh rebuild, and indexer younger than 24h/30d → null Δ% (copy must not say “0% change”).
  11. Snapshots, not event replay. Persist periodic global (and optionally per-pair) USD totals. Do not reconstruct 30d TVL from liquidity_events (swaps change reserves; prices move).
  12. Additive overview JSON. New keys optional on IndexerOverview until indexer ships. Existing volume/census/hub fields unchanged. Charts must keep working.
  13. Null vs "0". Idle DEX with no priced reserves → total_liquidity_usd = "0". Unpriced-only activity does not force volume-style null unless no pair could be priced and priced-pair count is 0 — prefer "0" plus optional priced_pair_count rather than a third meaning of null. Δ% stays null until a baseline exists.
  14. Copy / #489. Short labels: Total liquidity, 24h liquidity, 30d liquidity (or equivalent). One clause that TVL moves with LP, swaps, and reference prices — not a TWAP vs CEX essay. Do not headline unique LPs or unique traders.
  15. Page chrome. New boxes live inside protocol-global-stats. Do not add a second stats card. Do not clone factory/router AddressRow (#378). Light + dark; mobile grid wrap.
  16. Advisory (X5). Not settlement, not TWAP, not /ust1 window. Same “reference” voice as volume.
  17. Refresh coupling. Compute current TVL after hub USD is current (UST1/USTR). A 24h-only refresh_global_stats that omits liquidity columns must not ship (same footgun as #550 7d/30d columns).
  18. Do not “fix” CG liquidity_in_usd in this issue unless it is a one-line comment pointing at the new field — aggregators may already consume the mislabel. Protocol must not depend on it.

Relevant files

Path Role
indexer/src/api/overview.rs Additive JSON + 60s cache
indexer/src/db/queries/volume.rs refresh_global_stats / get_global_stats
indexer/src/indexer/volume_aggregator.rs ~5 min refresh loop
indexer/src/indexer/hub_usd.rs pair_tvl / usd_from_reserves / staleness / dust
indexer/src/indexer/pair_price_usd.rs P522-Q catalog, humanize, overflow, A1 spoof
indexer/src/db/queries/hub_prices.rs list_reserve_pairs, hub marks
indexer/src/db/queries/pair_reserves.rs Current reserves mirror
indexer/migrations/ New snapshot + global_stats_24h columns (pattern: 20260818120000_global_stats_windows_and_census.sql)
indexer/tests/api_overview.rs Overview shape
indexer/tests/indexer_overview_global_stats.rs Rollup vs live; EXPLAIN no request-path scan
frontend-dapp/src/types/index.ts IndexerOverview Additive fields
frontend-dapp/src/components/protocol/ProtocolGlobalStats.tsx New StatBoxes
frontend-dapp/src/utils/formatProtocolStats.ts USD + new % formatter
frontend-dapp/src/pages/ProtocolPage.test.tsx RTL
frontend-dapp/e2e/protocol-page.spec.ts Playwright smoke
docs/runbooks/overview-global-stats-brin.md Snapshot + rollup
docs/indexer-invariants.md New row + DoS list
skills/AGENTS_FRONTEND_PROTOCOL_STATS.md Invariants
docs/frontend.md Protocol section
scripts/qa/verify-issue-550.sh Pattern for verify-issue-<iid>
AGENTS.md Crosslink + make verify-issue-<iid>

Indexer — current TVL

Reuse hub math (pair_tvl / usd_from_reserves) over all factory pairs with a usable pair_reserves row. Price each leg via P522-Q + hub_prices. Apply one-sided 2× when only one leg is catalogued. Sum priced pairs into total_liquidity_usd (NUMERIC(38,18), clamp with the same LEAST/overflow pattern as volume USD). Optionally persist priced_pair_count / unpriced_pair_count on the rollup for operators (JSON ok; not Protocol headlines).

Refresh in the aggregator (~5 min) and once at startup, after hub USD refresh so UST1/USTR marks exist. Do not LCD-query pools on /overview.

Indexer — history for Δ%

New table (names may vary):

global_liquidity_snapshots (
  sampled_at      TIMESTAMPTZ PRIMARY KEY,
  total_liquidity_usd NUMERIC(38, 18) NOT NULL,
  priced_pair_count   INT NOT NULL
)

Insert on each successful TVL refresh. Retain ≥ 35 days; prune older in the same loop. Lookup then as the snapshot nearest to now() - 24h / now() - 30d within a documented tolerance (e.g. ±30 min). If none → Δ% null.

Materialize on global_stats_24h (additive columns) so GET stays O(1):

Field Meaning
total_liquidity_usd Current priced pool TVL (string on JSON)
liquidity_change_24h_pct Signed percent vs ~24h snapshot; JSON null if no baseline
liquidity_change_30d_pct Signed percent vs ~30d snapshot; JSON null if no baseline

Optional (useful for tests / UI tooltips, not required as headlines): total_liquidity_usd_24h_ago, total_liquidity_usd_30d_ago. Prefer indexer-computed % so clients do not divide by zero.

Per-pair snapshot history is out of scope unless needed to debug; Protocol only needs the global series.

Frontend — /protocol

  1. Add three StatBoxes inside protocol-global-stats (suggested testids): protocol-stat-liquidity, protocol-stat-liquidity-24h, protocol-stat-liquidity-30d.
  2. formatProtocolUsd for total. New formatProtocolPct: missing / non-finite → em-dash; finite → signed compact percent (no Infinity). XSS: text nodes only (existing StatBox).
  3. Partial/old indexer: missing keys → em-dash, no crash.
  4. Keep volume boxes; do not replace them. Do not show unique_traders_24h. Do not show mixed-unit reserves.
  5. One-line helper copy: liquidity is pool TVL in USD (same catalog as volume); 24h/30d are vs indexer snapshots, not on-chain genesis.

Docs / verify

Extend Protocol skill + invariants + overview runbook + docs/frontend.md + AGENTS.md. Add scripts/qa/verify-issue-<iid>.sh and make verify-issue-<iid> (Postgres-only indexer tests + frontend unit/RTL; Playwright e2e-smoke @ 5 workers when frontend-dapp/node_modules exists).

Acceptance criteria

  • AC1 — Total liquidity. /protocol Global stats shows total USD liquidity in factory pairs ($ + compact human from total_liquidity_usd). Not volume, not CG liquidity_in_usd, not raw reserves.
  • AC2 — 24h Δ%. Same card shows % change vs ~24h ago. Null baseline → em-dash, not 0% / Infinity.
  • AC3 — 30d Δ%. Same card shows % change vs ~30d ago, same null rules.
  • AC4 — Additive API. GET /api/v1/overview gains the new fields without breaking existing keys. Charts still works.
  • AC5 — Cheap reads. Cache-miss does not scan swap_events or walk 30d snapshots or recompute all pair TVL. EXPLAIN / tests prove GET reads the rollup row (and cheap census). 60s cache unchanged.
  • AC6 — Catalog / hub USD. TVL uses P522-Q + hub marks. vFDUSD unused. No $1 UST1. Spoof natives omitted. Unpriced pairs omitted (not $0).
  • AC7 — Humanized TVL. 6-dec vs 18-dec legs do not let raw integers dominate. Overflow skipped.
  • AC8 — Snapshot lag documented. Rebuild / young indexer → Δ% empty until windows fill. Copy does not claim on-chain 30d genesis TVL.
  • AC9 — Chrome. Boxes inside existing global-stats card; page order and audit/hooks unchanged (#550 / #378 / #556).
  • AC10 — Verify. make verify-issue-<iid> + indexer overview/TVL tests + RTL + Playwright (5 workers).

Test plan (all paths)

Global stats UI

# Path Assert
S1 Overview success with liquidity + both Δ% Total $…; 24h/30d signed %; volume boxes still present
S2 Loading Skeletons; no undefined / NaN / Infinity
S3 Overview 502 Outage banner or RetryError; no host:port
S4 Missing new fields (old indexer) Em-dash on liquidity / Δ%; no crash
S5 total_liquidity_usd = "0", Δ% null $0 (or $0.00); Δ% em-dash
S6 Negative 24h, positive 30d Signs render; not absolute-value-only
S7 Order Still inside protocol-global-stats, above hub + oracle
S8 Raw volume total_volume_24h still not a headline; liquidity ≠ volume string
S9 Light + dark + 390px wrap Grid wraps; no overflow clip of %

Indexer TVL

# Path Assert
T1 Two catalogued legs TVL = h0×usd0 + h1×usd1 (fixture: 6-dec + 18-dec)
T2 One catalogued leg TVL = 2× priced leg
T3 Neither catalogued Pair omitted; sum excludes it
T4 UST1/cUSTC Uses hub UST1 + USTC oracle, not $1
T5 USTR vs UST1 Uses hub marks, not 2.5×
T6 Zero / missing / stale reserves Omitted
T7 Same-asset pair Omitted
T8 Empty DB "0" total; Δ% null
T9 Overflow / huge 18-dec Pair skipped; GET 200; no panic
T10 Hub oracle down Unpriced hub legs omit those pairs; no fake peg

Indexer snapshots / Δ%

# Path Assert
H1 Seed snapshot 24h ago 24h % matches (now-then)/then*100
H2 Seed snapshot 30d ago 30d % matches; 24h independent
H3 No snapshots Both Δ% JSON null
H4 then = 0, now > 0 Δ% null (not Inf)
H5 Window cutoff Snapshot 25h old is not the 24h baseline if a closer 24h row exists
H6 Prune Rows older than retention dropped; 30d lookup still works inside 35d
H7 Partial refresh_global_stats Must update liquidity columns (regression vs 24h-only INSERT)

Indexer overview / DoS budget

# Path Assert
V1 Existing overview fields unchanged api_overview.rs
V2 New keys present strings / nulls as specified
V3 EXPLAIN cache-miss No swap_events 30d aggregate; no sequential scan of snapshot history on GET
V4 60s cache Burst does not recompute TVL
V5 OVERVIEW_GLOBAL_STATS_LIVE=1 Must not live-scan 30d snapshots either (or document that live mode is volume-only and still O(1) for liquidity)

Unit / RTL

  1. formatProtocolPct: null / NaN / Infinity / "" → em-dash; "12.5" / "-3" render as percent.
  2. ProtocolPage.test.tsx: new testids; factory/router still present; hub card still after stats.
  3. Charts overview mock still type-checks with extra optional fields.

Playwright (5 workers, e2e-smoke)

# Path Assert
P1 /protocol protocol-stat-liquidity + 24h + 30d testids visible with volume stats
P2 Phone 390×844 Liquidity boxes visible; no overlap with hub card
P3 Existing P1–P5 Oracle tabs + audit rows still pass

Test plan (attack, hack, abuse)

# Vector Assert
A1 DoS — live TVL on GET /overview cache miss does not join all pair_reserves×oracles or scan snapshot history. Burst within 60s hits cache.
A2 DoS — huge snapshot table Prune job bounds rows; GET never ORDER BY sampled_at over unbounded history.
A3 Flash LP Add huge LP then withdraw within one snapshot interval may move current TVL; Δ% uses snapshots, not mempool. Document lag. Do not treat as settlement. Optional: no extra “anti-flash” filter in v1 (would hide real LP).
A4 Dust / 18-dec raw takeover Unhumanized raw reserves must not dominate a 6-dec USTC pool (same class as H8).
A5 Symbol spoof Native symbol=USTR / UST1 with non-hub denom does not price (A1).
A6 Unpriced gem as $0 Omitted pairs must not pull protocol TVL to zero or fabricate −100% Δ.
A7 vFDUSD conversion FDUSD/vFDUSD feed never enters TVL or Δ%.
A8 Fake $1 UST1 / 2.5× USTR Hub missing → omit, do not peg.
A9 XSS / HTML in JSON total_liquidity_usd / pct strings with <script> / javascript: render as text; formatProtocolUsd / formatProtocolPct em-dash on non-finite.
A10 Infinity % then=0 or overflow → JSON null + UI em-dash. Client must not now/then itself without a zero guard.
A11 Oracle crash as “TVL rug” Price drop moves TVL (honest). Copy mentions reference prices. Do not freeze TVL at last “good” oracle without documenting it (v1: current oracles only).
A12 Stale drained pool Reserves past max-staleness omitted so a dead snapshot cannot keep TVL inflated.
A13 NUMERIC / JSON bomb Values that fail fits_numeric_38_18 skipped; response stays small; HTTP 200.
A14 Rebuild fake history After --fresh, do not backfill 30d Δ% from zeros or from liquidity_events. Δ% null until real snapshots accrue.
A15 CG field confusion Protocol UI never reads liquidity_in_usd. Integrator tests may still see the old CG mislabel.
A16 Book escrow inflation Resting limits / parked orders not in pool TVL.
A17 Same-asset / unlisted pair Skipped (hub already does this).
A18 Path / ticker injection No new unallowlisted ticker path. Overview stays a single GET. ?ticker= oracle allowlist unchanged.
A19 Cache poison Mutating DB after a GET does not change the cached body until TTL; tests may document 60s lag (not a bug).
A20 Partial column UPDATE A refresh that writes 24h volume but skips liquidity must fail tests (stale TVL / zero Δ% after real snapshots).

Verification criteria

Ship is done when all of the following hold:

  1. make verify-issue-<iid> is green on a Cloud Agent / local machine with make setup-indexer-postgres (no wasm deploy required for indexer + RTL). Playwright e2e-smoke is optional-but-run when frontend-dapp/node_modules exists, 5 workers.
  2. Indexer tests cover T1–T10, H1–H7, V1–V4, and A1/A4/A5/A7/A8/A10/A14 (lib and/or api_overview / new *_liquidity_* integration file, --test-threads=1).
  3. Frontend: formatProtocolStats + ProtocolPage RTL for S1–S8; Charts types still compile with additive overview fields.
  4. Docs: invariants row, Protocol skill (new P-ids or extend P550), overview runbook (snapshots + “not TVL from CG”), docs/frontend.md, AGENTS.md crosslink.
  5. Manual / QA: /protocol shows total liquidity + 24h% + 30d%; young indexer shows em-dash for Δ%; volume / hub / oracle / audit unchanged.
  6. make verify-issue-550 and make verify-issue-556 stay green (no regression of census, hub card, or cheap /overview).
## Summary Add **total USD liquidity in factory pairs** (pool TVL) to `/protocol` **Global stats**, plus **% change vs 24 hours ago** and **% change vs 30 days ago**. Bundle into **one** issue. Do **not** split “current TVL” vs “24h Δ%” vs “30d Δ%” vs “snapshot table” vs “overview JSON”. Related: [#550](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/550) (Protocol USD census + overview rollup), [#556](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/556) (DEX hub USD + humanized pair TVL for ranking), [#548](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/548) (catalog `volume_usd`), [#522](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/522) (P522-Q quote USD), [#515](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/515) (CEX USTC/LUNC; **not** vFDUSD for DEX conversion), [#281](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/281) / [#333](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/333) (`/overview` 60s cache + O(1) rollup), [#489](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/489) (short retail copy). ## Current codebase `/protocol` is the DEX **USD census** page after #550 / #556. Global stats show **volume** (flow) and listing census. There is **no** protocol-wide pool **liquidity** (stock / TVL) headline, and **no** historical liquidity series to compute 24h / 30d % change. | Layer | Behavior today | |-------|----------------| | **Global stats UI** | [`ProtocolGlobalStats.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/components/protocol/ProtocolGlobalStats.tsx): 24h / 7d / 30d **USD volume**, tokens, new tokens 30d, new pairs 30d, active pairs, 24h trades. `formatProtocolUsd` / `formatProtocolCount` → em-dash on missing / non-finite. Copy: “USD volume uses the USTC reference feed.” **No** liquidity / TVL / Δ% boxes. | | **Page order (P550-1)** | [`ProtocolPage.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/pages/ProtocolPage.tsx): title → **Global stats** → **DEX hub prices** → **one** CEX oracle card → audit contracts → hooks. | | **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)): 60s whole-response cache. Cache miss reads **`global_stats_24h`** (volume windows, `active_pairs_24h`, `unique_traders_24h`) + cheap `COUNT(*)` census + O(1) `hub_prices`. **No** `total_liquidity_usd`, **no** liquidity Δ%. | | **Volume vs liquidity** | Volume is `SUM(volume_usd)` over **swaps** (flow), P522-Q catalog, **not** vFDUSD (**P550-4**, **X4**). **P550-8** already states `active_pairs_24h` is **not** TVL. `unique_traders_24h` is on the JSON for DoS safety (**AC7**) but is **not** a Protocol headline (dust-swap gaming; #489). | | **Pair reserves** | [`pair_reserves`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/migrations/20260605010000_pair_reserves.sql) is a **current-state upsert** (one row per pair; `reserve_0` / `reserve_1` raw `NUMERIC(38,0)`). Written by [`book_snapshot.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/indexer/book_snapshot.rs) (~10s). **No history.** Swaps **and** LP add/remove both move reserves, so `liquidity_events` alone cannot reconstruct TVL. | | **Hub TVL (local only)** | [`hub_usd.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/indexer/hub_usd.rs) already computes **humanized USD TVL** = `human(reserve_0)×usd_0 + human(reserve_1)×usd_1` to **rank** cUSTC/UST1/USTR source pools (**H1–H8**). Dust floor, stale/`reserve_*=0`, symbol-spoof natives skipped. That TVL is stored per hub ticker on `hub_prices.tvl_usd`, **not** summed across all factory pairs. | | **Asset USD catalog** | [`pair_price_usd.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/indexer/pair_price_usd.rs) **P522-Q**: `uusd`/USTC/cUSTC → USTC oracle; `uluna`/LUNC/cLUNC → LUNC; UST1/USTR → `hub_prices` (**not** `$1` / `2.5×`). Unknown / spoof native → `None`. `humanize_raw_amount` + `fits_numeric_38_18`. | | **CG mislabel** | [`indexer/src/api/cg.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/api/cg.rs) `liquidity_in_usd` is currently **24h `volume_usd`**, not pool TVL. #550 already forbade inventing Protocol TVL from that field. | | **Rollup refresh** | [`volume_aggregator.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/indexer/volume_aggregator.rs) ~5 min + startup. `refresh_global_stats` **must not** be a 24h-only `INSERT` that leaves extra columns stale ([runbook](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/runbooks/overview-global-stats-brin.md)). | | **Tests / verify** | [`ProtocolPage.test.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/pages/ProtocolPage.test.tsx), [`formatProtocolStats.test.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/utils/__tests__/formatProtocolStats.test.ts), [`e2e/protocol-page.spec.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/e2e/protocol-page.spec.ts), [`indexer/tests/api_overview.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/tests/api_overview.rs), [`indexer/tests/indexer_overview_global_stats.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/tests/indexer_overview_global_stats.rs), `make verify-issue-550`. | | **Docs / skills** | [`skills/AGENTS_FRONTEND_PROTOCOL_STATS.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/skills/AGENTS_FRONTEND_PROTOCOL_STATS.md) (**P550-1–P550-12**), [`docs/indexer-invariants.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/indexer-invariants.md) row **Protocol global stats (#550)**, [`docs/frontend.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/frontend.md) § Protocol, [`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. **Volume ≠ liquidity.** 24h/7d/30d USD volume answers “how much traded.” Operators and retail who open `/protocol` also ask **how much is locked in pools** and whether that stock is growing or shrinking. 2. **% change needs a clock.** Current `pair_reserves` cannot answer “vs yesterday / vs last month.” Without snapshots, the UI would either lie (compute Δ from nothing) or omit the only numbers that make TVL useful. 3. **Protocol is the USD census page** (#550). Charts already has a thin 24h volume strip. Liquidity belongs next to Protocol volume, not as a second oracle essay or a CoinGecko-shaped field that is actually volume. 4. **Hub TVL is not protocol TVL.** #556 ranks **one** pool per hub ticker. A GEM/cUSTC pool can hold more USD than the UST1 hub pair and still be invisible on Protocol today. ## Constraints / guardrails 1. **Keep P550 / H1–H10 / X4.** Cache-miss `/overview` stays rollup + cheap `COUNT(*)` + O(1) hub read (**P550-5**). Do **not** `SUM` live pair TVL, scan `swap_events`, or walk 30d snapshots **on the GET path**. 60s whole-response cache stays (**V5**). 2. **Do not invent TVL from CG `liquidity_in_usd`.** That field is mislabeled 24h volume. Do not use `total_volume_*` as liquidity. Do not use LP token supply as TVL. 3. **AMM pool reserves only.** Protocol “liquidity in pairs” = humanized USD of **`pair_reserves`** (constant-product legs). **Do not** add resting limit-order escrow, parked dust, or wallet balances. Document that book depth is not in this number. 4. **Same USD catalog as volume (X4 / P522-Q / #556).** USTC/cUSTC/`uusd` → USTC oracle; LUNC/cLUNC/`uluna` → LUNC; UST1/USTR → `hub_prices`. **Never** convert with vFDUSD/FDUSD. **Never** `$1` UST1 or `2.5×` USTR. Oracle down → that handle is unpriced (omit / do not peg). 5. **Identity is contract/denom (A1 / H8).** Symbol-spoof natives (`symbol=USTR` on `ugem`) must not price. Hub CW20s need a contract address. 6. **Humanize decimals.** TVL is `human(reserve)×usd`, never raw 18-dec vs 6-dec integers. Reuse `humanize_raw_amount` + `fits_numeric_38_18`. Overflow / non-positive → skip that pair (do not wrap or Inf). 7. **Skip unusable rows.** Stale `snapshot_at` (same TTL as hub / book mirror), `reserve_*=0`, missing reserves, same-asset legs, unlisted pairs: **omit** from the sum. Omitted ≠ `$0` (zeros would fake a drain). 8. **One-sided catalog (CPAMM).** If **both** legs have catalog/hub USD → `h0×usd0 + h1×usd1`. If **exactly one** leg is catalogued → `2 ×` that leg’s USD (constant-product). If **neither** → omit. Do not invent gem/gem USD. 9. **Double-count across pools is correct.** The same USTC in two pairs is locked twice. Do not “unique-token TVL.” 10. **% change math.** `pct = (now − then) / then × 100` when `then > 0`. If no snapshot in the window, `then = 0`, or overflow → JSON **`null`**, UI em-dash. **Never** `Infinity` / `NaN` / `"∞%"`. Cold start, `--fresh` rebuild, and indexer younger than 24h/30d → null Δ% (copy must not say “0% change”). 11. **Snapshots, not event replay.** Persist periodic **global** (and optionally per-pair) USD totals. Do not reconstruct 30d TVL from `liquidity_events` (swaps change reserves; prices move). 12. **Additive overview JSON.** New keys optional on `IndexerOverview` until indexer ships. Existing volume/census/hub fields unchanged. Charts must keep working. 13. **Null vs `"0"`.** Idle DEX with no priced reserves → `total_liquidity_usd = "0"`. Unpriced-only activity does **not** force volume-style `null` unless **no** pair could be priced **and** priced-pair count is 0 — prefer `"0"` plus optional `priced_pair_count` rather than a third meaning of `null`. Δ% stays `null` until a baseline exists. 14. **Copy / #489.** Short labels: **Total liquidity**, **24h liquidity**, **30d liquidity** (or equivalent). One clause that TVL moves with LP, swaps, **and** reference prices — not a TWAP vs CEX essay. Do not headline unique LPs or unique traders. 15. **Page chrome.** New boxes live **inside** `protocol-global-stats`. Do not add a second stats card. Do not clone factory/router `AddressRow` (#378). Light + dark; mobile grid wrap. 16. **Advisory (X5).** Not settlement, not TWAP, not `/ust1` window. Same “reference” voice as volume. 17. **Refresh coupling.** Compute current TVL **after** hub USD is current (UST1/USTR). A 24h-only `refresh_global_stats` that omits liquidity columns must not ship (same footgun as #550 7d/30d columns). 18. **Do not “fix” CG `liquidity_in_usd` in this issue** unless it is a one-line comment pointing at the new field — aggregators may already consume the mislabel. Protocol must not depend on it. ## Relevant files | Path | Role | |------|------| | [`indexer/src/api/overview.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/api/overview.rs) | Additive JSON + 60s cache | | [`indexer/src/db/queries/volume.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/db/queries/volume.rs) | `refresh_global_stats` / `get_global_stats` | | [`indexer/src/indexer/volume_aggregator.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/indexer/volume_aggregator.rs) | ~5 min refresh loop | | [`indexer/src/indexer/hub_usd.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/indexer/hub_usd.rs) | `pair_tvl` / `usd_from_reserves` / staleness / dust | | [`indexer/src/indexer/pair_price_usd.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/indexer/pair_price_usd.rs) | P522-Q catalog, humanize, overflow, A1 spoof | | [`indexer/src/db/queries/hub_prices.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/db/queries/hub_prices.rs) | `list_reserve_pairs`, hub marks | | [`indexer/src/db/queries/pair_reserves.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/db/queries/pair_reserves.rs) | Current reserves mirror | | [`indexer/migrations/`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/tree/main/indexer/migrations) | New snapshot + `global_stats_24h` columns (pattern: `20260818120000_global_stats_windows_and_census.sql`) | | [`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 request-path scan | | [`frontend-dapp/src/types/index.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/types/index.ts) `IndexerOverview` | Additive fields | | [`frontend-dapp/src/components/protocol/ProtocolGlobalStats.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/components/protocol/ProtocolGlobalStats.tsx) | New StatBoxes | | [`frontend-dapp/src/utils/formatProtocolStats.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/utils/formatProtocolStats.ts) | USD + new % formatter | | [`frontend-dapp/src/pages/ProtocolPage.test.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/pages/ProtocolPage.test.tsx) | RTL | | [`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 | | [`docs/runbooks/overview-global-stats-brin.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/runbooks/overview-global-stats-brin.md) | Snapshot + rollup | | [`docs/indexer-invariants.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/indexer-invariants.md) | New row + DoS list | | [`skills/AGENTS_FRONTEND_PROTOCOL_STATS.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/skills/AGENTS_FRONTEND_PROTOCOL_STATS.md) | Invariants | | [`docs/frontend.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/frontend.md) | Protocol section | | [`scripts/qa/verify-issue-550.sh`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/scripts/qa/verify-issue-550.sh) | Pattern for `verify-issue-<iid>` | | [`AGENTS.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/AGENTS.md) | Crosslink + `make verify-issue-<iid>` | ## Recommended direction ### Indexer — current TVL Reuse hub math (`pair_tvl` / `usd_from_reserves`) over **all** factory pairs with a usable `pair_reserves` row. Price each leg via P522-Q + `hub_prices`. Apply one-sided `2×` when only one leg is catalogued. Sum priced pairs into `total_liquidity_usd` (`NUMERIC(38,18)`, clamp with the same `LEAST`/overflow pattern as volume USD). Optionally persist `priced_pair_count` / `unpriced_pair_count` on the rollup for operators (JSON ok; **not** Protocol headlines). Refresh in the aggregator (~5 min) **and** once at startup, **after** hub USD refresh so UST1/USTR marks exist. Do not LCD-query pools on `/overview`. ### Indexer — history for Δ% New table (names may vary): ```text global_liquidity_snapshots ( sampled_at TIMESTAMPTZ PRIMARY KEY, total_liquidity_usd NUMERIC(38, 18) NOT NULL, priced_pair_count INT NOT NULL ) ``` Insert on each successful TVL refresh. Retain ≥ **35 days**; prune older in the same loop. Lookup `then` as the snapshot **nearest** to `now() - 24h` / `now() - 30d` within a documented tolerance (e.g. ±30 min). If none → Δ% null. Materialize on `global_stats_24h` (additive columns) so GET stays O(1): | Field | Meaning | |-------|---------| | `total_liquidity_usd` | Current priced pool TVL (string on JSON) | | `liquidity_change_24h_pct` | Signed percent vs ~24h snapshot; JSON `null` if no baseline | | `liquidity_change_30d_pct` | Signed percent vs ~30d snapshot; JSON `null` if no baseline | Optional (useful for tests / UI tooltips, not required as headlines): `total_liquidity_usd_24h_ago`, `total_liquidity_usd_30d_ago`. Prefer indexer-computed % so clients do not divide by zero. Per-pair snapshot history is **out of scope** unless needed to debug; Protocol only needs the global series. ### Frontend — `/protocol` 1. Add three StatBoxes inside `protocol-global-stats` (suggested testids): `protocol-stat-liquidity`, `protocol-stat-liquidity-24h`, `protocol-stat-liquidity-30d`. 2. `formatProtocolUsd` for total. New `formatProtocolPct`: missing / non-finite → em-dash; finite → signed compact percent (no `Infinity`). XSS: text nodes only (existing StatBox). 3. Partial/old indexer: missing keys → em-dash, no crash. 4. Keep volume boxes; do not replace them. Do not show `unique_traders_24h`. Do not show mixed-unit reserves. 5. One-line helper copy: liquidity is pool TVL in USD (same catalog as volume); 24h/30d are vs indexer snapshots, not on-chain genesis. ### Docs / verify Extend Protocol skill + invariants + overview runbook + `docs/frontend.md` + `AGENTS.md`. Add `scripts/qa/verify-issue-<iid>.sh` and `make verify-issue-<iid>` (Postgres-only indexer tests + frontend unit/RTL; Playwright e2e-smoke @ **5 workers** when `frontend-dapp/node_modules` exists). ## Acceptance criteria - [ ] **AC1 — Total liquidity.** `/protocol` Global stats shows **total USD liquidity** in factory pairs (`$` + compact human from `total_liquidity_usd`). Not volume, not CG `liquidity_in_usd`, not raw reserves. - [ ] **AC2 — 24h Δ%.** Same card shows **% change vs ~24h ago**. Null baseline → em-dash, not `0%` / `Infinity`. - [ ] **AC3 — 30d Δ%.** Same card shows **% change vs ~30d ago**, same null rules. - [ ] **AC4 — Additive API.** `GET /api/v1/overview` gains the new fields without breaking existing keys. Charts still works. - [ ] **AC5 — Cheap reads.** Cache-miss does **not** scan `swap_events` or walk 30d snapshots or recompute all pair TVL. EXPLAIN / tests prove GET reads the rollup row (and cheap census). 60s cache unchanged. - [ ] **AC6 — Catalog / hub USD.** TVL uses P522-Q + hub marks. vFDUSD unused. No `$1` UST1. Spoof natives omitted. Unpriced pairs omitted (not `$0`). - [ ] **AC7 — Humanized TVL.** 6-dec vs 18-dec legs do not let raw integers dominate. Overflow skipped. - [ ] **AC8 — Snapshot lag documented.** Rebuild / young indexer → Δ% empty until windows fill. Copy does not claim on-chain 30d genesis TVL. - [ ] **AC9 — Chrome.** Boxes inside existing global-stats card; page order and audit/hooks unchanged (#550 / #378 / #556). - [ ] **AC10 — Verify.** `make verify-issue-<iid>` + indexer overview/TVL tests + RTL + Playwright (5 workers). ## Test plan (all paths) ### Global stats UI | # | Path | Assert | |---|------|--------| | S1 | Overview success with liquidity + both Δ% | Total `$…`; 24h/30d signed `%`; volume boxes still present | | S2 | Loading | Skeletons; no `undefined` / `NaN` / `Infinity` | | S3 | Overview 502 | Outage banner or RetryError; no host:port | | S4 | Missing new fields (old indexer) | Em-dash on liquidity / Δ%; no crash | | S5 | `total_liquidity_usd = "0"`, Δ% null | `$0` (or `$0.00`); Δ% em-dash | | S6 | Negative 24h, positive 30d | Signs render; not absolute-value-only | | S7 | Order | Still inside `protocol-global-stats`, **above** hub + oracle | | S8 | Raw volume | `total_volume_24h` still not a headline; liquidity ≠ volume string | | S9 | Light + dark + 390px wrap | Grid wraps; no overflow clip of `%` | ### Indexer TVL | # | Path | Assert | |---|------|--------| | T1 | Two catalogued legs | TVL = h0×usd0 + h1×usd1 (fixture: 6-dec + 18-dec) | | T2 | One catalogued leg | TVL = 2× priced leg | | T3 | Neither catalogued | Pair omitted; sum excludes it | | T4 | UST1/cUSTC | Uses hub UST1 + USTC oracle, not `$1` | | T5 | USTR vs UST1 | Uses hub marks, not `2.5×` | | T6 | Zero / missing / stale reserves | Omitted | | T7 | Same-asset pair | Omitted | | T8 | Empty DB | `"0"` total; Δ% null | | T9 | Overflow / huge 18-dec | Pair skipped; GET 200; no panic | | T10 | Hub oracle down | Unpriced hub legs omit those pairs; no fake peg | ### Indexer snapshots / Δ% | # | Path | Assert | |---|------|--------| | H1 | Seed snapshot 24h ago | 24h % matches `(now-then)/then*100` | | H2 | Seed snapshot 30d ago | 30d % matches; 24h independent | | H3 | No snapshots | Both Δ% JSON `null` | | H4 | `then = 0`, now > 0 | Δ% `null` (not Inf) | | H5 | Window cutoff | Snapshot 25h old is **not** the 24h baseline if a closer 24h row exists | | H6 | Prune | Rows older than retention dropped; 30d lookup still works inside 35d | | H7 | Partial `refresh_global_stats` | Must update liquidity columns (regression vs 24h-only INSERT) | ### Indexer overview / DoS budget | # | Path | Assert | |---|------|--------| | V1 | Existing overview fields unchanged | `api_overview.rs` | | V2 | New keys present | strings / nulls as specified | | V3 | EXPLAIN cache-miss | No `swap_events` 30d aggregate; no sequential scan of snapshot history on GET | | V4 | 60s cache | Burst does not recompute TVL | | V5 | `OVERVIEW_GLOBAL_STATS_LIVE=1` | Must not live-scan 30d snapshots either (or document that live mode is volume-only and still O(1) for liquidity) | ### Unit / RTL 1. `formatProtocolPct`: null / `NaN` / `Infinity` / `""` → em-dash; `"12.5"` / `"-3"` render as percent. 2. `ProtocolPage.test.tsx`: new testids; factory/router still present; hub card still after stats. 3. Charts overview mock still type-checks with extra optional fields. ### Playwright (5 workers, `e2e-smoke`) | # | Path | Assert | |---|------|--------| | P1 | `/protocol` | `protocol-stat-liquidity` + 24h + 30d testids visible with volume stats | | P2 | Phone 390×844 | Liquidity boxes visible; no overlap with hub card | | P3 | Existing P1–P5 | Oracle tabs + audit rows still pass | ## Test plan (attack, hack, abuse) | # | Vector | Assert | |---|--------|--------| | A1 | **DoS — live TVL on GET** | `/overview` cache miss does not join all `pair_reserves`×oracles or scan snapshot history. Burst within 60s hits cache. | | A2 | **DoS — huge snapshot table** | Prune job bounds rows; GET never `ORDER BY sampled_at` over unbounded history. | | A3 | **Flash LP** | Add huge LP then withdraw within one snapshot interval may move current TVL; Δ% uses **snapshots**, not mempool. Document lag. Do not treat as settlement. Optional: no extra “anti-flash” filter in v1 (would hide real LP). | | A4 | **Dust / 18-dec raw takeover** | Unhumanized raw reserves must not dominate a 6-dec USTC pool (same class as **H8**). | | A5 | **Symbol spoof** | Native `symbol=USTR` / `UST1` with non-hub denom does not price (A1). | | A6 | **Unpriced gem as `$0`** | Omitted pairs must not pull protocol TVL to zero or fabricate −100% Δ. | | A7 | **vFDUSD conversion** | FDUSD/vFDUSD feed never enters TVL or Δ%. | | A8 | **Fake `$1` UST1 / `2.5×` USTR** | Hub missing → omit, do not peg. | | A9 | **XSS / HTML in JSON** | `total_liquidity_usd` / pct strings with `<script>` / `javascript:` render as text; `formatProtocolUsd` / `formatProtocolPct` em-dash on non-finite. | | A10 | **Infinity %** | `then=0` or overflow → JSON null + UI em-dash. Client must not `now/then` itself without a zero guard. | | A11 | **Oracle crash as “TVL rug”** | Price drop moves TVL (honest). Copy mentions reference prices. Do not freeze TVL at last “good” oracle without documenting it (v1: current oracles only). | | A12 | **Stale drained pool** | Reserves past max-staleness omitted so a dead snapshot cannot keep TVL inflated. | | A13 | **NUMERIC / JSON bomb** | Values that fail `fits_numeric_38_18` skipped; response stays small; HTTP 200. | | A14 | **Rebuild fake history** | After `--fresh`, do not backfill 30d Δ% from zeros or from `liquidity_events`. Δ% null until real snapshots accrue. | | A15 | **CG field confusion** | Protocol UI never reads `liquidity_in_usd`. Integrator tests may still see the old CG mislabel. | | A16 | **Book escrow inflation** | Resting limits / parked orders **not** in pool TVL. | | A17 | **Same-asset / unlisted pair** | Skipped (hub already does this). | | A18 | **Path / ticker injection** | No new unallowlisted ticker path. Overview stays a single GET. `?ticker=` oracle allowlist unchanged. | | A19 | **Cache poison** | Mutating DB after a GET does not change the cached body until TTL; tests may document 60s lag (not a bug). | | A20 | **Partial column UPDATE** | A refresh that writes 24h volume but skips liquidity must fail tests (stale TVL / zero Δ% after real snapshots). | ## Verification criteria Ship is done when **all** of the following hold: 1. `make verify-issue-<iid>` is green on a Cloud Agent / local machine with `make setup-indexer-postgres` (no wasm deploy required for indexer + RTL). Playwright e2e-smoke is optional-but-run when `frontend-dapp/node_modules` exists, **5 workers**. 2. Indexer tests cover T1–T10, H1–H7, V1–V4, and A1/A4/A5/A7/A8/A10/A14 (lib and/or `api_overview` / new `*_liquidity_*` integration file, `--test-threads=1`). 3. Frontend: `formatProtocolStats` + `ProtocolPage` RTL for S1–S8; Charts types still compile with additive overview fields. 4. Docs: invariants row, Protocol skill (new P-ids or extend P550), overview runbook (snapshots + “not TVL from CG”), `docs/frontend.md`, `AGENTS.md` crosslink. 5. Manual / QA: `/protocol` shows total liquidity + 24h% + 30d%; young indexer shows em-dash for Δ%; volume / hub / oracle / audit unchanged. 6. `make verify-issue-550` and `make verify-issue-556` stay green (no regression of census, hub card, or cheap `/overview`).
PlasticDigits commented 2026-08-20 02:17:16 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1100

mentioned in merge request !1100
PlasticDigits commented 2026-08-20 02:17:46 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1101

mentioned in merge request !1101
PlasticDigits commented 2026-08-20 02:18:44 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1102

mentioned in merge request !1102
PlasticDigits commented 2026-08-20 03:29:40 +00:00 (Migrated from gitlab.com)

mentioned in commit 91aed331f1

mentioned in commit 91aed331f1e06120e252fe1c05caf9ece1c5f025
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-20 03:29:40 +00:00
PlasticDigits commented 2026-08-20 03:54:25 +00:00 (Migrated from gitlab.com)

Merged via !1102.

Conflict with #576 hid liquidity StatBoxes when overview failed; that was reversed before merge so /protocol e2e still finds protocol-stat-liquidity*. RTL + Playwright protocol-page (5 workers) passed. Remaining:

  • Production /protocol: Total liquidity + 24h% + 30d% next to volume; young indexer Δ% is em-dash, not 0% / Infinity
  • Indexer must apply global_stats_pool_tvl migration and run refresh_protocol_liquidity
Merged via !1102. Conflict with #576 hid liquidity StatBoxes when overview failed; that was reversed before merge so `/protocol` e2e still finds `protocol-stat-liquidity*`. RTL + Playwright protocol-page (5 workers) passed. Remaining: - Production `/protocol`: Total liquidity + 24h% + 30d% next to volume; young indexer Δ% is em-dash, not `0%` / Infinity - Indexer must apply `global_stats_pool_tvl` migration and run `refresh_protocol_liquidity`
PlasticDigits commented 2026-08-20 03:54:51 +00:00 (Migrated from gitlab.com)

mentioned in issue #583

mentioned in issue #583
PlasticDigits commented 2026-08-20 03:54:55 +00:00 (Migrated from gitlab.com)

marked as related to #583

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

mentioned in issue #586

mentioned in issue #586
PlasticDigits commented 2026-08-22 03:10:05 +00:00 (Migrated from gitlab.com)

mentioned in issue #589

mentioned in issue #589
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:36 +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:33 +00:00 (Migrated from gitlab.com)

mentioned in issue #655

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

marked as related to #655

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

mentioned in issue #664

mentioned in issue #664
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:47 +00:00 (Migrated from gitlab.com)

marked as related to #667

marked as related to #667
PlasticDigits commented 2026-08-26 07:27:29 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1173

mentioned in merge request !1173
PlasticDigits commented 2026-08-27 00:17:42 +00:00 (Migrated from gitlab.com)

mentioned in issue #677

mentioned in issue #677
PlasticDigits commented 2026-08-27 00:17:44 +00:00 (Migrated from gitlab.com)

marked as related to #677

marked as related to #677
PlasticDigits commented 2026-08-27 04:45:36 +00:00 (Migrated from gitlab.com)

mentioned in issue #684

mentioned in issue #684
PlasticDigits commented 2026-08-27 04:50:02 +00:00 (Migrated from gitlab.com)

mentioned in issue #685

mentioned in issue #685
PlasticDigits commented 2026-08-27 04:50:03 +00:00 (Migrated from gitlab.com)

marked as related to #685

marked as related to #685
PlasticDigits commented 2026-08-27 09:00:10 +00:00 (Migrated from gitlab.com)

mentioned in issue #689

mentioned in issue #689
PlasticDigits commented 2026-08-27 09:00:11 +00:00 (Migrated from gitlab.com)

marked as related to #689

marked as related to #689
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#569
No description provided.