fix: Charts 24h Vol and TWAP still compact-format raw mixed-decimal amounts (UST1 385.8M / USTR 36T) #564

Closed
opened 2026-08-18 12:12:13 +00:00 by PlasticDigits · 14 comments
PlasticDigits commented 2026-08-18 12:12:13 +00:00 (Migrated from gitlab.com)

Summary

Production /charts on UST1 / USTR (mainnet dex.cl8y.com) shows:

Control Displayed What it actually is
Vol (UST1) 385.8M Raw 6-dec integer passed to formatNum → ~385.8 human UST1 (15 trades ≈ 26 UST1/trade)
Vol (USTR) 36,113,437,940T Raw 18-dec integer compacted (T = 1e12). Human ≈ 36,113 USTR
TWAP 5m / 1h 111.009T On-chain raw reserve_b/reserve_a Decimal (token1 units / token0 units). Human ≈ 111 USTR per UST1 after × 10^(6−18)
TWAP 24h 94.9982T Same class
Trades 15 Fine
High/Low/Open/Close (USD) ~0.68–0.97 Factory USD of 1 human UST1 (#522); not the bug

This is not 385.8 million UST1 of flow. Pair-catalog vol badges already humanize volume_quote_24h (#534); Charts overview volume is USD-only (#548). The pair 24h Stats and TWAP Oracle panels were never converted and still call formatNum on raw integers / raw Decimal.

Related but out of scope: tape Amount in/out (#557).

Current codebase

24h Stats — formatNum on raw indexer sums

frontend-dapp/src/pages/ChartsPage.tsx (pair 24h Stats grid):

<StatBox label={`Vol (${activePair.asset_0.symbol})`} value={formatNum(stats.volume_base)} />
<StatBox label={`Vol (${activePair.asset_1.symbol})`} value={formatNum(stats.volume_quote)} />

USD OHLC uses formatNum(highUsd, 6) (magnitudes today are < 1e3 so no T, but it is still the compact formatter forbidden for prices in P522-5).

GET /api/v1/pairs/{addr}/stats (indexer/src/api/pairs.rs PairStatsResponse / get_24h_stats_for_pair):

  • volume_base / volume_quote = SUM of raw offer_amount / return_amount (orientation #466: base = asset_0, quote = asset_1).
  • volume_usd = SUM(swap_events.volume_usd) (P522-Q). Already returned by the API. Frontend IndexerPairStats omits the field, so Charts never shows pair 24h USD.
  • high / low / open_price / close_price = human quote-per-base after #522.
  • *_usd = USD of 1 human asset_0. Charts already prefers these via pairStatsUsdField.

formatNum is a human compact helper (K/M/B/T). Passing a raw 18-dec string is the documented anti-pattern:

Passing the raw 18-dec string to formatNum prints 19,297,048T for ordinary USTR flow (GitLab #534).

formatQuoteVolume24h / formatTokenAmount already scale by decimals. Pool table and pair pickers use them; Charts 24h Stats does not.

UST1 = 6 dec, USTR = 18 dec (tokenRegistry.ts; indexer asset_0.decimals / asset_1.decimals on the pair row).

TWAP Oracle — raw Decimal + formatNum

getTwapPrices queries pair observe and computeTwapPrice = (cum_end − cum_start) / dt / 1e18. That is CosmWasm Decimal of raw reserve_b / reserve_a (token1 base units per token0 base unit) — same units as on-chain limit price (#529 L529-1). Human quote-per-base is raw × 10^(decimals0 − decimals1).

Charts then:

value={entry?.price != null ? formatNum(entry.price, 6) : '—'}

On UST1(6)/USTR(18), raw ≈ 111 × 10^12 → 111.009T. After human scale ≈ 111.009 USTR per UST1, which is consistent with ~36k USTR / ~386 UST1 24h volume (ratio ~94) vs a 24h TWAP that can differ from volume-weighted spot.

The panel title uses factory indexerPairMenuLabel (UST1 / USTR), not the #524 invert pill. Keep TWAP factory human token1-per-token0 unless product later asks invert (do not 1/x of USD).

docs/twap-oracle.md still describes a geometric tick oracle; the live pair contract is arithmetic cumulative Decimal (smartcontracts/packages/dex-common/src/oracle.rs). Docs drift can be fixed in this issue.

Candle volume histogram — same raw integers

indexerCandlesToVolumeHistogramPoints plots parseFloat(volume_quote) else volume_base. Relative bar heights within a pair survive, but the volume axis is 1e6 / 1e18 too large. Same bug class on /trade + /charts PriceChart. C543-8 said volume is not inverted as price — it did not bless raw-vs-human display.

What is already correct (do not regress)

  • Charts overview 24h Volume (USD) (#548).
  • Pair picker / /pool vol badge formatQuoteVolume24h (#534).
  • Tape Price formatPairPrice (#522); tape amounts are #557.
  • Indexer JSON staying raw for volume_base / volume_quote / CG base_volume (integrators).

Why the new implementation is needed

Retail cannot tell real UST1/USTR flow from a unit bug. 385.8M UST1 next to 15 trades looks like wash or a broken market; 36T USTR and 111T TWAP next to ~$0.68–$0.97 USD makes the oracle look corrupted. The DEX already solved this class for prices, catalog volume, and overview USD. Charts pair stats is the remaining public surface on the same page as the screenshot.

Showing human token volumes plus pair 24h USD (stats.volume_usd, already indexed) is the same product pattern as overview (#548) + catalog (#534).

Constraints / guardrails

  1. Do not change indexer JSON units. volume_base / volume_quote stay raw integers (same as volume_quote_24h / CG). UI scales with asset_0.decimals / asset_1.decimals from the pair row (not symbol string matching — same A1 as #557).
  2. Do not formatNum raw 18-dec (or 6-dec) volume. Use formatTokenAmount / formatQuoteVolume24h. Compact K/M/B/T is allowed only on human sizes.
  3. Do not treat TWAP as USD. TWAP is human quote-per-base (factory token1 per token0). Use formatPairPrice, never compact T. Headline Price (USD) stays price_usd / invertUsd (#543).
  4. Human TWAP formula is raw × 10^(d0 − d1) — reuse rawLimitPriceToHuman / scaleDecimalStringByPow10, not Number × 10 ** (d0-d1) (float). On-chain oracle stays raw.
  5. Decimals source: indexer pair asset_*.decimals (0…18). Missing / out of range → —, never assume 6 (getDecimals default would make USTR look 1e12 too large again).
  6. Pair 24h USD: wire volume_usd on IndexerPairStats. Format with formatIndexedVolumeUsd(volume_usd, trade_count). Unpriced + trades > 0 → —. Idle 0 trades → $0. Do not invent USD from raw token volume × last price in the client.
  7. 24h Stats stay factory-oriented (heading already factory compact label). Do not 1/x USD OHLC or TWAP for the #524 invert pill in this issue (MAX/MIN USD do not invert cleanly). Invert remains chart + tape (#524 / #557).
  8. USD OHLC formatter: formatPairPrice (or $ + that), not formatNum. No compact T/B/M on High/Low/Open/Close (USD).
  9. Price change % stays API price_change_pct (from human open/close). Do not recompute from unscaled raw.
  10. Candle histogram: scale quote volume by quote decimals (base fallback by base decimals). Do not invert volume as price (C543-8). Relative heights within one series must stay proportional.
  11. #489: labels stay Vol (SYMBOL), TWAP 5m/1h/24h; tooltips ≤ one sentence (raw vs human). No token0 / base units in retail chrome.
  12. Display only. These figures are not settlement, limit price, or zap floors.
  13. Out of scope: #557 tape amounts; CG/CMC raw volumes; trader leaderboard (#553); changing CosmWasm oracle accumulation.

Relevant files

File Role
frontend-dapp/src/pages/ChartsPage.tsx 24h Stats + TWAP formatNum
frontend-dapp/src/pages/ChartsPage.test.tsx RTL — add mixed-decimal vol/TWAP cases
frontend-dapp/src/utils/formatAmount.ts formatTokenAmount, formatQuoteVolume24h, formatPairPrice
frontend-dapp/src/utils/chartsOverviewStats.ts formatIndexedVolumeUsd
frontend-dapp/src/utils/limitOrderPriceScale.ts Human scale 10^(d0−d1)
frontend-dapp/src/services/terraclassic/oracle.ts computeTwapPrice / getTwapPrices
frontend-dapp/src/types/index.ts IndexerPairStats missing volume_usd
frontend-dapp/src/components/charts/priceChartCandles.ts Histogram raw volume
frontend-dapp/src/components/charts/PriceChart.tsx Pass pair decimals into histogram
frontend-dapp/src/components/ui/StatBox.tsx Add data-testid on Charts boxes
indexer/src/api/pairs.rs Stats JSON (keep raw; volume_usd already present)
indexer/src/db/queries/swap_events.rs 24h SUM raw + volume_usd
smartcontracts/packages/dex-common/src/oracle.rs Raw Decimal TWAP (do not change units)
docs/frontend.md Charts pair stats / TWAP / histogram
docs/twap-oracle.md Arithmetic vs stale geometric copy
docs/indexer-invariants.md Pair stats volume human display
skills/AGENTS_FRONTEND_PAIR_CATALOG_RANK.md Cross-link: catalog vol ≠ Charts 24h Stats
  1. Helper (e.g. formatPairStatsVolume(raw, decimals)): formatTokenAmount; null/0/— policy documented. Unit tests: raw 385800000 + 6 → 385.8 class not 385.8M; raw 36113437940000000000000 + 18 → 36.1K class not 36,113,437,940T.
  2. Charts 24h Stats: Vol (asset_0) / Vol (asset_1) from that helper + pair decimals. Add Vol (USD) from stats.volume_usd via formatIndexedVolumeUsd. USD OHLC via formatPairPrice. data-testids: charts-pair-vol-base, charts-pair-vol-quote, charts-pair-vol-usd, charts-twap-5m, etc.
  3. TWAP: after computeTwapPrice, convert to decimal string and rawLimitPriceToHuman(raw, d0, d1) then formatPairPrice. Equal-decimal pairs unchanged (UST1/cUSTC ~1 class, not T). Failed observe stays — / “TWAP building…”.
  4. Histogram: indexerCandlesToVolumeHistogramPoints(..., { quoteDecimals, baseDecimals }) divides raw by 10^dec using string/BigInt then Number only if finite and in range; drop bar if overflow. Pass decimals from PriceChart pair query.
  5. Docs + skill + make verify-issue-<iid> (vitest + greps that ChartsPage 24h/TWAP no longer formatNum(stats.volume_* / formatNum(entry.price).
  6. Do not rewrite pair_volume_24h rollup or CG fields.

Acceptance criteria

  • S1. Charts UST1/USTR Vol (UST1) is human (hundreds, not 385.8M) for the production-scale raw 6-dec sum.
  • S2. Charts UST1/USTR Vol (USTR) is human (~tens of thousands, K OK) and never T unless human ≥ 1e12.
  • S3. Equal-decimal pair (UST1/cUSTC 6/6): human vols still readable; no extra 1e6 or 1e12 scale.
  • S4. Vol (USD) shown from indexer volume_usd; unpriced + trades > 0 → —; 0 trades → $0.
  • S5. TWAP 5m/1h/24h on UST1/USTR is human USTR per UST1 (111 class), not 111T. formatPairPrice only.
  • S6. Same-decimal TWAP unchanged in magnitude (no × 10^12 applied by mistake).
  • S7. High/Low/Open/Close (USD) never compact T/M; still factory *_usd (not tape price).
  • S8. Candle volume histogram uses human quote (else human base); invert still does not flip volume.
  • S9. Indexer stats JSON units unchanged; CG/CMC raw unchanged.
  • S10. RTL + make verify-issue-<iid>; docs/frontend.md pair-stats contract; docs/twap-oracle.md matches arithmetic oracle.
  • S11. Missing decimals or non-numeric volume/TWAP → —; no NaN / Infinity / crash.

Test plan — functional paths

Unit / RTL

  • formatPairStatsVolume: 6-dec 385800000 → not /M$/; 18-dec USTR screenshot-scale → not /T$/.
  • formatPairStatsVolume: 0, '', 'abc', negative → — or 0 (documented).
  • TWAP helper: raw 111.009e12 + (6,18) → 111.009 class; (6,6) identity; (18,6) scales the other way without T.
  • ChartsPage: mock UST1/USTR pair + stats matching screenshot raw strings → StatBox text satisfies S1/S2/S4/S5. Body must not match 36,113,437,940T or 111.009T.
  • ChartsPage: 6/6 pair vols 1000000/206000000 → 1 / 206 class.
  • ChartsPage: volume_usd: null, trade_count: 15 → Vol (USD) —; trade_count: 0, usd 0 → $0.
  • USD OHLC 0.682427 still prints as that class (not $682.4K / T).
  • priceChartCandles histogram: 18-dec quote 1e19 with 18 dec → value 10 class, not 1e19.
  • Quote vol 0, base vol used: scaled by base decimals, not quote.
  • Oracle observe failure: TWAP — + existing unavailable copy; stats still render.

Indexer (no unit change required unless types/docs)

  • Confirm GET /pairs/{addr}/stats still returns raw volume_base / volume_quote strings and volume_usd on a mixed-decimal fixture (existing swap ingest tests).
  • Do not add a “human volume” JSON field in this issue (avoids dual units).

Manual / production

  • https://dex.cl8y.com/charts UST1/USTR: Vol (UST1) hundreds-not-millions; Vol (USTR) no T; TWAP no T; USD OHLC still ~$0.7–$1 class.
  • UST1/cUSTC: vols human; TWAP ~1 class if reserves ~1:1 human.
  • /trade chart volume pane not 1e18 scale on UST1/USTR.
  • Overview 24h Volume (USD) still $ human (#548); catalog vol badges still #534.

Test plan — attack, hack, and abuse

Vector Expected
Decimal lie — UI uses getDecimals default 6 on USTR Forbidden. Use indexer pair-leg decimals; out of 0…18 → —.
Symbol matching — two CW20s named UST1 Decimals from activePair.asset_0/1 for that pair_address only.
TWAP as USD — show 111 in a control labeled USD Forbidden. Labels stay TWAP 5m/1h/24h.
Invert mix-up — 1/price_usd into TWAP or High (USD) Forbidden. TWAP = human token1/token0; USD OHLC = factory *_usd.
Client-invented USD — volume_quote / 1e18 × lastUsd Forbidden. Only indexer volume_usd.
XSS in volume_base / TWAP string Text nodes only; reject <> like overview USD cap if compact string is huge.
Huge raw string / BigInt overflow formatTokenAmount BigInt path; cap display length (overview A4 24-char class) → — rather than lock the tab.
Negative / non-finite TWAP —. computeTwapPrice already returns 0 on inverted cum; treat ≤ 0 as —.
Observation injection LCD observe is chain state; UI must not execute strings. No eval.
Histogram DoS — volume_quote = 1e309 Existing candle parse drops non-finite; keep that. Human scale must not reintroduce Infinity.
Misleading settlement Tooltip: 24h indexed volume / pair TWAP, not a quote or limit.
Wash-trade interpretation After the fix, 15 trades / ~386 UST1 is small flow — do not add a “manipulation” banner from volume alone (anomaly product is separate).

Verification criteria

make verify-issue-<iid>
# expected:
# - vitest: formatAmount (or new helper), ChartsPage 6/18 + 6/6 stats/TWAP, priceChartCandles histogram scale
# - grep ChartsPage: no formatNum(stats.volume_base) / formatNum(stats.volume_quote) / formatNum(entry.price
# - grep IndexerPairStats includes volume_usd
# - docs/frontend.md pair 24h stats + TWAP human contract
# - docs/twap-oracle.md arithmetic cumulative (not stale geometric-only)

Production check: UST1/USTR 24h Stats must not show 385.8M UST1 or …T USTR/TWAP for current mainnet raw magnitudes.

Out of scope

  • Tape Amount in / out / Price (#557).
  • Applying #524 invert to 24h Stats USD OHLC / TWAP.
  • Changing on-chain oracle or indexer raw stats / CG volumes.
  • Leaderboard / trader lifetime USD (#553).
  • Wash-trading detection from trade-count vs volume.
## Summary Production `/charts` on **UST1 / USTR** (mainnet `dex.cl8y.com`) shows: | Control | Displayed | What it actually is | |---------|-----------|---------------------| | **Vol (UST1)** | **385.8M** | Raw 6-dec integer passed to `formatNum` → ~**385.8 human UST1** (15 trades ≈ 26 UST1/trade) | | **Vol (USTR)** | **36,113,437,940T** | Raw **18-dec** integer compacted (`T` = 1e12). Human ≈ **36,113 USTR** | | **TWAP 5m / 1h** | **111.009T** | On-chain raw `reserve_b/reserve_a` Decimal (token1 units / token0 units). Human ≈ **111 USTR per UST1** after `× 10^(6−18)` | | **TWAP 24h** | **94.9982T** | Same class | | **Trades** | 15 | Fine | | **High/Low/Open/Close (USD)** | ~0.68–0.97 | Factory USD of 1 human UST1 ([#522](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/522)); not the bug | This is **not** 385.8 million UST1 of flow. Pair-catalog vol badges already humanize `volume_quote_24h` ([#534](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/534)); Charts **overview** volume is USD-only ([#548](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/548)). The **pair 24h Stats** and **TWAP Oracle** panels were never converted and still call `formatNum` on raw integers / raw Decimal. Related but **out of scope:** tape Amount in/out ([#557](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/557)). ## Current codebase ### 24h Stats — `formatNum` on raw indexer sums [`frontend-dapp/src/pages/ChartsPage.tsx`](frontend-dapp/src/pages/ChartsPage.tsx) (pair 24h Stats grid): ```ts <StatBox label={`Vol (${activePair.asset_0.symbol})`} value={formatNum(stats.volume_base)} /> <StatBox label={`Vol (${activePair.asset_1.symbol})`} value={formatNum(stats.volume_quote)} /> ``` USD OHLC uses `formatNum(highUsd, 6)` (magnitudes today are `< 1e3` so no `T`, but it is still the compact formatter forbidden for prices in **P522-5**). `GET /api/v1/pairs/{addr}/stats` ([`indexer/src/api/pairs.rs`](indexer/src/api/pairs.rs) `PairStatsResponse` / [`get_24h_stats_for_pair`](indexer/src/db/queries/swap_events.rs)): - `volume_base` / `volume_quote` = `SUM` of **raw** `offer_amount` / `return_amount` (orientation #466: base = asset_0, quote = asset_1). - `volume_usd` = `SUM(swap_events.volume_usd)` (P522-Q). **Already returned by the API.** Frontend `IndexerPairStats` **omits** the field, so Charts never shows pair 24h USD. - `high` / `low` / `open_price` / `close_price` = human quote-per-base after #522. - `*_usd` = USD of 1 human `asset_0`. Charts already prefers these via `pairStatsUsdField`. [`formatNum`](frontend-dapp/src/utils/formatAmount.ts) is a **human compact** helper (`K`/`M`/`B`/`T`). Passing a raw 18-dec string is the documented anti-pattern: > Passing the raw 18-dec string to `formatNum` prints `19,297,048T` for ordinary USTR flow (GitLab #534). `formatQuoteVolume24h` / `formatTokenAmount` already scale by decimals. Pool table and pair pickers use them; **Charts 24h Stats does not**. UST1 = **6** dec, USTR = **18** dec ([`tokenRegistry.ts`](frontend-dapp/src/utils/tokenRegistry.ts); indexer `asset_0.decimals` / `asset_1.decimals` on the pair row). ### TWAP Oracle — raw Decimal + `formatNum` [`getTwapPrices`](frontend-dapp/src/services/terraclassic/oracle.ts) queries pair `observe` and `computeTwapPrice` = `(cum_end − cum_start) / dt / 1e18`. That is CosmWasm `Decimal` of **raw** `reserve_b / reserve_a` (token1 **base units** per token0 **base unit**) — same units as on-chain limit `price` ([#529](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/529) **L529-1**). Human quote-per-base is `raw × 10^(decimals0 − decimals1)`. Charts then: ```ts value={entry?.price != null ? formatNum(entry.price, 6) : '—'} ``` On UST1(6)/USTR(18), raw ≈ `111 × 10^12` → **111.009T**. After human scale ≈ **111.009** USTR per UST1, which is consistent with ~36k USTR / ~386 UST1 24h volume (ratio ~94) vs a 24h TWAP that can differ from volume-weighted spot. The panel title uses factory `indexerPairMenuLabel` (**UST1 / USTR**), not the #524 invert pill. Keep TWAP **factory** human token1-per-token0 unless product later asks invert (do **not** `1/x` of USD). `docs/twap-oracle.md` still describes a geometric tick oracle; the live pair contract is **arithmetic** cumulative Decimal ([`smartcontracts/packages/dex-common/src/oracle.rs`](smartcontracts/packages/dex-common/src/oracle.rs)). Docs drift can be fixed in this issue. ### Candle volume histogram — same raw integers [`indexerCandlesToVolumeHistogramPoints`](frontend-dapp/src/components/charts/priceChartCandles.ts) plots `parseFloat(volume_quote)` else `volume_base`. Relative **bar heights within a pair** survive, but the volume axis is 1e6 / 1e18 too large. Same bug class on `/trade` + `/charts` `PriceChart`. **C543-8** said volume is not inverted as price — it did **not** bless raw-vs-human display. ### What is already correct (do not regress) - Charts overview **24h Volume (USD)** ([#548](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/548)). - Pair picker / `/pool` vol badge `formatQuoteVolume24h` ([#534](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/534)). - Tape **Price** `formatPairPrice` ([#522](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/522)); tape **amounts** are [#557](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/557). - Indexer JSON staying **raw** for `volume_base` / `volume_quote` / CG `base_volume` (integrators). ## Why the new implementation is needed Retail cannot tell real UST1/USTR flow from a unit bug. 385.8M UST1 next to 15 trades looks like wash or a broken market; 36T USTR and 111T TWAP next to ~$0.68–$0.97 USD makes the oracle look corrupted. The DEX already solved this class for prices, catalog volume, and overview USD. Charts pair stats is the remaining public surface on the same page as the screenshot. Showing **human token volumes** plus **pair 24h USD** (`stats.volume_usd`, already indexed) is the same product pattern as overview (#548) + catalog (#534). ## Constraints / guardrails 1. **Do not change indexer JSON units.** `volume_base` / `volume_quote` stay raw integers (same as `volume_quote_24h` / CG). UI scales with `asset_0.decimals` / `asset_1.decimals` from the **pair row** (not symbol string matching — same **A1** as #557). 2. **Do not `formatNum` raw 18-dec (or 6-dec) volume.** Use `formatTokenAmount` / `formatQuoteVolume24h`. Compact `K`/`M`/`B`/`T` is allowed only on **human** sizes. 3. **Do not treat TWAP as USD.** TWAP is human quote-per-base (factory token1 per token0). Use `formatPairPrice`, never compact `T`. Headline **Price (USD)** stays `price_usd` / invertUsd ([#543](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/543)). 4. **Human TWAP formula** is `raw × 10^(d0 − d1)` — reuse [`rawLimitPriceToHuman`](frontend-dapp/src/utils/limitOrderPriceScale.ts) / `scaleDecimalStringByPow10`, not `Number` × `10 ** (d0-d1)` (float). On-chain oracle stays raw. 5. **Decimals source:** indexer pair `asset_*.decimals` (0…18). Missing / out of range → `—`, never assume 6 (`getDecimals` default would make USTR look 1e12 too large again). 6. **Pair 24h USD:** wire `volume_usd` on `IndexerPairStats`. Format with `formatIndexedVolumeUsd(volume_usd, trade_count)`. Unpriced + trades > 0 → `—`. Idle 0 trades → `$0`. Do **not** invent USD from raw token volume × last price in the client. 7. **24h Stats stay factory-oriented** (heading already factory compact label). Do **not** `1/x` USD OHLC or TWAP for the #524 invert pill in this issue (MAX/MIN USD do not invert cleanly). Invert remains chart + tape (#524 / #557). 8. **USD OHLC formatter:** `formatPairPrice` (or `$` + that), not `formatNum`. No compact `T`/`B`/`M` on High/Low/Open/Close (USD). 9. **Price change %** stays API `price_change_pct` (from human open/close). Do not recompute from unscaled raw. 10. **Candle histogram:** scale quote volume by quote decimals (base fallback by base decimals). Do not invert volume as price (**C543-8**). Relative heights within one series must stay proportional. 11. **#489:** labels stay **Vol (SYMBOL)**, **TWAP 5m/1h/24h**; tooltips ≤ one sentence (raw vs human). No `token0` / `base units` in retail chrome. 12. **Display only.** These figures are not settlement, limit price, or zap floors. 13. **Out of scope:** [#557](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/557) tape amounts; CG/CMC raw volumes; trader leaderboard ([#553](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/553)); changing CosmWasm oracle accumulation. ## Relevant files | File | Role | |------|------| | [`frontend-dapp/src/pages/ChartsPage.tsx`](frontend-dapp/src/pages/ChartsPage.tsx) | 24h Stats + TWAP `formatNum` | | [`frontend-dapp/src/pages/ChartsPage.test.tsx`](frontend-dapp/src/pages/ChartsPage.test.tsx) | RTL — add mixed-decimal vol/TWAP cases | | [`frontend-dapp/src/utils/formatAmount.ts`](frontend-dapp/src/utils/formatAmount.ts) | `formatTokenAmount`, `formatQuoteVolume24h`, `formatPairPrice` | | [`frontend-dapp/src/utils/chartsOverviewStats.ts`](frontend-dapp/src/utils/chartsOverviewStats.ts) | `formatIndexedVolumeUsd` | | [`frontend-dapp/src/utils/limitOrderPriceScale.ts`](frontend-dapp/src/utils/limitOrderPriceScale.ts) | Human scale `10^(d0−d1)` | | [`frontend-dapp/src/services/terraclassic/oracle.ts`](frontend-dapp/src/services/terraclassic/oracle.ts) | `computeTwapPrice` / `getTwapPrices` | | [`frontend-dapp/src/types/index.ts`](frontend-dapp/src/types/index.ts) | `IndexerPairStats` missing `volume_usd` | | [`frontend-dapp/src/components/charts/priceChartCandles.ts`](frontend-dapp/src/components/charts/priceChartCandles.ts) | Histogram raw volume | | [`frontend-dapp/src/components/charts/PriceChart.tsx`](frontend-dapp/src/components/charts/PriceChart.tsx) | Pass pair decimals into histogram | | [`frontend-dapp/src/components/ui/StatBox.tsx`](frontend-dapp/src/components/ui/StatBox.tsx) | Add `data-testid` on Charts boxes | | [`indexer/src/api/pairs.rs`](indexer/src/api/pairs.rs) | Stats JSON (keep raw; `volume_usd` already present) | | [`indexer/src/db/queries/swap_events.rs`](indexer/src/db/queries/swap_events.rs) | 24h SUM raw + `volume_usd` | | [`smartcontracts/packages/dex-common/src/oracle.rs`](smartcontracts/packages/dex-common/src/oracle.rs) | Raw Decimal TWAP (do not change units) | | [`docs/frontend.md`](docs/frontend.md) | Charts pair stats / TWAP / histogram | | [`docs/twap-oracle.md`](docs/twap-oracle.md) | Arithmetic vs stale geometric copy | | [`docs/indexer-invariants.md`](docs/indexer-invariants.md) | Pair stats volume human display | | [`skills/AGENTS_FRONTEND_PAIR_CATALOG_RANK.md`](skills/AGENTS_FRONTEND_PAIR_CATALOG_RANK.md) | Cross-link: catalog vol ≠ Charts 24h Stats | ## Recommended direction 1. **Helper** (e.g. `formatPairStatsVolume(raw, decimals)`): `formatTokenAmount`; `null`/0/`—` policy documented. Unit tests: raw `385800000` + 6 → `385.8` class **not** `385.8M`; raw `36113437940000000000000` + 18 → `36.1K` class **not** `36,113,437,940T`. 2. **Charts 24h Stats:** Vol (asset_0) / Vol (asset_1) from that helper + pair decimals. Add **Vol (USD)** from `stats.volume_usd` via `formatIndexedVolumeUsd`. USD OHLC via `formatPairPrice`. `data-testid`s: `charts-pair-vol-base`, `charts-pair-vol-quote`, `charts-pair-vol-usd`, `charts-twap-5m`, etc. 3. **TWAP:** after `computeTwapPrice`, convert to decimal string and `rawLimitPriceToHuman(raw, d0, d1)` then `formatPairPrice`. Equal-decimal pairs unchanged (UST1/cUSTC ~1 class, not `T`). Failed observe stays `—` / “TWAP building…”. 4. **Histogram:** `indexerCandlesToVolumeHistogramPoints(..., { quoteDecimals, baseDecimals })` divides raw by `10^dec` using string/BigInt then `Number` only if finite and in range; drop bar if overflow. Pass decimals from `PriceChart` pair query. 5. **Docs + skill** + `make verify-issue-<iid>` (vitest + greps that ChartsPage 24h/TWAP no longer `formatNum(stats.volume_*` / `formatNum(entry.price`). 6. **Do not** rewrite `pair_volume_24h` rollup or CG fields. ## Acceptance criteria - [ ] **S1.** Charts UST1/USTR **Vol (UST1)** is human (hundreds, not `385.8M`) for the production-scale raw 6-dec sum. - [ ] **S2.** Charts UST1/USTR **Vol (USTR)** is human (~tens of thousands, `K` OK) and **never** `T` unless human ≥ 1e12. - [ ] **S3.** Equal-decimal pair (UST1/cUSTC 6/6): human vols still readable; no extra 1e6 or 1e12 scale. - [ ] **S4.** **Vol (USD)** shown from indexer `volume_usd`; unpriced + trades > 0 → `—`; 0 trades → `$0`. - [ ] **S5.** TWAP 5m/1h/24h on UST1/USTR is human USTR per UST1 (`111` class), **not** `111T`. `formatPairPrice` only. - [ ] **S6.** Same-decimal TWAP unchanged in magnitude (no `× 10^12` applied by mistake). - [ ] **S7.** High/Low/Open/Close (USD) never compact `T`/`M`; still factory `*_usd` (not tape `price`). - [ ] **S8.** Candle volume histogram uses human quote (else human base); invert still does not flip volume. - [ ] **S9.** Indexer stats JSON units unchanged; CG/CMC raw unchanged. - [ ] **S10.** RTL + `make verify-issue-<iid>`; `docs/frontend.md` pair-stats contract; `docs/twap-oracle.md` matches arithmetic oracle. - [ ] **S11.** Missing decimals or non-numeric volume/TWAP → `—`; no `NaN` / `Infinity` / crash. ## Test plan — functional paths ### Unit / RTL - [ ] `formatPairStatsVolume`: 6-dec `385800000` → not `/M$/`; 18-dec USTR screenshot-scale → not `/T$/`. - [ ] `formatPairStatsVolume`: `0`, `''`, `'abc'`, negative → `—` or `0` (documented). - [ ] TWAP helper: raw `111.009e12` + (6,18) → `111.009` class; (6,6) identity; (18,6) scales the other way without `T`. - [ ] ChartsPage: mock UST1/USTR pair + stats matching screenshot raw strings → StatBox text satisfies S1/S2/S4/S5. Body must not match `36,113,437,940T` or `111.009T`. - [ ] ChartsPage: 6/6 pair vols `1000000`/`206000000` → `1` / `206` class. - [ ] ChartsPage: `volume_usd: null`, `trade_count: 15` → Vol (USD) `—`; `trade_count: 0`, usd `0` → `$0`. - [ ] USD OHLC `0.682427` still prints as that class (not `$682.4K` / `T`). - [ ] `priceChartCandles` histogram: 18-dec quote `1e19` with 18 dec → value `10` class, not `1e19`. - [ ] Quote vol `0`, base vol used: scaled by **base** decimals, not quote. - [ ] Oracle observe failure: TWAP `—` + existing unavailable copy; stats still render. ### Indexer (no unit change required unless types/docs) - [ ] Confirm `GET /pairs/{addr}/stats` still returns raw `volume_base` / `volume_quote` strings and `volume_usd` on a mixed-decimal fixture (existing swap ingest tests). - [ ] Do not add a “human volume” JSON field in this issue (avoids dual units). ### Manual / production - [ ] `https://dex.cl8y.com/charts` UST1/USTR: Vol (UST1) hundreds-not-millions; Vol (USTR) no `T`; TWAP no `T`; USD OHLC still ~$0.7–$1 class. - [ ] UST1/cUSTC: vols human; TWAP ~1 class if reserves ~1:1 human. - [ ] `/trade` chart volume pane not 1e18 scale on UST1/USTR. - [ ] Overview 24h Volume (USD) still `$` human ([#548](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/548)); catalog vol badges still #534. ## Test plan — attack, hack, and abuse | Vector | Expected | |--------|----------| | **Decimal lie** — UI uses `getDecimals` default 6 on USTR | Forbidden. Use indexer pair-leg decimals; out of `0…18` → `—`. | | **Symbol matching** — two CW20s named UST1 | Decimals from `activePair.asset_0/1` for **that** `pair_address` only. | | **TWAP as USD** — show `111` in a control labeled USD | Forbidden. Labels stay **TWAP 5m/1h/24h**. | | **Invert mix-up** — `1/price_usd` into TWAP or High (USD) | Forbidden. TWAP = human token1/token0; USD OHLC = factory `*_usd`. | | **Client-invented USD** — `volume_quote / 1e18 × lastUsd` | Forbidden. Only indexer `volume_usd`. | | **XSS** in `volume_base` / TWAP string | Text nodes only; reject `<>` like overview USD cap if compact string is huge. | | **Huge raw string / BigInt overflow** | `formatTokenAmount` BigInt path; cap display length (overview **A4** 24-char class) → `—` rather than lock the tab. | | **Negative / non-finite TWAP** | `—`. `computeTwapPrice` already returns 0 on inverted cum; treat `≤ 0` as `—`. | | **Observation injection** | LCD `observe` is chain state; UI must not execute strings. No `eval`. | | **Histogram DoS** — `volume_quote` = `1e309` | Existing candle parse drops non-finite; keep that. Human scale must not reintroduce `Infinity`. | | **Misleading settlement** | Tooltip: 24h indexed volume / pair TWAP, not a quote or limit. | | **Wash-trade interpretation** | After the fix, 15 trades / ~386 UST1 is small flow — do not add a “manipulation” banner from volume alone (anomaly product is separate). | ## Verification criteria ```bash make verify-issue-<iid> # expected: # - vitest: formatAmount (or new helper), ChartsPage 6/18 + 6/6 stats/TWAP, priceChartCandles histogram scale # - grep ChartsPage: no formatNum(stats.volume_base) / formatNum(stats.volume_quote) / formatNum(entry.price # - grep IndexerPairStats includes volume_usd # - docs/frontend.md pair 24h stats + TWAP human contract # - docs/twap-oracle.md arithmetic cumulative (not stale geometric-only) ``` Production check: UST1/USTR 24h Stats must not show `385.8M` UST1 or `…T` USTR/TWAP for current mainnet raw magnitudes. ## Out of scope - Tape Amount in / out / Price ([#557](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/557)). - Applying #524 invert to 24h Stats USD OHLC / TWAP. - Changing on-chain oracle or indexer raw stats / CG volumes. - Leaderboard / trader lifetime USD ([#553](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/553)). - Wash-trading detection from trade-count vs volume.
PlasticDigits commented 2026-08-18 12:12:15 +00:00 (Migrated from gitlab.com)

marked as related to #557

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

mentioned in merge request !1088

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

mentioned in commit e9a6270966

mentioned in commit e9a627096668d4f963a9ebc4ef1a1c9687c5946f
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-19 02:05:21 +00:00
PlasticDigits commented 2026-08-19 02:12:07 +00:00 (Migrated from gitlab.com)

mentioned in commit 975e062004

mentioned in commit 975e0620049bcb7f66cd5ad09d970d5dc00efeef
PlasticDigits commented 2026-08-19 02:16:47 +00:00 (Migrated from gitlab.com)

Post-merge note (!1088 on main, then combined with !1089 / #565). TWAP human quote-per-base, USD OHLC via formatPairStatsUsdOhlc, and candle histogram decimal scaling are on main. Volume strip layout follows #565 (Vol USD primary + token row; testids charts-pair-volume-*).

Remaining:

  • Manual: /charts UST1/USTR Vol hundreds-not-millions / no T on TWAP; /trade volume pane not 1e18.
  • CI was not waited. Run make verify-issue-564 and make verify-issue-565 together.
**Post-merge note (!1088 on main, then combined with !1089 / #565).** TWAP human quote-per-base, USD OHLC via `formatPairStatsUsdOhlc`, and candle histogram decimal scaling are on `main`. Volume strip layout follows **#565** (Vol USD primary + token row; testids `charts-pair-volume-*`). Remaining: - Manual: `/charts` UST1/USTR Vol hundreds-not-millions / no `T` on TWAP; `/trade` volume pane not 1e18. - CI was not waited. Run `make verify-issue-564` and `make verify-issue-565` together.
PlasticDigits commented 2026-08-19 02:16:56 +00:00 (Migrated from gitlab.com)

mentioned in issue #565

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

mentioned in commit 4f24107fa7

mentioned in commit 4f24107fa7b1a2e1fcb93bea0500c9590f71ce1f
PlasticDigits commented 2026-08-19 02:28:44 +00:00 (Migrated from gitlab.com)

Sanity after stack: make verify-issue-564 initially failed because the grep still expected formatPairStatsVolume / charts-pair-vol-* after combining with #565 (formatChartsPairTokenVolume / charts-pair-volume-*). Fixed on main in 4f24107f. Re-run is 3/3 PASS. Remaining work is production Charts smoke (UST1/USTR TWAP not T).

**Sanity after stack:** `make verify-issue-564` initially failed because the grep still expected `formatPairStatsVolume` / `charts-pair-vol-*` after combining with #565 (`formatChartsPairTokenVolume` / `charts-pair-volume-*`). Fixed on `main` in `4f24107f`. Re-run is **3/3 PASS**. Remaining work is production Charts smoke (UST1/USTR TWAP not `T`).
PlasticDigits commented 2026-08-19 02:28:45 +00:00 (Migrated from gitlab.com)

mentioned in issue #573

mentioned in issue #573
PlasticDigits commented 2026-08-19 02:28:59 +00:00 (Migrated from gitlab.com)

marked as related to #573

marked as related to #573
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:17:24 +00:00 (Migrated from gitlab.com)

marked as related to #666

marked as related to #666
PlasticDigits commented 2026-08-27 00:20:53 +00:00 (Migrated from gitlab.com)

mentioned in issue #680

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

mentioned in issue #692

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