fix: humanize Amount in / Amount out / Price on Charts, Trade, Trader tape #557

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

Summary

Amount in, Amount out, and Price on the public tape (and the same table on Trade / Trader / Portfolio / wallet history) still pass raw chain integers through formatNum. On UST1/cUSTC (6/6) a 1-token swap prints as 1.000M / 206.0M. On UST1/USTR (6/18) outflows print as T. Retail must see human token amounts and a human quote-per-base Price that matches the pair (and display invert when the rest of the page is inverted).

This is display scale, not USD oracle. Hub DEX USD for Charts Price (USD) is #556. Related: #522 (tape Price must not compact as T; still human not USD), #534 (formatQuoteVolume24h), #548 / #553 (same formatNum(raw) class), #551 (portfolio P&L — out of scope here), #524 (pair invert). Wallet history comment already defers decimal-aware format to “later (#479)”.

Current codebase

Shared tape: TradesTable

frontend-dapp/src/components/ui/TradesTable.tsx:

  • Amount in = formatNum(t.offer_amount)
  • Amount out = formatNum(t.return_amount)
  • Price = formatPairPrice(t.price, 6) with tooltip “Human quote per base … Not USD”

formatNum is a human compact formatter (K/M/B/T). Indexer offer_amount / return_amount are raw integers (TradeResponse). formatTokenAmount(raw, decimals) already exists and is the correct helper ("1000000" + 6 → "1").

Used on:

Surface File activePair passed?
/charts/:pair Recent Trades ChartsPage.tsx yes
/trade/:pair Recent trades TradeRecentTradesSection.tsx yes
/trader/:addr TraderPage.tsx no (mixed pairs)
/portfolio activity PortfolioPage.tsx no

Wallet pair history

WalletIndexerHistoryPanel.tsx formatHistoryAmount = formatNum(raw) on purpose, with a comment that formatTokenAmount waits on decimals. Same bug on swap and limit fill token0_amount / token1_amount.

Indexer trade JSON

IndexerTrade / TradeResponse expose offer_asset / ask_asset symbols and raw amounts. No offer_decimals / ask_decimals. Trader/Portfolio mixed-pair tables cannot humanize from activePair. Matching decimals by symbol string is unsafe (A1 spoof / two CW20s with the same ticker).

Price after #522 is already human quote-per-base. On UST1/cUSTC that is 206 cUSTC per UST1, which looks “wrong” next to Amount in 1.000M and next to chart Price (USD) ($1 factory or ~$0.00x inverted). Two bugs stack: amounts are 10^decimals too large, and tape Price does not follow the #524 invert pill (chart says cUSTC/UST1 while tape Price stays UST1-as-base).

Limit fills on the same history panel are raw token0/token1 (6 vs 18 on UST1/USTR → T).

Why the new implementation is needed

Retail cannot read the tape. UST1/cUSTC is the primary economic market; showing millions for single-digit swaps is the same class of bug that #522 / #534 / #548 already fixed for USD and volume. Trade and Trader reuse the same component, so Charts-only patches will drift.

Constraints / guardrails

  1. Do not treat tape Price as USD. Headline Price (USD) stays price_usd / invertUsd. Tape Price stays human quote-per-base (or the display reciprocal when inverted). Never formatNum compact T on Price (#522).
  2. Humanize with the offer/ask asset’s decimals, not a hardcoded 1e6. USTR is 18; UST1/cUSTC are 6.
  3. Do not match decimals by symbol alone. Prefer API fields offer_decimals / ask_decimals (and fill token0_decimals / token1_decimals). Pair-scoped UI may use activePair as a fallback only when pair_address matches and both symbols match that pair’s two legs.
  4. JSON may keep raw integers (integrators / CSV). UI must scale. Do not break CSV columns (#479).
  5. Invert: when /trade or /charts display invert is on, tape Price must be the reciprocal human price (display quote per display base) or the column header must stay explicit factory orientation. Prefer matching the pill (cUSTC per UST1 vs UST1 per cUSTC). Amount in/out stay pay → receive (offer → ask) and must not be swapped by invert — invert is display price, not fill direction.
  6. Hybrid splits (pool_return_amount / book_return_amount) stay raw in JSON; if shown, same decimal as ask. Do not sum legs into Amount out.
  7. Zero / missing / non-numeric → 0 or — (pick one per column, document). No NaN, no Infinity.
  8. Do not use tape amounts for settlement. Display only.
  9. #489: keep column titles Amount in / Amount out / Price; tooltips ≤ one sentence.
  10. Out of scope: leaderboard volume (#553), portfolio P&L totals (#551), hub DEX USD catalog (sibling issue), CG/CMC.

Relevant files

File Role
frontend-dapp/src/components/ui/TradesTable.tsx Shared tape
frontend-dapp/src/components/ui/__tests__/TradesTable.test.tsx RTL
frontend-dapp/src/utils/formatAmount.ts formatTokenAmount / formatPairPrice / formatNum
frontend-dapp/src/components/trade/WalletIndexerHistoryPanel.tsx Pair wallet history
frontend-dapp/src/pages/ChartsPage.tsx Charts tape
frontend-dapp/src/components/trade/TradeRecentTradesSection.tsx Trade tape
frontend-dapp/src/pages/TraderPage.tsx Mixed-pair history
frontend-dapp/src/pages/PortfolioPage.tsx Same table
frontend-dapp/src/types/index.ts IndexerTrade
indexer/src/api/pairs.rs TradeResponse
indexer/src/api/text_csv.rs CSV — keep raw unless additive columns
frontend-dapp/src/utils/tradePairDisplayOrientation.ts Invert math
docs/frontend.md Charts / Trade tape notes
  1. Indexer (required for mixed-pair pages): additive offer_decimals, ask_decimals on trade JSON (from offer/ask assets.decimals). Same for limit-fill rows if the wallet panel shows them. CSV: additive columns OK; do not rewrite existing raw columns (breaking integrators).
  2. Shared formatter: e.g. formatTradeAmount(raw, decimals) → formatTokenAmount. TradesTable uses trade decimals when present, else pair-leg fallback when activePair.pair_address === t.pair_address.
  3. Price: formatPairPrice(human, 6). If inverted prop is true, show 1/human only when finite and > 0 (reuse invert helpers; never 1/x of price_usd). Optional subtitle in tooltip: {displayQuote} per {displayBase}.
  4. Pass inverted from Charts/Trade via existing usePairDisplayOrientation. Trader/Portfolio: no invert (mixed pairs) — factory human price + human amounts via API decimals.
  5. Wallet history: thread pair decimals from the pair query already on /trade / /limits; stop formatNum(raw).
  6. Tests: UST1/cUSTC 1e6/2.06e8 → 1 and 206 (compact OK for large human sizes only). UST1/USTR 1e6 / 1e19 → 1 and 10 not T. Pre-#522 raw price still must not show T if a legacy indexer appears (keep existing test; human 79.72 prints as 79.72).

Acceptance criteria

  • A1. Charts UST1/cUSTC tape: Amount in/out are human (1 UST1 → 1 class, not 1.000M).
  • A2. Charts UST1/USTR tape: 18-dec amounts never compact as T unless the human size is actually ≥ 1e12.
  • A3. Trade page tape matches Charts for the same pair.
  • A4. Trader + Portfolio mixed history humanize per trade decimals (not one pair’s decimals for all rows).
  • A5. Wallet history Amount in/out/Price (and fills) humanized.
  • A6. Tape Price is human quote-per-base (formatPairPrice); never USD; never T from raw 18/6.
  • A7. When Charts/Trade invert is on, tape Price orientation matches the pill; Amount in/out stay offer→ask.
  • A8. Integrator JSON/CSV raw amounts unchanged (additive decimals allowed).
  • A9. RTL + make verify-issue-<iid>; docs mention the contract.

Test plan — functional paths

Indexer

  • Trade JSON includes decimals matching offer/ask assets for 6/6, 6/18, 18/6.
  • Missing asset decimals fail safe (omit field / skip) — UI falls back without crashing.
  • CSV still parses; existing columns raw; new columns documented.
  • Trader trades endpoint same shape as pair trades.

Frontend RTL / unit

  • TradesTable: UST1/cUSTC raw 1000000 / 206000000 / price 206 → human in/out, price 206 (or inverted 0.00x class when inverted).
  • TradesTable: USTR raw 10000000000000000000 + 18 dec → 10 (example), not 10.00E / T.
  • No decimals + no matching pair → — or documented raw fallback without T for typical hub sizes (prefer — over lying).
  • Invert: amounts unchanged; price reciprocal; ≤0 / non-finite price → —.
  • Hybrid badge still renders; Amount out is full return_amount humanized, not a raw split.
  • WalletIndexerHistoryPanel: swap + fill rows humanized; CSV download still raw file from API.
  • ChartsPage / TradeRecentTradesSection pass invert flag.
  • TraderPage without activePair still humanizes via trade decimals.

E2E / manual

  • https://dex.cl8y.com/charts UST1/cUSTC Recent Trades vs explorer/LCD raw ÷ 10^6.
  • Same pair on /trade.
  • Toggle invert: Last USD and tape Price stay consistent with pill; amounts still pay→receive.
  • /trader/{addr} with mixed UST1/cUSTC and UST1/USTR rows.
  • LocalTerra: make verify-issue-<iid> after frontend unit tests.

Test plan — attack, hack, and abuse

Vector Expected
Decimal lie — trade JSON offer_decimals=0 on an 18-dec USTR to inflate UI Decimals must come from indexed assets row for that offer asset id, not from the wasm event. Ignore/clamp out-of-range (<0 or >38) → —.
Symbol collision — two CW20s both UST1 Decimals keyed by asset id / pair legs, not offer_asset string.
Invert + USD mix-up — UI shows 1/price_usd in Price column Forbidden. Reciprocal of human price only.
XSS in asset symbol Existing text render; do not dangerouslySetInnerHTML.
Huge raw string formatTokenAmount BigInt path; no crash; compact human if huge.
Negative / zero / empty amounts 0 or —; no negative compact T.
CSV injection CSV stays as today (leading-equals etc. already an integrator concern); do not pass CSV through formatNum in the file.
Pair mismatch — activePair decimals applied to another pair’s row Only when t.pair_address matches; otherwise API decimals or —.
DoS via giant decimals field i16 from DB; reject absurd values.
Misleading “Price” as executable Tooltip: not a limit/swap instruction; not USD.

Verification criteria

make verify-issue-<iid>
# expected:
# - vitest TradesTable, WalletIndexerHistoryPanel, formatAmount
# - indexer trade JSON decimals tests
# - grep TradesTable: no formatNum(t.offer_amount) / formatNum(t.return_amount)
# - grep WalletIndexerHistoryPanel: no formatNum(raw) for amounts
# - docs/frontend.md tape contract

Out of scope

  • DEX hub USD / Protocol USTR (#556).
  • #553 leaderboard Volume.
  • #551 Portfolio P&L / cost basis.
  • Changing on-chain events or swap settlement.
## Summary **Amount in**, **Amount out**, and **Price** on the public tape (and the same table on Trade / Trader / Portfolio / wallet history) still pass **raw chain integers** through `formatNum`. On UST1/cUSTC (6/6) a 1-token swap prints as **`1.000M` / `206.0M`**. On UST1/USTR (6/18) outflows print as **`T`**. Retail must see **human token amounts** and a **human quote-per-base Price** that matches the pair (and display invert when the rest of the page is inverted). This is **display scale**, not USD oracle. Hub DEX USD for Charts **Price (USD)** is **[#556](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/556)**. Related: [#522](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/522) (tape Price must not compact as `T`; still human not USD), [#534](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/534) (`formatQuoteVolume24h`), [#548](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/548) / [#553](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/553) (same `formatNum(raw)` class), [#551](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/551) (portfolio P&amp;L — **out of scope** here), [#524](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/524) (pair invert). Wallet history comment already defers decimal-aware format to “later (#479)”. ## Current codebase ### Shared tape: `TradesTable` [`frontend-dapp/src/components/ui/TradesTable.tsx`](frontend-dapp/src/components/ui/TradesTable.tsx): - **Amount in** = `formatNum(t.offer_amount)` - **Amount out** = `formatNum(t.return_amount)` - **Price** = `formatPairPrice(t.price, 6)` with tooltip “Human quote per base … Not USD” [`formatNum`](frontend-dapp/src/utils/formatAmount.ts) is a **human compact** formatter (`K`/`M`/`B`/`T`). Indexer `offer_amount` / `return_amount` are **raw integers** ([`TradeResponse`](indexer/src/api/pairs.rs)). `formatTokenAmount(raw, decimals)` already exists and is the correct helper (`"1000000"` + 6 → `"1"`). Used on: | Surface | File | `activePair` passed? | |---------|------|----------------------| | `/charts/:pair` Recent Trades | [`ChartsPage.tsx`](frontend-dapp/src/pages/ChartsPage.tsx) | yes | | `/trade/:pair` Recent trades | [`TradeRecentTradesSection.tsx`](frontend-dapp/src/components/trade/TradeRecentTradesSection.tsx) | yes | | `/trader/:addr` | [`TraderPage.tsx`](frontend-dapp/src/pages/TraderPage.tsx) | **no** (mixed pairs) | | `/portfolio` activity | [`PortfolioPage.tsx`](frontend-dapp/src/pages/PortfolioPage.tsx) | **no** | ### Wallet pair history [`WalletIndexerHistoryPanel.tsx`](frontend-dapp/src/components/trade/WalletIndexerHistoryPanel.tsx) `formatHistoryAmount` = `formatNum(raw)` on purpose, with a comment that `formatTokenAmount` waits on decimals. Same bug on swap **and** limit fill `token0_amount` / `token1_amount`. ### Indexer trade JSON `IndexerTrade` / `TradeResponse` expose `offer_asset` / `ask_asset` **symbols** and raw amounts. **No `offer_decimals` / `ask_decimals`.** Trader/Portfolio mixed-pair tables cannot humanize from `activePair`. Matching decimals by **symbol string** is unsafe (A1 spoof / two CW20s with the same ticker). Price after [#522](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/522) is already **human** quote-per-base. On UST1/cUSTC that is ~**206** cUSTC per UST1, which looks “wrong” next to **Amount in `1.000M`** and next to chart **Price (USD)** (~`$1` factory or ~`$0.00x` inverted). Two bugs stack: amounts are 10^decimals too large, and tape Price does **not** follow the [#524](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/524) invert pill (chart says cUSTC/UST1 while tape Price stays UST1-as-base). Limit fills on the same history panel are raw token0/token1 (6 vs 18 on UST1/USTR → `T`). ## Why the new implementation is needed Retail cannot read the tape. UST1/cUSTC is the primary economic market; showing millions for single-digit swaps is the same class of bug that #522 / #534 / #548 already fixed for USD and volume. Trade and Trader reuse the same component, so Charts-only patches will drift. ## Constraints / guardrails 1. **Do not treat tape Price as USD.** Headline **Price (USD)** stays `price_usd` / invertUsd. Tape **Price** stays human quote-per-base (or the **display** reciprocal when inverted). Never `formatNum` compact `T` on Price ([#522](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/522)). 2. **Humanize with the offer/ask asset’s decimals**, not a hardcoded `1e6`. USTR is 18; UST1/cUSTC are 6. 3. **Do not match decimals by symbol alone.** Prefer API fields `offer_decimals` / `ask_decimals` (and fill `token0_decimals` / `token1_decimals`). Pair-scoped UI may use `activePair` as a fallback only when `pair_address` matches and both symbols match **that pair’s** two legs. 4. **JSON may keep raw integers** (integrators / CSV). UI must scale. Do not break CSV columns ([#479](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/479)). 5. **Invert:** when `/trade` or `/charts` display invert is on, tape **Price** must be the reciprocal human price (display quote per display base) **or** the column header must stay explicit factory orientation. Prefer matching the pill (cUSTC per UST1 vs UST1 per cUSTC). Amount in/out stay **pay → receive** (offer → ask) and must **not** be swapped by invert — invert is display price, not fill direction. 6. **Hybrid splits** (`pool_return_amount` / `book_return_amount`) stay raw in JSON; if shown, same decimal as ask. Do not sum legs into Amount out. 7. **Zero / missing / non-numeric** → `0` or `—` (pick one per column, document). No `NaN`, no `Infinity`. 8. **Do not use tape amounts for settlement.** Display only. 9. **#489:** keep column titles **Amount in / Amount out / Price**; tooltips ≤ one sentence. 10. **Out of scope:** leaderboard volume ([#553](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/553)), portfolio P&amp;L totals ([#551](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/551)), hub DEX USD catalog (sibling issue), CG/CMC. ## Relevant files | File | Role | |------|------| | [`frontend-dapp/src/components/ui/TradesTable.tsx`](frontend-dapp/src/components/ui/TradesTable.tsx) | Shared tape | | [`frontend-dapp/src/components/ui/__tests__/TradesTable.test.tsx`](frontend-dapp/src/components/ui/__tests__/TradesTable.test.tsx) | RTL | | [`frontend-dapp/src/utils/formatAmount.ts`](frontend-dapp/src/utils/formatAmount.ts) | `formatTokenAmount` / `formatPairPrice` / `formatNum` | | [`frontend-dapp/src/components/trade/WalletIndexerHistoryPanel.tsx`](frontend-dapp/src/components/trade/WalletIndexerHistoryPanel.tsx) | Pair wallet history | | [`frontend-dapp/src/pages/ChartsPage.tsx`](frontend-dapp/src/pages/ChartsPage.tsx) | Charts tape | | [`frontend-dapp/src/components/trade/TradeRecentTradesSection.tsx`](frontend-dapp/src/components/trade/TradeRecentTradesSection.tsx) | Trade tape | | [`frontend-dapp/src/pages/TraderPage.tsx`](frontend-dapp/src/pages/TraderPage.tsx) | Mixed-pair history | | [`frontend-dapp/src/pages/PortfolioPage.tsx`](frontend-dapp/src/pages/PortfolioPage.tsx) | Same table | | [`frontend-dapp/src/types/index.ts`](frontend-dapp/src/types/index.ts) | `IndexerTrade` | | [`indexer/src/api/pairs.rs`](indexer/src/api/pairs.rs) | `TradeResponse` | | [`indexer/src/api/text_csv.rs`](indexer/src/api/text_csv.rs) | CSV — keep raw unless additive columns | | [`frontend-dapp/src/utils/tradePairDisplayOrientation.ts`](frontend-dapp/src/utils/tradePairDisplayOrientation.ts) | Invert math | | [`docs/frontend.md`](docs/frontend.md) | Charts / Trade tape notes | ## Recommended direction 1. **Indexer (required for mixed-pair pages):** additive `offer_decimals`, `ask_decimals` on trade JSON (from offer/ask `assets.decimals`). Same for limit-fill rows if the wallet panel shows them. CSV: additive columns OK; do not rewrite existing raw columns (breaking integrators). 2. **Shared formatter:** e.g. `formatTradeAmount(raw, decimals)` → `formatTokenAmount`. `TradesTable` uses trade decimals when present, else pair-leg fallback when `activePair.pair_address === t.pair_address`. 3. **Price:** `formatPairPrice(human, 6)`. If `inverted` prop is true, show `1/human` only when finite and `> 0` (reuse invert helpers; never `1/x` of `price_usd`). Optional subtitle in tooltip: `{displayQuote} per {displayBase}`. 4. **Pass `inverted` from Charts/Trade** via existing `usePairDisplayOrientation`. Trader/Portfolio: no invert (mixed pairs) — factory human price + human amounts via API decimals. 5. **Wallet history:** thread pair decimals from the pair query already on `/trade` / `/limits`; stop `formatNum(raw)`. 6. **Tests:** UST1/cUSTC 1e6/2.06e8 → `1` and `206` (compact OK for large **human** sizes only). UST1/USTR 1e6 / 1e19 → `1` and `10` not `T`. Pre-#522 raw price still must not show `T` if a legacy indexer appears (keep existing test; human `79.72` prints as `79.72`). ## Acceptance criteria - [ ] **A1.** Charts UST1/cUSTC tape: Amount in/out are human (1 UST1 → `1` class, not `1.000M`). - [ ] **A2.** Charts UST1/USTR tape: 18-dec amounts never compact as `T` unless the **human** size is actually ≥ 1e12. - [ ] **A3.** Trade page tape matches Charts for the same pair. - [ ] **A4.** Trader + Portfolio mixed history humanize per trade decimals (not one pair’s decimals for all rows). - [ ] **A5.** Wallet history Amount in/out/Price (and fills) humanized. - [ ] **A6.** Tape **Price** is human quote-per-base (`formatPairPrice`); never USD; never `T` from raw 18/6. - [ ] **A7.** When Charts/Trade invert is on, tape Price orientation matches the pill; Amount in/out stay offer→ask. - [ ] **A8.** Integrator JSON/CSV raw amounts unchanged (additive decimals allowed). - [ ] **A9.** RTL + `make verify-issue-<iid>`; docs mention the contract. ## Test plan — functional paths ### Indexer - [ ] Trade JSON includes decimals matching offer/ask assets for 6/6, 6/18, 18/6. - [ ] Missing asset decimals fail safe (omit field / skip) — UI falls back without crashing. - [ ] CSV still parses; existing columns raw; new columns documented. - [ ] Trader trades endpoint same shape as pair trades. ### Frontend RTL / unit - [ ] `TradesTable`: UST1/cUSTC raw `1000000` / `206000000` / price `206` → human in/out, price `206` (or inverted `0.00x` class when `inverted`). - [ ] `TradesTable`: USTR raw `10000000000000000000` + 18 dec → `10` (example), **not** `10.00E` / `T`. - [ ] No decimals + no matching pair → `—` or documented raw fallback **without** `T` for typical hub sizes (prefer `—` over lying). - [ ] Invert: amounts unchanged; price reciprocal; `≤0` / non-finite price → `—`. - [ ] Hybrid badge still renders; Amount out is full `return_amount` humanized, not a raw split. - [ ] `WalletIndexerHistoryPanel`: swap + fill rows humanized; CSV download still raw file from API. - [ ] ChartsPage / TradeRecentTradesSection pass invert flag. - [ ] TraderPage without `activePair` still humanizes via trade decimals. ### E2E / manual - [ ] `https://dex.cl8y.com/charts` UST1/cUSTC Recent Trades vs explorer/LCD raw ÷ 10^6. - [ ] Same pair on `/trade`. - [ ] Toggle invert: Last USD and tape Price stay consistent with pill; amounts still pay→receive. - [ ] `/trader/{addr}` with mixed UST1/cUSTC and UST1/USTR rows. - [ ] LocalTerra: `make verify-issue-<iid>` after frontend unit tests. ## Test plan — attack, hack, and abuse | Vector | Expected | |--------|----------| | **Decimal lie** — trade JSON `offer_decimals=0` on an 18-dec USTR to inflate UI | Decimals must come from **indexed `assets` row** for that offer asset id, not from the wasm event. Ignore/clamp out-of-range (`<0` or `>38`) → `—`. | | **Symbol collision** — two CW20s both `UST1` | Decimals keyed by asset id / pair legs, not `offer_asset` string. | | **Invert + USD mix-up** — UI shows `1/price_usd` in Price column | Forbidden. Reciprocal of **human** price only. | | **XSS in asset symbol** | Existing text render; do not `dangerouslySetInnerHTML`. | | **Huge raw string** | `formatTokenAmount` BigInt path; no crash; compact human if huge. | | **Negative / zero / empty amounts** | `0` or `—`; no negative compact `T`. | | **CSV injection** | CSV stays as today (leading-equals etc. already an integrator concern); do not pass CSV through `formatNum` in the file. | | **Pair mismatch** — `activePair` decimals applied to another pair’s row | Only when `t.pair_address` matches; otherwise API decimals or `—`. | | **DoS via giant decimals field** | i16 from DB; reject absurd values. | | **Misleading “Price” as executable** | Tooltip: not a limit/swap instruction; not USD. | ## Verification criteria ```bash make verify-issue-<iid> # expected: # - vitest TradesTable, WalletIndexerHistoryPanel, formatAmount # - indexer trade JSON decimals tests # - grep TradesTable: no formatNum(t.offer_amount) / formatNum(t.return_amount) # - grep WalletIndexerHistoryPanel: no formatNum(raw) for amounts # - docs/frontend.md tape contract ``` ## Out of scope - DEX hub USD / Protocol USTR ([#556](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/556)). - [#553](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/553) leaderboard Volume. - [#551](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/551) Portfolio P&amp;L / cost basis. - Changing on-chain events or swap settlement.
PlasticDigits commented 2026-08-18 00:29:12 +00:00 (Migrated from gitlab.com)

marked as related to #556

marked as related to #556
PlasticDigits commented 2026-08-18 00:29:14 +00:00 (Migrated from gitlab.com)

mentioned in issue #556

mentioned in issue #556
PlasticDigits commented 2026-08-18 00:29:15 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
PlasticDigits commented 2026-08-18 02:08:27 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1082

mentioned in merge request !1082
PlasticDigits commented 2026-08-18 02:30:14 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1083

mentioned in merge request !1083
PlasticDigits commented 2026-08-18 03:12:08 +00:00 (Migrated from gitlab.com)

mentioned in issue #560

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

marked as related to #560

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

mentioned in issue #561

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

Production QA 2026-08-18 (https://dex.cl8y.com/charts)

Retail report: UST1/cUSTC Recent Trades Amount in / Amount out “looks wrong.” Confirmed against live indexer GET /api/v1/pairs/{addr}/trades.

Pair terra1ceprjsxp86ggftf5e38wwt34l83e5gq7penkdnv4wsatkwcs8v6qccw55f (UST1 6-dec / cUSTC 6-dec). TradesTable still does formatNum(t.offer_amount) / formatNum(t.return_amount) on raw integers.

Worked example matching the screenshot class:

Column Screenshot / UI Indexer raw Human (÷ 10^6)
Amount in 45.39M ~45390000 45.39 UST1
Amount out 7.909B ~7909000000 7,909 cUSTC
Price 174.250 already human quote-per-base (#522) 174.25 cUSTC per UST1 (7909 / 45.39)

Price is internally consistent with the raw ratio. Amounts are 10^decimals too large because formatNum treats micro-units as human compact (M/B). Live row at 2026-08-18T07:37:45Z: offer_amount=43844964 → UI 43.84M, human 43.85 UST1; return_amount=8818873964 → UI 8.819B, human 8,819 cUSTC; price=201.137…; price_usd≈0.956.

Extra ACs (do not split)

  • A10. Amount in/out should name the pay/receive token (or keep the PAIR UST1 → cUSTC column as the only unit). Do not leave a bare 45.39 / 7.91K with no association after humanizing.
  • A11. Green/red “buy” coloring (offer_asset === asset_0.symbol) must follow the #524 invert pill the same way tape Price does. Paying UST1 while inverted (display-base = cUSTC) is a buy of display-base; paying UST1 when not inverted is a sell of UST1. Amounts stay offer → ask (A7).

Related leftover on the same page (24h Vol (UST1) / Vol (cUSTC) still formatNum(raw)) is filed separately so this issue stays tape + wallet history.

## Production QA 2026-08-18 (`https://dex.cl8y.com/charts`) Retail report: UST1/cUSTC **Recent Trades** Amount in / Amount out “looks wrong.” Confirmed against live indexer `GET /api/v1/pairs/{addr}/trades`. Pair `terra1ceprjsxp86ggftf5e38wwt34l83e5gq7penkdnv4wsatkwcs8v6qccw55f` (UST1 6-dec / cUSTC 6-dec). `TradesTable` still does `formatNum(t.offer_amount)` / `formatNum(t.return_amount)` on **raw** integers. Worked example matching the screenshot class: | Column | Screenshot / UI | Indexer raw | Human (÷ 10^6) | |--------|-----------------|-------------|----------------| | Amount in | `45.39M` | `~45390000` | **45.39 UST1** | | Amount out | `7.909B` | `~7909000000` | **7,909 cUSTC** | | Price | `174.250` | already human quote-per-base (#522) | **174.25 cUSTC per UST1** (`7909 / 45.39`) | Price is internally consistent with the **raw ratio**. Amounts are 10^decimals too large because `formatNum` treats micro-units as human compact (`M`/`B`). Live row at 2026-08-18T07:37:45Z: `offer_amount=43844964` → UI `43.84M`, human **43.85 UST1**; `return_amount=8818873964` → UI `8.819B`, human **8,819 cUSTC**; `price=201.137…`; `price_usd≈0.956`. ### Extra ACs (do not split) - **A10.** Amount in/out should name the pay/receive token (or keep the PAIR `UST1 → cUSTC` column as the only unit). Do not leave a bare `45.39` / `7.91K` with no association after humanizing. - **A11.** Green/red “buy” coloring (`offer_asset === asset_0.symbol`) must follow the #524 invert pill the same way tape Price does. Paying UST1 while inverted (display-base = cUSTC) is a **buy** of display-base; paying UST1 when not inverted is a **sell** of UST1. Amounts stay offer → ask (A7). Related leftover on the same page (24h **Vol (UST1)** / **Vol (cUSTC)** still `formatNum(raw)`) is filed separately so this issue stays tape + wallet history.
PlasticDigits commented 2026-08-18 12:12:14 +00:00 (Migrated from gitlab.com)

mentioned in issue #564

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

marked as related to #564

marked as related to #564
PlasticDigits commented 2026-08-18 12:13:07 +00:00 (Migrated from gitlab.com)

mentioned in issue #565

mentioned in issue #565
PlasticDigits commented 2026-08-18 12:13:10 +00:00 (Migrated from gitlab.com)

marked as related to #565

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

mentioned in merge request !1089

mentioned in merge request !1089
PlasticDigits commented 2026-08-19 01:12:10 +00:00 (Migrated from gitlab.com)

mentioned in commit db630256fd

mentioned in commit db630256fd9b7b02309ef51f94ab028584e9c4ff
PlasticDigits commented 2026-08-19 01:12:33 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1093

mentioned in merge request !1093
PlasticDigits commented 2026-08-19 01:22:22 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1095

mentioned in merge request !1095
PlasticDigits commented 2026-08-19 02:03:47 +00:00 (Migrated from gitlab.com)

mentioned in commit f36aedac2a

mentioned in commit f36aedac2ac2d7a3562bf61ab57bc8c022a513cf
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-19 02:03:47 +00:00
PlasticDigits commented 2026-08-19 02:16:39 +00:00 (Migrated from gitlab.com)

Post-merge note (!1093 stacked onto main with !1086–!1095). Code for T557-1–T557-11 is on main. Indexer JSON now adds offer_decimals / ask_decimals (fills: token0_decimals / token1_decimals) and plain integer amount strings.

Remaining (not done in merge):

  • Indexer deploy/restart required before mixed-pair Trader/Portfolio tape gets API decimals in production (pair-scoped Charts/Trade still have activePair fallback).
  • Manual production: Charts UST1/cUSTC tape vs LCD raw ÷ 10^6; invert pill Price reciprocal only; /trader/{addr} mixed UST1/cUSTC and UST1/USTR rows.
  • GitLab CI was not waited (pipelines were failing / quota). Local verify still needed after Coolify + indexer restart.
**Post-merge note (!1093 stacked onto main with !1086–!1095).** Code for T557-1–T557-11 is on `main`. Indexer JSON now adds `offer_decimals` / `ask_decimals` (fills: `token0_decimals` / `token1_decimals`) and plain integer amount strings. Remaining (not done in merge): - **Indexer deploy/restart required** before mixed-pair Trader/Portfolio tape gets API decimals in production (pair-scoped Charts/Trade still have `activePair` fallback). - Manual production: Charts UST1/cUSTC tape vs LCD raw ÷ 10^6; invert pill Price reciprocal only; `/trader/{addr}` mixed UST1/cUSTC and UST1/USTR rows. - GitLab CI was **not** waited (pipelines were failing / quota). Local verify still needed after Coolify + indexer restart.
PlasticDigits commented 2026-08-19 02:28:46 +00:00 (Migrated from gitlab.com)

marked as related to #573

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

mentioned in issue #573

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

Sanity after stack: frontend tape tests PASS. Indexer cargo test rungs FAIL on this agent VM: Permission denied on root-owned indexer/target/debug/.cargo-artifact-lock (not a product defect). Re-run make verify-issue-557 after sudo chown -R $(whoami) indexer/target.

**Sanity after stack:** frontend tape tests **PASS**. Indexer `cargo test` rungs **FAIL** on this agent VM: `Permission denied` on root-owned `indexer/target/debug/.cargo-artifact-lock` (not a product defect). Re-run `make verify-issue-557` after `sudo chown -R $(whoami) indexer/target`.
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#557
No description provided.