feat: /pool Vol column + pair-list 24h volume in USD (leftover #544) #692

Closed
opened 2026-08-28 05:22:05 +00:00 by PlasticDigits · 15 comments
PlasticDigits commented 2026-08-28 05:22:05 +00:00 (Migrated from gitlab.com)

Summary

/pool Vol still prints human quote-token 24h volume (formatQuoteVolume24h(volume_quote_24h)). Retailers cannot compare UST1/cUSTC vs UST1/USTR vs cLUNC/UST1. v2 LP USD (#655) is pool stock; this issue is 24h flow in USD — the leftover of #544 on pair-list surfaces.

Bundle one MR: indexer list field + rollup + sort + /pool Vol cell + Manage line + Trade/Limits/Charts pair-search badges. Do not split “API field” vs “table cell.”

Related (do not reopen as done): #544 (ingest + Charts/Protocol USD shipped; list JSON never got USD), #547 (/pool table), #534 (human quote badges — still not USD), #548 (Protocol/overview USD), #565 (Charts pair-detail GET …/stats volume_usd), #655 (v2 LP USD stock, not Vol), #576 (trailing 24h copy), #577 (window zero), #243 (pair_volume_24h rollup), #489 (no lectures).

Current codebase

Indexer — pair list is quote-raw; USD exists only on detail stats

GET /api/v1/pairs (indexer/src/api/pairs.rs PairResponse) exposes:

Field Unit Role
volume_quote_24h raw quote-side integer LEFT JOIN pair_volume_24h (volume_quote)
liquidity_usd human USD pair_liquidity_usd stamp (#655 stock)
(none) 24h flow USD missing

refresh_pair_volumes (indexer/src/db/queries/volume.rs) rebuilds only volume_quote:

SUM(CASE WHEN se.offer_asset_id = p.asset_0_id THEN se.return_amount ELSE se.offer_amount END)

No volume_usd column on pair_volume_24h (migration 20260531143000_pair_volume_24h_rollup.sql; later widened to NUMERIC(38, 0)). sort=volume_24h is ORDER BY COALESCE(pv.volume_quote, 0) — 18-dec USTR always outranks 6-dec cUSTC.

Per-swap USD is already ingested (swap_events.volume_usd via volume_usd_for_swap / P522-Q + hub, #548 / #556). GET /api/v1/pairs/{addr}/stats already returns human volume_usd (SUM(swap_events.volume_usd) on the request path). The list must not copy that live SUM (V1 / #243). Overview already rolls SUM(volume_usd) into global_stats_24h. Token windows already store volume_usd. Pair list is the gap.

IndexerPair (frontend-dapp/src/types/index.ts) has no volume_usd_24h.

Frontend — /pool Vol is quote tokens; same list JSON feeds pickers

Surface Today Why it fails retail
/pool table Vol (PoolPairsTable.tsx pool-row-vol) formatQuoteVolume24h(ip.volume_quote_24h, ip.asset_1.decimals) Human cUSTC / USTR / UST1, no $. Header POOL_VOL_HEADER_TITLE = “Quote-side volume…”. Sort key volume_24h (raw).
/pool Manage expand (PoolAdvancedManage.tsx) 24h vol (quote, indexed): {volumeLabel} Same quote figure; lecture-ish copy (#489).
Trade / Limits / Charts pair search (PairSearchSelect.tsx) Badge vol {formatQuoteVolume24h(…, quoteDecimals ?? 6)} #534 scaled USTR so tests expect vol 19.3 USTR, not USD. Mixed quotes still incomparable.
Catalog rank (pairCatalogRank.ts P534-3) Human quote raw / 10^decimals Better than raw; 1 human USTR ≠ 1 human cUSTC.

v2 LP USD sits in the next column (formatProtocolUsd(liquidity_usd)). Retailers already see one $ column (stock) next to a token Vol (flow). That mismatch is the bug.

Charts pair-detail Vol (USD) is out of scope (already #565 via …/stats). Do not N+1 GET /pairs/{addr}/stats from /pool (P547-9 / A8).

Why this is needed

  1. Vol without a unit is a lie. “48.2M” on UST1/cUSTC is cUSTC (and cUSTC ≉ $1). UST1/USTR 18-dec still reads as an enormous token number even after #534 humanizes it. Price is already USD (#522 / #524); pool stock is already USD (#655). Flow is the last mixed-unit column on /pool.
  2. #534 / #544 did not close the list. #534 only stopped formatNum(raw) → …T. #544 shipped ingest + Charts/Protocol; GET /pairs never gained volume_usd_24h, so /pool had nothing truthful to render.
  3. Sort distortion. User click Vol still ranks by raw quote, so an 18-dec market can sit above a deeper 6-dec USD market.
  4. No client workaround is allowed. Browser volume_quote × hub-prices, CG liquidity_in_usd, or per-row LCD//stats are explicitly forbidden (#655 / P547-9). The indexer must stamp the list.

Constraints / guardrails

  1. USD is advisory, not settlement (X5 / P522). Label compact $…. Tooltip: trailing 24h priced swaps, not a peg. Never imply redeemability.
  2. Reuse volume_usd_for_swap / swap_events.volume_usd. Do not invent a second catalog. Do not convert with vFDUSD (X4). Do not use $1 UST1 or 2.5× USTR — hub marks (#556), same as #548 / #565.
  3. One notional per swap (L10). SUM(volume_usd) of consolidated swaps only. No pool_leg + book_leg, no limit_order_fills.
  4. Pair-list JSON stays additive. Keep volume_quote_24h raw for integrators / #534. Add optional volume_usd_24h (human USD decimal string, same scale as stats/overview). Omit/null when unpriced or overflow; do not COALESCE to 0 for unpriced. Idle (no 24h swaps) may be 0 or omitted — UI — either way (today’s zero hide). Do not humanize volume_quote_24h in the indexer.
  5. Rollup, not live scan (V1 / #243). Extend pair_volume_24h + refresh_pair_volumes (~5 min). GET /pairs (including sort=volume_usd_24h) must not SUM(swap_events) per request. Idle pairs zero USD when swaps leave the window (#577 D3), same as quote. Pagination caps unchanged (limit clamp, offset ≤ 10_000 → 400).
  6. Sort. Keep sort=volume_24h as raw quote (do not break integrators). Add sort=volume_usd_24h (default desc, NULLS LAST, same as liquidity_usd). /pool Vol header must use volume_usd_24h so caret matches the $ cell. Invalid sort (injection / tvl / volume_usd) → 400.
  7. Do not N+1. Default /pool paint: zero new getPool / getPairFeeConfig / GET …/stats. Manage colSpan stays 7 unless a column is added (it is not).
  8. Do not invent USD in the browser from volume_quote_24h, hub-prices, CG, or $1/leg.
  9. Missing USD → —, not $0, not raw fallback. Unpriced gems with quote volume must not show 19297048T or 48.2M. Hostile / non-finite / negative / Infinity / HTML → — (reuse formatIndexedVolumeUsd length + <> guards, #548 A4).
  10. Do not change CG/CMC base_volume / target_volume, candle histogram, #522 price/price_usd, #655 liquidity_usd, or Charts pair-detail stats.
  11. Copy (#489 / #576). Header stays Vol (one word). Update POOL_VOL_HEADER_TITLE to priced-USD trailing 24h (not “quote-side”). Manage line: short 24h vol $… or hide when —. Badge vol $12.4K. No oracle essays.
  12. Catalog rank. Empty /pool browse stays catalog overlay (P547-3). Within a hub, prefer USD 24h when present, else existing human-quote fallback (P534-3). Do not re-apply catalog after the user clicks Vol.
  13. Production hide-gems (#562) unchanged. No nested card-glass (#653).
  14. Docs + skill + make verify-issue-<iid> in the same MR. Invariants PVol-1–PVol-8. Update docs/indexer-invariants.md pair-list row, docs/frontend.md P547-4 / P534-4, skills/AGENTS_FRONTEND_POOL_TABLE.md, skills/AGENTS_FRONTEND_PAIR_CATALOG_RANK.md, AGENTS.md, docs/testing.md.

Relevant files

File Role
indexer/migrations/20260531143000_pair_volume_24h_rollup.sql Existing quote rollup — add volume_usd column via new migration
indexer/src/db/queries/volume.rs refresh_pair_volumes — stamp SUM(volume_usd) + zero idle
indexer/src/db/queries/pairs.rs PairListRow + SELECT + PairListSort::VolumeUsd24h NULLS LAST
indexer/src/api/pairs.rs Additive volume_usd_24h; sort allowlist; pair_to_response
indexer/src/indexer/pair_price_usd.rs Shared ingest — do not fork
indexer/tests/indexer_pair_volume_pagination.rs EXPLAIN still uses rollup, not swap_events
frontend-dapp/src/types/index.ts IndexerPair.volume_usd_24h; sort union
frontend-dapp/src/utils/poolListQuery.ts POOL_COLUMN_SORTS Vol → volume_usd_24h
frontend-dapp/src/components/pool/PoolPairsTable.tsx Cell + header title
frontend-dapp/src/components/pool/PoolAdvancedManage.tsx Manage 24h vol line
frontend-dapp/src/utils/trailingWindowCopy.ts POOL_VOL_HEADER_TITLE
frontend-dapp/src/utils/chartsOverviewStats.ts Reuse formatIndexedVolumeUsd
frontend-dapp/src/components/trade/PairSearchSelect.tsx Badge vol $…
frontend-dapp/src/utils/pairCatalogRank.ts USD-first volume compare when present
frontend-dapp/src/pages/PoolPage.test.tsx Vol sort + cell
docs/frontend.md, docs/indexer-invariants.md, skills above Invariants + verify
  1. Migration: ALTER TABLE pair_volume_24h ADD COLUMN volume_usd NUMERIC(38, 18) (nullable; absent/NULL = unpriced). Index (volume_usd DESC NULLS LAST) if needed for sort=volume_usd_24h. Do not change volume_quote.
  2. refresh_pair_volumes: In the same INSERT/UPDATE that sets volume_quote, set volume_usd = SUM(se.volume_usd) FILTER (WHERE volume_usd IS NOT NULL AND volume_usd > 0) clamped to NUMERIC(38, 18) / < 10^20 (same overflow guard as overview). Idle UPDATE zeros both quote and USD.
  3. API: volume_usd_24h?: string with skip_serializing_if. List and token-pairs / shared pair_to_response stay one shape. Single-pair GET /pairs/{addr} may omit 24h USD (today it already omits volume_quote_24h) — do not live-scan there.
  4. dApp: Vol cell = formatIndexedVolumeUsd(ip.volume_usd_24h, /* idle→0 */ ) or equivalent: priced → $ + compact; missing/invalid → —; never formatQuoteVolume24h / formatNum(raw) on this cell. Old indexers omit the field → all —.
  5. Pair search: Same field; hide badge when — / missing (same as today’s zero-quote hide). Do not keep a quote badge as the default once USD exists.
  6. Manage: Replace “24h vol (quote, indexed)” with the same $ string; optional title for trailing window. Quote remainder is not required on /pool (Charts already has token vols).

Acceptance criteria

  • PVol-1 — /pool Vol cells are compact USD ($ + formatIndexedVolumeUsd / equivalent) or —. Never raw 18-dec, never quote-token 48.2M / 19.3 without $.
  • PVol-2 — Values come only from list JSON volume_usd_24h. No per-row /stats, LCD, hub-prices, or volume_quote_24h × anything.
  • PVol-3 — GET /api/v1/pairs is additive: volume_quote_24h unchanged (raw). volume_usd_24h omitted/null when unpriced. sort=volume_usd_24h default desc, NULLS LAST. sort=volume_24h still quote-raw.
  • PVol-4 — First Vol header click is sort=volume_usd_24h&order=desc; toggle asc/desc; aria-sort on that <th> only. Catalog default (no click) unchanged (P547-3) except within-hub tie-break may use USD.
  • PVol-5 — Priced UST1/cUSTC (or UST1/USTR) 24h USD matches GET /pairs/{addr}/stats volume_usd within rollup lag (~5 min). Header title is trailing 24h USD, not midnight reset, not “quote-side.”
  • PVol-6 — Unpriced / stale / overflow → no USD (UI —), sort last. Idle → — (not a fake $0 next to empty quote). Production gems still hidden.
  • PVol-7 — Default paint: zero new LCD / /stats. Manage colSpan 7. Manage + pair-search badges use the same USD field. #489: no architecture lecture.
  • PVol-8 — Invalid sort → 400. limit=-1 clamps to 1. Hostile USD strings cannot XSS or lock the tab.

Test plan (all paths)

Indexer

  1. refresh_pair_volumes writes volume_usd = SUM(swap_events.volume_usd) for priced 24h swaps; idle pair quote and USD go to 0 / NULL as specified (D3).
  2. GET /api/v1/pairs includes volume_usd_24h on priced rows; omits/null on unpriced; volume_quote_24h still raw.
  3. sort=volume_usd_24h&order=desc ranks priced USD desc, unpriced last; asc ranks low priced first, unpriced still last.
  4. sort=volume_24h still quote-raw (regression).
  5. EXPLAIN of list + sort=volume_usd_24h uses pair_volume_24h, not a 24h swap_events aggregate (indexer_pair_volume_pagination.rs pattern).
  6. sort=volume_usd / tvl / SQL injection → 400. limit=0/-1 → clamp 1. offset > 10_000 → 400.
  7. Overflow / NUMERIC clamp: huge volume_usd does not 500 the list.
  8. Shared pair_to_response (list / token-pairs) stays one shape. utoipa/OpenAPI updated.
  9. Window decay: swaps older than 24h drop USD on next refresh (#577).

Frontend — /pool

  1. Catalog default: Vol cells $… or —; v2 LP USD column unchanged; gems hidden in production.
  2. Click Vol → request sort=volume_usd_24h&order=desc; caret + aria-sort; second click asc; other columns clear Vol aria-sort.
  3. Search q= still relevance until cleared (P534-6).
  4. Missing field (old indexer / mock) → all Vol —, no throw.
  5. volume_usd_24h Infinity / NaN / -1 / 1e309 / <script> / 10k-char → —, text-only.
  6. Manage expand: USD line (or hidden); no “quote, indexed” lecture; colSpan={7}; no extra LCD.
  7. Light + dark; phone table still overflow-x-auto.
  8. Existing #547 / #655 / #662 / #576 tests updated (header title, sort key) and still pass.

Frontend — pair search (bundled)

  1. Empty browse + typed search: badge vol $12.4K (or hidden). No vol 19.3 as the default for USTR.
  2. ?? 6 must not rescale a USD string as 6-dec raw.
  3. Catalog rank: USD-first within hub when present; gems still last; production hide-gems.

Vitest / Playwright / make

  • Vitest: PoolPage.test.tsx, poolListQuery.test.ts, pairCatalogRank.test.ts, PairSearchSelect #534 tests (update expected badge), formatAmount / chartsOverviewStats if helpers change, trailingWindowCopy title.
  • Indexer: cargo test --test indexer_pair_volume_pagination --test api_pairs (+ new volume_usd list test) -- --test-threads=1.
  • Playwright pool-table (e2e/pool-table-547.spec.ts) Vol column $ or — (dedicated Vite port; do not leak PLAYWRIGHT_WEB_PORT).
  • make verify-issue-<iid> static + unit + related VERIFY_ISSUE_547_SKIP_E2E=1 make verify-issue-547 / make verify-issue-655 (no nested full e2e unless needed).

Test plan — attack, hack, and abuse

  1. Sort / query injection. sort=volume_usd_24h;DROP+TABLE+pairs, sort=volume_usd, order=desc--. Must 400, never interpolate into SQL (keep QueryBuilder bind + enum).
  2. Pagination abuse. limit=-1, limit=999999, offset=10001 — clamp / 400; no full-table scan; no live 24h SUM as a DoS.
  3. Hostile JSON (XSS / UI lock). volume_usd_24h = <img onerror=…>, javascript:, 1MB string, NaN, Infinity, scientific 1e309. Render —; no HTML; cap display length (#548 A4).
  4. Wash / spoof rank. Native gem with symbol=USTR must not price (A1). Factory-listed clones with hub tickers stay residual (provenance #311). Unpriced wash volume must not sort as $0 first.
  5. Oracle / hub lie. Do not fall back to $1 UST1 or 2.5× USTR when hub is down — omit USD. Do not use Venus/vFDUSD (X4).
  6. Double-count. Confirm rollup does not add book + pool legs or limit fills. One volume_usd per swap_events row.
  7. Stale rollup. Serving last stamp is OK (log if updated_at stale, same class as overview D6). Must not fall back to GET-path 24h scan.
  8. Open redirect / charts href. Unrelated but keep chartsPairHref bech32-only (P547-5). Do not put USD into URLs.
  9. Old indexer downgrade. Missing field → —, not a crash and not a raw-token fallback that reintroduces …T.

Verification criteria

Issue is done only when all are true:

  1. Local /pool Vol column shows $ compact USD (or —) for catalog + Vol sort + search; Manage and pair-search badges match.
  2. GET /api/v1/pairs documents volume_usd_24h; sort=volume_usd_24h works; sort=volume_24h unchanged; EXPLAIN is rollup-only.
  3. A priced pair’s list USD ≈ GET /pairs/{addr}/stats volume_usd (≤ ~5 min lag). Unpriced → — / omitted.
  4. make verify-issue-<iid> is green; #547 / #655 / #534 / #576 regressions updated and green.
  5. Docs + skills + AGENTS.md + docs/testing.md list the new verify target and PVol-1–PVol-8.
  6. No CG/CMC, candle, or #655 stock-column behavior change.

Out of scope

  • Charts pair 24h Stats layout (#565 / #564) and Protocol census (#548).
  • Changing liquidity_usd / v2 LP USD (#655 / #664).
  • Humanizing indexer volume_quote_24h.
  • Calendar-day volume (#668) or DeFiLlama (#631).
  • Raising PAIR_LIST_LIMIT_MAX (catalog limit=500 clamp is a separate concern).
## Summary `/pool` **Vol** still prints **human quote-token** 24h volume (`formatQuoteVolume24h(volume_quote_24h)`). Retailers cannot compare UST1/cUSTC vs UST1/USTR vs cLUNC/UST1. **v2 LP USD** (#655) is pool *stock*; this issue is 24h *flow* in USD — the leftover of [#544](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/544) on **pair-list** surfaces. Bundle **one** MR: indexer list field + rollup + sort + `/pool` Vol cell + Manage line + Trade/Limits/Charts pair-search badges. Do **not** split “API field” vs “table cell.” Related (do **not** reopen as done): [#544](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/544) (ingest + Charts/Protocol USD shipped; list JSON never got USD), [#547](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/547) (`/pool` table), [#534](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/534) (human quote badges — still not USD), [#548](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/548) (Protocol/overview USD), [#565](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/565) (Charts **pair-detail** `GET …/stats` `volume_usd`), [#655](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/655) (**v2 LP USD** stock, not Vol), [#576](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/576) (trailing 24h copy), [#577](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/577) (window zero), [#243](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/243) (`pair_volume_24h` rollup), [#489](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/489) (no lectures). ## Current codebase ### Indexer — pair **list** is quote-raw; USD exists only on detail stats `GET /api/v1/pairs` (`indexer/src/api/pairs.rs` `PairResponse`) exposes: | Field | Unit | Role | |-------|------|------| | `volume_quote_24h` | raw quote-side integer | `LEFT JOIN pair_volume_24h` (`volume_quote`) | | `liquidity_usd` | human USD | `pair_liquidity_usd` stamp (#655 stock) | | *(none)* | 24h flow USD | **missing** | `refresh_pair_volumes` (`indexer/src/db/queries/volume.rs`) rebuilds **only** `volume_quote`: ```sql SUM(CASE WHEN se.offer_asset_id = p.asset_0_id THEN se.return_amount ELSE se.offer_amount END) ``` No `volume_usd` column on `pair_volume_24h` (migration `20260531143000_pair_volume_24h_rollup.sql`; later widened to `NUMERIC(38, 0)`). `sort=volume_24h` is `ORDER BY COALESCE(pv.volume_quote, 0)` — 18-dec USTR always outranks 6-dec cUSTC. Per-swap USD **is** already ingested (`swap_events.volume_usd` via `volume_usd_for_swap` / P522-Q + hub, #548 / #556). `GET /api/v1/pairs/{addr}/stats` already returns human `volume_usd` (`SUM(swap_events.volume_usd)` on the request path). The **list** must not copy that live `SUM` (V1 / #243). Overview already rolls `SUM(volume_usd)` into `global_stats_24h`. Token windows already store `volume_usd`. Pair list is the gap. `IndexerPair` (`frontend-dapp/src/types/index.ts`) has no `volume_usd_24h`. ### Frontend — `/pool` Vol is quote tokens; same list JSON feeds pickers | Surface | Today | Why it fails retail | |---------|-------|---------------------| | `/pool` table Vol (`PoolPairsTable.tsx` `pool-row-vol`) | `formatQuoteVolume24h(ip.volume_quote_24h, ip.asset_1.decimals)` | Human **cUSTC / USTR / UST1**, no `$`. Header `POOL_VOL_HEADER_TITLE` = “Quote-side volume…”. Sort key `volume_24h` (raw). | | `/pool` Manage expand (`PoolAdvancedManage.tsx`) | `24h vol (quote, indexed): {volumeLabel}` | Same quote figure; lecture-ish copy (#489). | | Trade / Limits / Charts pair search (`PairSearchSelect.tsx`) | Badge `vol {formatQuoteVolume24h(…, quoteDecimals ?? 6)}` | #534 scaled USTR so tests expect `vol 19.3` **USTR**, not USD. Mixed quotes still incomparable. | | Catalog rank (`pairCatalogRank.ts` **P534-3**) | Human quote `raw / 10^decimals` | Better than raw; 1 human USTR ≠ 1 human cUSTC. | **v2 LP USD** sits in the next column (`formatProtocolUsd(liquidity_usd)`). Retailers already see one `$` column (stock) next to a token Vol (flow). That mismatch is the bug. Charts pair-detail **Vol (USD)** is **out of scope** (already #565 via `…/stats`). Do **not** N+1 `GET /pairs/{addr}/stats` from `/pool` (**P547-9** / A8). ## Why this is needed 1. **Vol without a unit is a lie.** “48.2M” on UST1/cUSTC is cUSTC (and cUSTC ≉ $1). UST1/USTR 18-dec still reads as an enormous token number even after #534 humanizes it. Price is already USD (#522 / #524); pool **stock** is already USD (#655). Flow is the last mixed-unit column on `/pool`. 2. **#534 / #544 did not close the list.** #534 only stopped `formatNum(raw)` → `…T`. #544 shipped ingest + Charts/Protocol; `GET /pairs` never gained `volume_usd_24h`, so `/pool` had nothing truthful to render. 3. **Sort distortion.** User click **Vol** still ranks by raw quote, so an 18-dec market can sit above a deeper 6-dec USD market. 4. **No client workaround is allowed.** Browser `volume_quote × hub-prices`, CG `liquidity_in_usd`, or per-row LCD/`/stats` are explicitly forbidden (#655 / **P547-9**). The indexer must stamp the list. ## Constraints / guardrails 1. **USD is advisory, not settlement** (X5 / P522). Label compact `$…`. Tooltip: trailing 24h priced swaps, not a peg. Never imply redeemability. 2. **Reuse `volume_usd_for_swap` / `swap_events.volume_usd`.** Do not invent a second catalog. Do **not** convert with vFDUSD (X4). Do **not** use `$1` UST1 or `2.5×` USTR — hub marks (#556), same as #548 / #565. 3. **One notional per swap (L10).** `SUM(volume_usd)` of consolidated swaps only. No `pool_leg` + `book_leg`, no `limit_order_fills`. 4. **Pair-list JSON stays additive.** Keep `volume_quote_24h` **raw** for integrators / #534. Add optional `volume_usd_24h` (human USD decimal string, same scale as stats/`overview`). Omit/`null` when unpriced or overflow; do **not** `COALESCE` to `0` for unpriced. Idle (no 24h swaps) may be `0` or omitted — UI **—** either way (today’s zero hide). Do **not** humanize `volume_quote_24h` in the indexer. 5. **Rollup, not live scan (V1 / #243).** Extend `pair_volume_24h` + `refresh_pair_volumes` (~5 min). `GET /pairs` (including `sort=volume_usd_24h`) must **not** `SUM(swap_events)` per request. Idle pairs **zero** USD when swaps leave the window (#577 **D3**), same as quote. Pagination caps unchanged (`limit` clamp, `offset` ≤ 10_000 → 400). 6. **Sort.** Keep `sort=volume_24h` as **raw quote** (do not break integrators). Add `sort=volume_usd_24h` (default desc, **NULLS LAST**, same as `liquidity_usd`). `/pool` Vol header **must** use `volume_usd_24h` so caret matches the `$` cell. Invalid `sort` (injection / `tvl` / `volume_usd`) → **400**. 7. **Do not N+1.** Default `/pool` paint: zero new `getPool` / `getPairFeeConfig` / `GET …/stats`. Manage `colSpan` stays 7 unless a column is added (it is not). 8. **Do not invent USD in the browser** from `volume_quote_24h`, hub-prices, CG, or `$1`/leg. 9. **Missing USD → `—`, not `$0`, not raw fallback.** Unpriced gems with quote volume must not show `19297048T` or `48.2M`. Hostile / non-finite / negative / `Infinity` / HTML → `—` (reuse `formatIndexedVolumeUsd` length + `<>` guards, #548 **A4**). 10. **Do not change** CG/CMC `base_volume` / `target_volume`, candle histogram, #522 `price`/`price_usd`, #655 `liquidity_usd`, or Charts pair-detail stats. 11. **Copy (#489 / #576).** Header stays **Vol** (one word). Update `POOL_VOL_HEADER_TITLE` to priced-USD trailing 24h (not “quote-side”). Manage line: short `24h vol $…` or hide when `—`. Badge `vol $12.4K`. No oracle essays. 12. **Catalog rank.** Empty `/pool` browse stays catalog overlay (**P547-3**). Within a hub, prefer **USD 24h** when present, else existing human-quote fallback (**P534-3**). Do **not** re-apply catalog after the user clicks Vol. 13. **Production hide-gems (#562)** unchanged. No nested `card-glass` (#653). 14. **Docs + skill + `make verify-issue-<iid>`** in the same MR. Invariants **PVol-1–PVol-8**. Update `docs/indexer-invariants.md` pair-list row, `docs/frontend.md` **P547-4** / **P534-4**, `skills/AGENTS_FRONTEND_POOL_TABLE.md`, `skills/AGENTS_FRONTEND_PAIR_CATALOG_RANK.md`, `AGENTS.md`, `docs/testing.md`. ## Relevant files | File | Role | |------|------| | `indexer/migrations/20260531143000_pair_volume_24h_rollup.sql` | Existing quote rollup — add `volume_usd` column via **new** migration | | `indexer/src/db/queries/volume.rs` | `refresh_pair_volumes` — stamp `SUM(volume_usd)` + zero idle | | `indexer/src/db/queries/pairs.rs` | `PairListRow` + SELECT + `PairListSort::VolumeUsd24h` **NULLS LAST** | | `indexer/src/api/pairs.rs` | Additive `volume_usd_24h`; sort allowlist; `pair_to_response` | | `indexer/src/indexer/pair_price_usd.rs` | Shared ingest — do not fork | | `indexer/tests/indexer_pair_volume_pagination.rs` | EXPLAIN still uses rollup, not `swap_events` | | `frontend-dapp/src/types/index.ts` | `IndexerPair.volume_usd_24h`; sort union | | `frontend-dapp/src/utils/poolListQuery.ts` | `POOL_COLUMN_SORTS` Vol → `volume_usd_24h` | | `frontend-dapp/src/components/pool/PoolPairsTable.tsx` | Cell + header title | | `frontend-dapp/src/components/pool/PoolAdvancedManage.tsx` | Manage 24h vol line | | `frontend-dapp/src/utils/trailingWindowCopy.ts` | `POOL_VOL_HEADER_TITLE` | | `frontend-dapp/src/utils/chartsOverviewStats.ts` | Reuse `formatIndexedVolumeUsd` | | `frontend-dapp/src/components/trade/PairSearchSelect.tsx` | Badge `vol $…` | | `frontend-dapp/src/utils/pairCatalogRank.ts` | USD-first volume compare when present | | `frontend-dapp/src/pages/PoolPage.test.tsx` | Vol sort + cell | | `docs/frontend.md`, `docs/indexer-invariants.md`, skills above | Invariants + verify | ## Recommended direction 1. **Migration:** `ALTER TABLE pair_volume_24h ADD COLUMN volume_usd NUMERIC(38, 18)` (nullable; absent/NULL = unpriced). Index `(volume_usd DESC NULLS LAST)` if needed for `sort=volume_usd_24h`. Do not change `volume_quote`. 2. **`refresh_pair_volumes`:** In the same INSERT/UPDATE that sets `volume_quote`, set `volume_usd = SUM(se.volume_usd) FILTER (WHERE volume_usd IS NOT NULL AND volume_usd > 0)` clamped to `NUMERIC(38, 18)` / `< 10^20` (same overflow guard as overview). Idle UPDATE zeros **both** quote and USD. 3. **API:** `volume_usd_24h?: string` with `skip_serializing_if`. List **and** token-pairs / shared `pair_to_response` stay one shape. Single-pair `GET /pairs/{addr}` may omit 24h USD (today it already omits `volume_quote_24h`) — do not live-scan there. 4. **dApp:** Vol cell = `formatIndexedVolumeUsd(ip.volume_usd_24h, /* idle→0 */ )` or equivalent: priced → `$` + compact; missing/invalid → **—**; never `formatQuoteVolume24h` / `formatNum(raw)` on this cell. Old indexers omit the field → all **—**. 5. **Pair search:** Same field; hide badge when `—` / missing (same as today’s zero-quote hide). Do not keep a quote badge as the default once USD exists. 6. **Manage:** Replace “24h vol (quote, indexed)” with the same `$` string; optional `title` for trailing window. Quote remainder is **not** required on `/pool` (Charts already has token vols). ## Acceptance criteria - **PVol-1** — `/pool` **Vol** cells are compact **USD** (`$` + `formatIndexedVolumeUsd` / equivalent) or **—**. Never raw 18-dec, never quote-token `48.2M` / `19.3` without `$`. - **PVol-2** — Values come only from list JSON `volume_usd_24h`. No per-row `/stats`, LCD, hub-prices, or `volume_quote_24h ×` anything. - **PVol-3** — `GET /api/v1/pairs` is additive: `volume_quote_24h` unchanged (raw). `volume_usd_24h` omitted/`null` when unpriced. `sort=volume_usd_24h` default desc, **NULLS LAST**. `sort=volume_24h` still quote-raw. - **PVol-4** — First Vol header click is `sort=volume_usd_24h&order=desc`; toggle asc/desc; `aria-sort` on that `<th>` only. Catalog default (no click) unchanged (**P547-3**) except within-hub tie-break may use USD. - **PVol-5** — Priced UST1/cUSTC (or UST1/USTR) 24h USD matches `GET /pairs/{addr}/stats` `volume_usd` within rollup lag (~5 min). Header `title` is trailing 24h USD, not midnight reset, not “quote-side.” - **PVol-6** — Unpriced / stale / overflow → no USD (UI **—**), sort last. Idle → **—** (not a fake `$0` next to empty quote). Production gems still hidden. - **PVol-7** — Default paint: zero new LCD / `/stats`. Manage `colSpan` 7. Manage + pair-search badges use the same USD field. `#489`: no architecture lecture. - **PVol-8** — Invalid `sort` → **400**. `limit=-1` clamps to 1. Hostile USD strings cannot XSS or lock the tab. ## Test plan (all paths) ### Indexer 1. `refresh_pair_volumes` writes `volume_usd` = `SUM(swap_events.volume_usd)` for priced 24h swaps; idle pair quote **and** USD go to 0 / NULL as specified (**D3**). 2. `GET /api/v1/pairs` includes `volume_usd_24h` on priced rows; omits/null on unpriced; `volume_quote_24h` still raw. 3. `sort=volume_usd_24h&order=desc` ranks priced USD desc, unpriced last; `asc` ranks low priced first, unpriced still last. 4. `sort=volume_24h` still quote-raw (regression). 5. `EXPLAIN` of list + `sort=volume_usd_24h` uses `pair_volume_24h`, **not** a 24h `swap_events` aggregate (`indexer_pair_volume_pagination.rs` pattern). 6. `sort=volume_usd` / `tvl` / SQL injection → **400**. `limit=0`/`-1` → clamp 1. `offset` > 10_000 → **400**. 7. Overflow / `NUMERIC` clamp: huge `volume_usd` does not 500 the list. 8. Shared `pair_to_response` (list / token-pairs) stays one shape. utoipa/OpenAPI updated. 9. Window decay: swaps older than 24h drop USD on next refresh (#577). ### Frontend — `/pool` 1. Catalog default: Vol cells `$…` or **—**; v2 LP USD column unchanged; gems hidden in production. 2. Click Vol → request `sort=volume_usd_24h&order=desc`; caret + `aria-sort`; second click asc; other columns clear Vol `aria-sort`. 3. Search `q=` still `relevance` until cleared (**P534-6**). 4. Missing field (old indexer / mock) → all Vol **—**, no throw. 5. `volume_usd_24h` `Infinity` / `NaN` / `-1` / `1e309` / `<script>` / 10k-char → **—**, text-only. 6. Manage expand: USD line (or hidden); no “quote, indexed” lecture; `colSpan={7}`; no extra LCD. 7. Light + dark; phone table still `overflow-x-auto`. 8. Existing #547 / #655 / #662 / #576 tests updated (header title, sort key) and still pass. ### Frontend — pair search (bundled) 1. Empty browse + typed search: badge `vol $12.4K` (or hidden). No `vol 19.3` as the default for USTR. 2. `?? 6` must not rescale a USD string as 6-dec raw. 3. Catalog rank: USD-first within hub when present; gems still last; production hide-gems. ### Vitest / Playwright / make - Vitest: `PoolPage.test.tsx`, `poolListQuery.test.ts`, `pairCatalogRank.test.ts`, `PairSearchSelect` #534 tests (update expected badge), `formatAmount` / `chartsOverviewStats` if helpers change, `trailingWindowCopy` title. - Indexer: `cargo test --test indexer_pair_volume_pagination --test api_pairs` (+ new volume_usd list test) `-- --test-threads=1`. - Playwright pool-table (`e2e/pool-table-547.spec.ts`) Vol column `$` or **—** (dedicated Vite port; do not leak `PLAYWRIGHT_WEB_PORT`). - `make verify-issue-<iid>` static + unit + related `VERIFY_ISSUE_547_SKIP_E2E=1 make verify-issue-547` / `make verify-issue-655` (no nested full e2e unless needed). ## Test plan — attack, hack, and abuse 1. **Sort / query injection.** `sort=volume_usd_24h;DROP+TABLE+pairs`, `sort=volume_usd`, `order=desc--`. Must **400**, never interpolate into SQL (keep `QueryBuilder` bind + enum). 2. **Pagination abuse.** `limit=-1`, `limit=999999`, `offset=10001` — clamp / 400; no full-table scan; no live 24h `SUM` as a DoS. 3. **Hostile JSON (XSS / UI lock).** `volume_usd_24h` = `<img onerror=…>`, `javascript:`, 1MB string, `NaN`, `Infinity`, scientific `1e309`. Render **—**; no HTML; cap display length (#548 **A4**). 4. **Wash / spoof rank.** Native gem with `symbol=USTR` must not price (A1). Factory-listed clones with hub tickers stay residual (provenance #311). Unpriced wash volume must not sort as `$0` first. 5. **Oracle / hub lie.** Do not fall back to `$1` UST1 or `2.5×` USTR when hub is down — omit USD. Do not use Venus/vFDUSD (X4). 6. **Double-count.** Confirm rollup does not add book + pool legs or limit fills. One `volume_usd` per `swap_events` row. 7. **Stale rollup.** Serving last stamp is OK (log if `updated_at` stale, same class as overview **D6**). Must not fall back to GET-path 24h scan. 8. **Open redirect / charts href.** Unrelated but keep `chartsPairHref` bech32-only (**P547-5**). Do not put USD into URLs. 9. **Old indexer downgrade.** Missing field → **—**, not a crash and not a raw-token fallback that reintroduces `…T`. ## Verification criteria Issue is done only when **all** are true: 1. Local `/pool` Vol column shows `$` compact USD (or **—**) for catalog + Vol sort + search; Manage and pair-search badges match. 2. `GET /api/v1/pairs` documents `volume_usd_24h`; `sort=volume_usd_24h` works; `sort=volume_24h` unchanged; EXPLAIN is rollup-only. 3. A priced pair’s list USD ≈ `GET /pairs/{addr}/stats` `volume_usd` (≤ ~5 min lag). Unpriced → **—** / omitted. 4. `make verify-issue-<iid>` is green; #547 / #655 / #534 / #576 regressions updated and green. 5. Docs + skills + `AGENTS.md` + `docs/testing.md` list the new verify target and **PVol-1–PVol-8**. 6. No CG/CMC, candle, or #655 stock-column behavior change. ## Out of scope - Charts pair 24h Stats layout (#565 / #564) and Protocol census (#548). - Changing `liquidity_usd` / v2 LP USD (#655 / #664). - Humanizing indexer `volume_quote_24h`. - Calendar-day volume (#668) or DeFiLlama (#631). - Raising `PAIR_LIST_LIMIT_MAX` (catalog `limit=500` clamp is a separate concern).
PlasticDigits commented 2026-08-28 05:22:07 +00:00 (Migrated from gitlab.com)

marked as related to #544

marked as related to #544
PlasticDigits commented 2026-08-28 05:22:09 +00:00 (Migrated from gitlab.com)

marked as related to #547

marked as related to #547
PlasticDigits commented 2026-08-28 05:22:10 +00:00 (Migrated from gitlab.com)

marked as related to #534

marked as related to #534
PlasticDigits commented 2026-08-28 05:22:10 +00:00 (Migrated from gitlab.com)

marked as related to #548

marked as related to #548
PlasticDigits commented 2026-08-28 05:22:11 +00:00 (Migrated from gitlab.com)

marked as related to #565

marked as related to #565
PlasticDigits commented 2026-08-28 05:22:12 +00:00 (Migrated from gitlab.com)

marked as related to #655

marked as related to #655
PlasticDigits commented 2026-08-28 05:22:13 +00:00 (Migrated from gitlab.com)

marked as related to #576

marked as related to #576
PlasticDigits commented 2026-08-29 04:22:36 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1195

mentioned in merge request !1195
PlasticDigits commented 2026-08-29 04:53:38 +00:00 (Migrated from gitlab.com)

Merge review (!1195)

Local AC: make verify-issue-692 8/8 PASS (docs, source guards, indexer integration, Pool/catalog Vitest, Playwright e2e/pool-table-547 with 5 workers, related 547/655/534). GitLab CI on !1195 failed with ci_quota_exceeded only — not a product signal; merging without automerge / CI wait.

PVol-1–4, 6–8: met in source (list volume_usd_24h, USD Vol/—, NULLS LAST, no quote fallback, no /stats N+1).

PVol-5 gap (non-blocking): header title is trailing 24h USD. There is no automated check that list volume_usd_24h equals GET /pairs/{addr}/stats volume_usd right after refresh_pair_volumes. Rollup uses SUM(volume_usd) FILTER (>0) + NULL on ≥1e20; /stats uses live SUM(volume_usd) without that clamp — fine for normal priced pairs. Needs a reviewer curl on a live indexer (or a small integration assert) after Coolify deploy.

Manual leftovers: light+dark /pool Vol readability; production gem hide (existing P562 path, not re-proven in verify-issue-692).

Do not reopen this issue for ops/QA after merge. Track Coolify + live list↔stats check on a leftover ops issue.

## Merge review (!1195) Local AC: `make verify-issue-692` **8/8 PASS** (docs, source guards, indexer integration, Pool/catalog Vitest, Playwright `e2e/pool-table-547` with 5 workers, related 547/655/534). GitLab CI on !1195 failed with `ci_quota_exceeded` only — not a product signal; merging without automerge / CI wait. **PVol-1–4, 6–8:** met in source (list `volume_usd_24h`, USD Vol/`—`, NULLS LAST, no quote fallback, no `/stats` N+1). **PVol-5 gap (non-blocking):** header title is trailing 24h USD. There is no automated check that list `volume_usd_24h` equals `GET /pairs/{addr}/stats` `volume_usd` right after `refresh_pair_volumes`. Rollup uses `SUM(volume_usd) FILTER (>0)` + NULL on ≥1e20; `/stats` uses live `SUM(volume_usd)` without that clamp — fine for normal priced pairs. Needs a reviewer curl on a live indexer (or a small integration assert) after Coolify deploy. **Manual leftovers:** light+dark `/pool` Vol readability; production gem hide (existing P562 path, not re-proven in verify-issue-692). Do not reopen this issue for ops/QA after merge. Track Coolify + live list↔stats check on a leftover ops issue.
PlasticDigits commented 2026-08-29 04:53:46 +00:00 (Migrated from gitlab.com)

mentioned in commit f4fa678838

mentioned in commit f4fa67883832114331d05cfa55d5eea94251e2a0
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-29 04:53:46 +00:00
PlasticDigits commented 2026-08-29 04:56:14 +00:00 (Migrated from gitlab.com)

mentioned in issue #701

mentioned in issue #701
PlasticDigits commented 2026-08-29 04:56:14 +00:00 (Migrated from gitlab.com)

marked as related to #701

marked as related to #701
PlasticDigits commented 2026-08-29 04:56:24 +00:00 (Migrated from gitlab.com)

Merged

!1195 is on main (f4fa6788). Local make verify-issue-692 was 8/8. Ops leftovers (Coolify migrate 20260829120000_pair_volume_24h_usd.sql, live PVol-5 list↔stats, light+dark /pool) tracked on #701. Do not reopen this issue for ops/QA.

## Merged !1195 is on `main` (`f4fa6788`). Local `make verify-issue-692` was 8/8. Ops leftovers (Coolify migrate `20260829120000_pair_volume_24h_usd.sql`, live PVol-5 list↔stats, light+dark `/pool`) tracked on #701. Do not reopen this issue for ops/QA.
PlasticDigits commented 2026-08-29 04:56:26 +00:00 (Migrated from gitlab.com)

mentioned in issue #693

mentioned in issue #693
PlasticDigits commented 2026-08-29 05:14:43 +00:00 (Migrated from gitlab.com)

mentioned in issue #702

mentioned in issue #702
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#692
No description provided.