feat: /trade identity row — v2 LP USD (humanized AMM TVL) #664

Closed
opened 2026-08-26 04:15:25 +00:00 by PlasticDigits · 10 comments
PlasticDigits commented 2026-08-26 04:15:25 +00:00 (Migrated from gitlab.com)

Summary

Show humanized factory v2 AMM LP USD on /trade/{pair} next to the existing token-identity chips (leg names + pair address). Bundle the same identity chrome on /charts/{pair} (shared PairTokenLinks) plus GET /api/v1/pairs/{addr} returning the per-pair field. Do not split “Trade chip” vs “Charts chip” vs “single-pair JSON.”

This is pair TVL (both reserve legs, P522-Q / hub catalog — same number as #569 protocol_pair_tvl and the #655 list stamp). It is not the connected wallet’s LP-token mark-to-market, not book escrow, not V3 Grid / #619 vaults, not 24h volume, not CoinGecko liquidity_in_usd (mislabeled volume).

Depends on / shares rollup with #655 (pair_liquidity_usd + list liquidity_usd). This ticket owns single-pair GET + Trade / Charts identity. Do not re-implement the /pool sortable column here.

Related: #541 (PairTokenLinks), #655 (/pool column + list rollup), #569 (protocol TVL math), #556 (hub USD), #524 (invert), #489 (no lectures), #653 (no nested chrome), #565 (Charts 24h Vol USD is flow, not stock), #430 (href safety).

Current codebase

/trade/{pair} already identifies the market (symbols + pair bech32). It does not show how large the v2 pool is. Protocol can price a factory pair; the single-pair API does not return that number. Charts pair 24h Stats show volume USD, which is easy to confuse with pool size.

Layer Behavior today
/trade identity TradePage.tsx mounts PairTokenLinks under trade-pair-select-panel, outside PairSearchSelect. Chips: factory asset_0 / asset_1 via TokenIdentity + pair AddressRow (token-identity-pair). Hidden when workspace is off (#176 / #175 / unknown pair — T541-6). No LP / TVL / USD.
Trade pair fetch Same page already queries GET /api/v1/pairs/{addr} (queryKey: ['indexer-pair-trade', pairAddr], staleTime: 60_000) via getPair. Used for tape USD / freeze / unknown-pair — not for identity LP. Identity assets still come from factory PairInfo.
/charts identity ChartsPage.tsx uses the same PairTokenLinks under the pair MenuSelect. Pair 24h Stats (charts-pair-24h-stats) show Last 24h Vol (USD) from GET …/stats volume_usd (P565-1) — flow, not pool stock.
PairTokenLinks Legs + pair chip only. Optional inverted reorders chips; payloads stay factory assets (T541-5). No USD prop. Also used on /pool table rows and Manage expand — those rows must not grow a second LP chip (column is #655).
Single-pair API GET /api/v1/pairs/{addr} (indexer/src/api/pairs.rs get_pair) returns PairResponse: address, assets, lp_token, fee_bps, is_active, code_id_frozen, volume_quote_24h: None. No liquidity_usd. Unknown addr → 404.
Pair list GET /api/v1/pairs has volume_quote_24h only. Per-pair USD stamp + sort=liquidity_usd is #655 — do not fork a second field name.
Pair stats GET /api/v1/pairs/{addr}/stats is 24h OHLC + volume (volume_usd is swap notional). Do not overload this with TVL.
Protocol pair TVL protocol_tvl.rs protocol_pair_tvl / pair_usable_for_protocol_tvl. Both legs priced → h0×usd0 + h1×usd1. One catalogued → 2× that leg (CPAMM). Neither / stale / same-asset / overflow / reserve_*=0 → omit (not $0). Book escrow excluded. Writes global total_liquidity_usd only until #655 materializes per-pair.
USD catalog USTC/cUSTC/uusd → USTC oracle; LUNC/cLUNC/uluna → LUNC; UST1/USTR → hub_prices. Never $1 UST1, never 2.5× USTR, never vFDUSD/FDUSD (P569-4). Identity = contract/denom.
Reserves pair_reserves via book_snapshot.rs (~10s). Stale TTL same class as hub / protocol TVL.
USD format formatProtocolUsd: missing / non-finite → —, never Infinity. Compact via formatNum. Protocol Total liquidity already uses this.
Copy / chrome #489: labels ≤ ~5 words; no always-on TVL lecture. #653: trade-pair-select-panel is already shell-panel — no inner StatBox / card-glass. #541: explorer stays a sibling; do not wrap symbols in <a>.
Tests / verify PairTokenLinks.test.tsx, TradePage.test.tsx #541 describe, ChartsPage.test.tsx, e2e/token-identity-541.spec.ts, make verify-issue-541. Indexer: api_pairs.rs.
Docs / skills docs/frontend.md § Token identity (T541-1–T541-8), skills/AGENTS_FRONTEND_TOKEN_IDENTITY.md, docs/indexer-invariants.md (pair GET / protocol TVL).

Why this is needed

  1. Traders cannot see pool size on the ticket. /trade/{pair} is where size vs book vs slippage is decided. Identity shows which tokens; it does not show how much v2 LP backs the market. Charts 24h Vol (USD) answers flow, not stock — easy to misread as TVL.
  2. The number already exists in-process. protocol_pair_tvl is the census primitive. #655 stamps it for the list. Trade already calls single-pair GET, which today returns metadata only (volume_quote_24h forced None). Without that field on {addr}, the UI will LCD getPool or invent USD from volume / $1 UST1.
  3. Shared chrome. Pool / Trade / Charts already share PairTokenLinks (#541). Trade asked first; Charts is the same pair-detail identity row. Implementing Trade-only would leave Charts as a leftover (same class as the old #188 pair-chip leftover).
  4. Do not fork TVL. A second client-side formula (reserves × hub fetch, CG liquidity_in_usd, LP supply × last print) will drift from Protocol and the /pool column.

Constraints / guardrails

  1. Reuse #569 / #655 math and field name. liquidity_usd (human USD decimal string). Call protocol_pair_tvl / read the #655 pair_liquidity_usd stamp. Never a second catalog. Never vFDUSD. Never $1 UST1. Never 2.5× USTR. Identity = contract/denom.
  2. Single-pair GET is a rollup read, not a live TVL sum. GET /api/v1/pairs/{addr} must not join pair_reserves × oracles × hub on the request path. Read the same materialized stamp as #655 (or NULL if that row is missing). If this ticket lands first, still create/read that stamp — do not compute TVL in get_pair.
  3. Do not put TVL on /stats. volume_usd stays 24h swap notional. Mixing stock into flow JSON will break Charts #565 consumers.
  4. Unpriced ≠ $0. Stale / zero reserves / same-asset / overflow / hub-oracle down / neither leg catalogued → omit/null. UI em-dash or omit the chip. $0 only if a priced empty pool can exist after reserves_usable; otherwise still —.
  5. AMM v2 reserves only. No book escrow, parked dust, wallet LP, wrap-mapper inventory, V3 Grid AUM. Retail label v2 LP + compact $… (≤ ~5 words). title / aria-label: factory AMM pool USD (snapshot), not 24h volume. No lecture paragraph (#489-9).
  6. Identity invariants stay. T541-1–T541-8 unchanged. Invert (#524) does not change the USD. Invalid / missing pair: no identity row and no LP chip (T541-6). Copy payload stays contract/denom, never the USD string.
  7. Chrome. Sibling of token-identity-pair inside PairTokenLinks (or an adjacent typographic node in the same flex wrap). No StatBox / card-glass inside trade-pair-select-panel (C653-1). No gold fill. Light + dark; 375px wrap is OK.
  8. /pool must not double-paint. PairTokenLinks takes an optional liquidityUsd. Trade + Charts pass it. Pool table / Manage do not (column is #655). Default omit = today’s row.
  9. No extra LCD. Trade default paint must not add getPool / getPairFeeConfig / GET /hub-prices / GET /overview solely to render this chip. Use the existing getPair query (refetch interval may shorten vs 60s if the stamp is fresher; do not poll LCD).
  10. Hostile JSON. Cap display length (same class as Charts overview 24 chars). < > / javascript: / non-finite / scientific overflow → —. Text only — no innerHTML.
  11. Indexer down / 404. Existing Trade outage / unknown-pair notices stay. Chip omitted or —; never a leftover $ from the previous pair after switch.
  12. Gems. Production hide-gems (#562) unchanged. Deep-link gem: show TVL only if stamped; never invent.
  13. No new outbound hosts, no /token route, no picker-option LP badges.

Relevant files

File Role
indexer/src/api/pairs.rs PairResponse + get_pair must emit optional liquidity_usd from the #655 stamp
indexer/src/indexer/protocol_tvl.rs Shared TVL math (do not fork)
indexer/src/db/queries/pairs.rs Single-pair SELECT JOIN stamp (or None)
indexer/tests/api_pairs.rs GET /pairs/{addr} field / 404 / no live sum
frontend-dapp/src/types/index.ts IndexerPair.liquidity_usd?: string | null (shared with #655)
frontend-dapp/src/services/indexer/client.ts getPair types only — no new client
frontend-dapp/src/components/ui/PairTokenLinks.tsx Optional compact v2 LP chip
frontend-dapp/src/pages/TradePage.tsx Pass indexerPairQuery.data?.liquidity_usd
frontend-dapp/src/pages/ChartsPage.tsx Same for selected pair (getPair or list field — prefer getPair so it does not wait on #655 list JOIN)
frontend-dapp/src/utils/formatProtocolStats.ts formatProtocolUsd
frontend-dapp/src/components/ui/__tests__/PairTokenLinks.test.tsx Chip present / omitted / hostile
frontend-dapp/src/pages/TradePage.test.tsx #541 still holds; new LP cases
frontend-dapp/src/pages/ChartsPage.test.tsx Identity LP; stats Vol USD unchanged
frontend-dapp/src/components/pool/PoolPairsTable.tsx Must not pass liquidityUsd
frontend-dapp/e2e/token-identity-541.spec.ts Chip on /trade/{pair} + /charts
docs/frontend.md Token identity + trade pair chrome
docs/indexer-invariants.md Single-pair liquidity_usd
skills/AGENTS_FRONTEND_TOKEN_IDENTITY.md Do/don’t + verify
scripts/qa/verify-issue-NNN.sh + Makefile make verify-issue-NNN
  1. Indexer: After #655 stamp exists (or land the stamp in the same MR if this ships first), get_pair JOINs pair_liquidity_usd and sets liquidity_usd: Option<String> (skip_serializing_if). 404 still 404. Do not populate volume_quote_24h as a side effect. Do not compute TVL in the handler.
  2. PairTokenLinks: Optional liquidityUsd?: string \| null. When formatProtocolUsd ≠ —, render a compact sibling data-testid="token-identity-v2-lp-usd": label v2 LP + $…. When — / missing, omit the chip (or show — only if the pair row is visible and we need a stable testid — prefer omit-when-unpriced so identity stays quiet).
  3. Trade: Pass indexerPairQuery.data?.liquidity_usd. Pair switch must drop the previous pair’s $ immediately (key the chip on pairAddr).
  4. Charts: Same chip on the identity row. Do not add a 24h Stats “TVL” tile (that would nest chrome and collide with Vol USD).
  5. Docs + make verify-issue-NNN. Vitest + GET /pairs/{addr} indexer test + scoped Playwright (5 workers, no e2e-tx). make verify-issue-541 stays green.

Do not LCD getPool to multiply reserves in the browser. Do not GET /overview and pretend protocol TVL is this pair.

Acceptance criteria

  • AC1. /trade/{pair} identity row shows v2 LP + compact human USD next to token chips / pair address when liquidity_usd is priced.
  • AC2. /charts/{pair} identity row shows the same chip for the selected pair. Charts 24h Vol (USD) is unchanged (still volume_usd).
  • AC3. GET /api/v1/pairs/{addr} includes optional liquidity_usd string when stamped; omitted/null when not. Additive. Unknown addr still 404.
  • AC4. Value matches #655 / protocol_pair_tvl for the same stamp (UST1/cUSTC not $1/leg; USTR not 2.5× USTC).
  • AC5. Unpriced / missing / "" / "Infinity" / "NaN" / hostile → no fake $0; chip omitted or —.
  • AC6. #524 invert does not change the USD or swap identity payloads (T541-5).
  • AC7. Invalid / unknown / empty pair: no identity row, no LP chip (T541-6). Pair switch does not flash the previous pair’s USD.
  • AC8. /pool table PairTokenLinks does not show the chip (no double paint vs #655 column).
  • AC9. No new getPool / hub / overview fetch on Trade first paint. Existing getPair only.
  • AC10. No StatBox / nested card-glass in the pair-select panel. No TVL lecture.
  • AC11. Explorer / copy behavior unchanged (T541-2–T541-4). USD is not the copy payload.
  • AC12. Docs/skills/invariants + make verify-issue-NNN land with the implementation. make verify-issue-541 still green.

Test plan (all paths)

Indexer (GET /api/v1/pairs/{addr})

ID Path Expect
I1 Priced pair with stamp liquidity_usd = stamped protocol_pair_tvl string
I2 Unstamped / NULL rollup field omitted/null; 200
I3 Unknown bech32 / missing row 404 (not liquidity_usd: "0")
I4 UST1/cUSTC uses hub + USTC — not peg $1
I5 UST1/USTR uses hub — not 2.5×
I6 Handler does not query pair_reserves × oracle on GET stamp JOIN only (or no field)
I7 volume_quote_24h on single GET stays None / unchanged do not silently start filling it
I8 code_id_frozen / lp_token unchanged additive JSON
I9 Overflow stamp omitted GET 200, no Inf

Frontend — Trade

ID Path Expect
T1 Factory pair + priced getPair chip v2 LP + $ compact next to identity
T2 getPair missing field / null / hostile no chip or —; page does not crash
T3 Bare /trade auto-pick chip follows selected catalog pair
T4 Switch A → B B’s USD (or omit); A’s $ gone before B paints
T5 Invert pill USD unchanged; identity payloads unchanged
T6 #176 / #175 / unknown route no pair-token-links, no LP chip
T7 Indexer outage existing banner; no leftover $
T8 Legacy gem notice (LocalTerra) chip only if stamped; no invented USD
T9 Light + dark, 375 / 1280 wraps with chips; still one chrome layer
T10 #541 copy/explorer still pass; USD not copied

Frontend — Charts + shared

ID Path Expect
C1 /charts/{pair} identity same chip semantics as Trade
C2 24h Stats Vol (USD) still volume_usd / formatIndexedVolumeUsd; not TVL
C3 Invalid / unknown chart pair no identity, no LP chip
C4 PairTokenLinks without liquidityUsd identical to #541 (Pool table regression)
C5 Priced "1234.5" $ + compact (formatProtocolUsd)
C6 Display cap / <> — or omit

E2E / LocalTerra

ID Path Expect
E1 /trade/{factoryPair} identity + v2 LP $ or quiet omit — not blank garbage
E2 /charts/{same} chip present iff Trade would show it
E3 Token-identity-541 spec copy/explorer still pass (5 workers, no e2e-tx)

Test plan (attack, hack, abuse)

ID Vector Expect
A1 XSS liquidity_usd = <img onerror=…> / javascript: text or omit; no HTML / no explorer href from USD
A2 Copy hijack Copy buttons still emit contract/denom, never the USD string
A3 Pair switch stale USD Previous pair’s $12M must not linger on a dust / unpriced pair (spoof size)
A4 Client invents TVL from volume_quote_24h / stats.volume_usd / CG liquidity_in_usd forbidden
A5 LCD N+1 getPool on Trade paint forbidden
A6 GET-path DoS live reserve×oracle in get_pair forbidden — stamp read only
A7 $1 UST1 / 2.5× USTR forbidden
A8 vFDUSD / FDUSD conversion forbidden
A9 Symbol-spoof native symbol=USTR not priced
A10 Book escrow / wallet LP share forbidden (privacy + wrong product)
A11 Unpriced as $0 forbidden (looks like an empty real pool)
A12 Stats overload adding liquidity_usd to /stats and Charts reading the wrong field forbidden
A13 Pool double paint identity chip + #655 column /pool identity must not take liquidityUsd
A14 Nested chrome / lecture no card-glass chip, no “this is AMM TVL not book” blurb
A15 Scientific JSON 1e+19 / huge string — / omit; no tab lock
A16 Invalid pair javascript: route no identity, no LP (T541-6 / #430)
A17 Gem wash TVL on production Trade pair hidden (#562); deep-link does not invent USD
A18 Overview total as pair TVL forbidden (would show protocol sum on every pair)

Verification criteria

Ship is done when all of the following hold:

  1. make verify-issue-NNN (new) is green: indexer GET /pairs/{addr} stamp/omit/404; Vitest PairTokenLinks + Trade + Charts; static check that Trade identity does not call getPool.
  2. make verify-issue-541 still green (identity copy/explorer/invert/hide).
  3. make verify-issue-565 still green (Charts Vol USD is still flow).
  4. If #655 has landed: make verify-issue-655 (or its verify target) still green; Trade/Charts USD equals the /pool cell for the same pair.
  5. LocalTerra smoke: /trade/{pair} and /charts/{pair} show v2 LP $… or quiet omit; pair switch; invert; no LCD storm.
  6. Docs updated: docs/frontend.md token identity, docs/indexer-invariants.md single-pair liquidity_usd, skills/AGENTS_FRONTEND_TOKEN_IDENTITY.md. AGENTS.md verify one-liner if that is house style.
  7. Old indexer without the field: chip omitted, no console throw.

Out of scope: /pool sortable v2 LP USD column and list sort=liquidity_usd (#655); Protocol global TVL (#569); V3 Grid TVL (#619); per-wallet LP on /portfolio; Charts 24h Stats extra tile; CG liquidity_in_usd rename.

## Summary Show **humanized factory v2 AMM LP USD** on `/trade/{pair}` next to the existing token-identity chips (leg names + pair address). Bundle the **same identity chrome on `/charts/{pair}`** (shared `PairTokenLinks`) plus **`GET /api/v1/pairs/{addr}`** returning the per-pair field. Do **not** split “Trade chip” vs “Charts chip” vs “single-pair JSON.” This is **pair TVL** (both reserve legs, P522-Q / hub catalog — same number as [#569](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/569) `protocol_pair_tvl` and the [#655](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/655) list stamp). It is **not** the connected wallet’s LP-token mark-to-market, **not** book escrow, **not** V3 Grid / #619 vaults, **not** 24h volume, **not** CoinGecko `liquidity_in_usd` (mislabeled volume). **Depends on / shares rollup with [#655](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/655)** (`pair_liquidity_usd` + list `liquidity_usd`). This ticket owns **single-pair GET** + **Trade / Charts identity**. Do **not** re-implement the `/pool` sortable column here. Related: [#541](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/541) (`PairTokenLinks`), [#655](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/655) (`/pool` column + list rollup), [#569](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/569) (protocol TVL math), [#556](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/556) (hub USD), [#524](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/524) (invert), [#489](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/489) (no lectures), [#653](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/653) (no nested chrome), [#565](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/565) (Charts 24h **Vol** USD is flow, not stock), [#430](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/430) (href safety). ## Current codebase `/trade/{pair}` already identifies the market (symbols + pair bech32). It does **not** show how large the v2 pool is. Protocol can price a factory pair; the **single-pair** API does not return that number. Charts pair 24h Stats show **volume** USD, which is easy to confuse with pool size. | Layer | Behavior today | |-------|----------------| | **`/trade` identity** | [`TradePage.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/pages/TradePage.tsx) mounts [`PairTokenLinks`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/components/ui/PairTokenLinks.tsx) under `trade-pair-select-panel`, **outside** `PairSearchSelect`. Chips: factory `asset_0` / `asset_1` via [`TokenIdentity`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/components/ui/TokenIdentity.tsx) + pair [`AddressRow`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/components/ui/AddressRow.tsx) (`token-identity-pair`). Hidden when workspace is off (#176 / #175 / unknown pair — **T541-6**). **No LP / TVL / USD.** | | **Trade pair fetch** | Same page already queries `GET /api/v1/pairs/{addr}` (`queryKey: ['indexer-pair-trade', pairAddr]`, `staleTime: 60_000`) via [`getPair`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/services/indexer/client.ts). Used for tape USD / freeze / unknown-pair — **not** for identity LP. Identity assets still come from **factory** `PairInfo`. | | **`/charts` identity** | [`ChartsPage.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/pages/ChartsPage.tsx) uses the same `PairTokenLinks` under the pair `MenuSelect`. Pair 24h Stats (`charts-pair-24h-stats`) show **Last 24h Vol (USD)** from `GET …/stats` `volume_usd` (**P565-1**) — **flow**, not pool stock. | | **`PairTokenLinks`** | Legs + pair chip only. Optional `inverted` reorders chips; payloads stay factory assets (**T541-5**). No USD prop. Also used on `/pool` table rows and Manage expand — those rows must **not** grow a second LP chip (column is #655). | | **Single-pair API** | `GET /api/v1/pairs/{addr}` ([`indexer/src/api/pairs.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/api/pairs.rs) `get_pair`) returns `PairResponse`: address, assets, `lp_token`, `fee_bps`, `is_active`, `code_id_frozen`, `volume_quote_24h: None`. **No** `liquidity_usd`. Unknown addr → **404**. | | **Pair list** | `GET /api/v1/pairs` has `volume_quote_24h` only. Per-pair USD stamp + `sort=liquidity_usd` is **[#655](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/655)** — do not fork a second field name. | | **Pair stats** | `GET /api/v1/pairs/{addr}/stats` is 24h **OHLC + volume** (`volume_usd` is swap notional). Do **not** overload this with TVL. | | **Protocol pair TVL** | [`protocol_tvl.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/indexer/protocol_tvl.rs) `protocol_pair_tvl` / `pair_usable_for_protocol_tvl`. Both legs priced → `h0×usd0 + h1×usd1`. One catalogued → `2×` that leg (CPAMM). Neither / stale / same-asset / overflow / `reserve_*=0` → **omit** (not `$0`). Book escrow excluded. Writes **global** `total_liquidity_usd` only until #655 materializes per-pair. | | **USD catalog** | USTC/cUSTC/`uusd` → USTC oracle; LUNC/cLUNC/`uluna` → LUNC; UST1/USTR → `hub_prices`. Never `$1` UST1, never `2.5×` USTR, never vFDUSD/FDUSD (**P569-4**). Identity = contract/denom. | | **Reserves** | `pair_reserves` via [`book_snapshot.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/src/indexer/book_snapshot.rs) (~10s). Stale TTL same class as hub / protocol TVL. | | **USD format** | [`formatProtocolUsd`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/utils/formatProtocolStats.ts): missing / non-finite → `—`, never `Infinity`. Compact via `formatNum`. Protocol Total liquidity already uses this. | | **Copy / chrome** | #489: labels ≤ ~5 words; no always-on TVL lecture. #653: `trade-pair-select-panel` is already `shell-panel` — **no** inner `StatBox` / `card-glass`. #541: explorer stays a sibling; do not wrap symbols in `<a>`. | | **Tests / verify** | [`PairTokenLinks.test.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/components/ui/__tests__/PairTokenLinks.test.tsx), [`TradePage.test.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/pages/TradePage.test.tsx) `#541` describe, [`ChartsPage.test.tsx`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/pages/ChartsPage.test.tsx), [`e2e/token-identity-541.spec.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/e2e/token-identity-541.spec.ts), `make verify-issue-541`. Indexer: [`api_pairs.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/indexer/tests/api_pairs.rs). | | **Docs / skills** | [`docs/frontend.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/frontend.md) § Token identity (**T541-1–T541-8**), [`skills/AGENTS_FRONTEND_TOKEN_IDENTITY.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/skills/AGENTS_FRONTEND_TOKEN_IDENTITY.md), [`docs/indexer-invariants.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/indexer-invariants.md) (pair GET / protocol TVL). | ## Why this is needed 1. **Traders cannot see pool size on the ticket.** `/trade/{pair}` is where size vs book vs slippage is decided. Identity shows *which* tokens; it does not show *how much* v2 LP backs the market. Charts 24h **Vol (USD)** answers flow, not stock — easy to misread as TVL. 2. **The number already exists in-process.** `protocol_pair_tvl` is the census primitive. #655 stamps it for the **list**. Trade already calls **single-pair** GET, which today returns metadata only (`volume_quote_24h` forced `None`). Without that field on `{addr}`, the UI will LCD `getPool` or invent USD from volume / `$1` UST1. 3. **Shared chrome.** Pool / Trade / Charts already share `PairTokenLinks` (#541). Trade asked first; Charts is the same pair-detail identity row. Implementing Trade-only would leave Charts as a leftover (same class as the old #188 pair-chip leftover). 4. **Do not fork TVL.** A second client-side formula (reserves × hub fetch, CG `liquidity_in_usd`, LP supply × last print) will drift from Protocol and the `/pool` column. ## Constraints / guardrails 1. **Reuse #569 / #655 math and field name.** `liquidity_usd` (human USD decimal string). Call `protocol_pair_tvl` / read the #655 `pair_liquidity_usd` stamp. Never a second catalog. Never vFDUSD. Never `$1` UST1. Never `2.5×` USTR. Identity = contract/denom. 2. **Single-pair GET is a rollup read, not a live TVL sum.** `GET /api/v1/pairs/{addr}` must **not** join `pair_reserves` × oracles × hub on the request path. Read the same materialized stamp as #655 (or `NULL` if that row is missing). If this ticket lands first, still create/read that stamp — do not compute TVL in `get_pair`. 3. **Do not put TVL on `/stats`.** `volume_usd` stays 24h swap notional. Mixing stock into flow JSON will break Charts #565 consumers. 4. **Unpriced ≠ `$0`.** Stale / zero reserves / same-asset / overflow / hub-oracle down / neither leg catalogued → omit/`null`. UI **em-dash** or omit the chip. `$0` only if a priced empty pool can exist after `reserves_usable`; otherwise still `—`. 5. **AMM v2 reserves only.** No book escrow, parked dust, wallet LP, wrap-mapper inventory, V3 Grid AUM. Retail label **v2 LP** + compact `$…` (≤ ~5 words). `title` / `aria-label`: factory AMM pool USD (snapshot), not 24h volume. No lecture paragraph (**#489-9**). 6. **Identity invariants stay.** **T541-1–T541-8** unchanged. Invert (#524) does **not** change the USD. Invalid / missing pair: no identity row **and** no LP chip (**T541-6**). Copy payload stays contract/denom, never the USD string. 7. **Chrome.** Sibling of `token-identity-pair` inside `PairTokenLinks` (or an adjacent typographic node in the same flex wrap). **No** `StatBox` / `card-glass` inside `trade-pair-select-panel` (**C653-1**). No gold fill. Light + dark; 375px wrap is OK. 8. **`/pool` must not double-paint.** `PairTokenLinks` takes an **optional** `liquidityUsd`. Trade + Charts pass it. Pool table / Manage **do not** (column is #655). Default omit = today’s row. 9. **No extra LCD.** Trade default paint must not add `getPool` / `getPairFeeConfig` / `GET /hub-prices` / `GET /overview` solely to render this chip. Use the existing `getPair` query (refetch interval may shorten vs 60s if the stamp is fresher; do not poll LCD). 10. **Hostile JSON.** Cap display length (same class as Charts overview **24** chars). `<` `>` / `javascript:` / non-finite / scientific overflow → `—`. Text only — no `innerHTML`. 11. **Indexer down / 404.** Existing Trade outage / unknown-pair notices stay. Chip omitted or `—`; never a leftover `$` from the previous pair after switch. 12. **Gems.** Production hide-gems (#562) unchanged. Deep-link gem: show TVL only if stamped; never invent. 13. **No new outbound hosts, no `/token` route, no picker-option LP badges.** ## Relevant files | File | Role | |------|------| | `indexer/src/api/pairs.rs` | `PairResponse` + `get_pair` must emit optional `liquidity_usd` from the #655 stamp | | `indexer/src/indexer/protocol_tvl.rs` | Shared TVL math (do not fork) | | `indexer/src/db/queries/pairs.rs` | Single-pair SELECT JOIN stamp (or `None`) | | `indexer/tests/api_pairs.rs` | `GET /pairs/{addr}` field / 404 / no live sum | | `frontend-dapp/src/types/index.ts` | `IndexerPair.liquidity_usd?: string \| null` (shared with #655) | | `frontend-dapp/src/services/indexer/client.ts` | `getPair` types only — no new client | | `frontend-dapp/src/components/ui/PairTokenLinks.tsx` | Optional compact **v2 LP** chip | | `frontend-dapp/src/pages/TradePage.tsx` | Pass `indexerPairQuery.data?.liquidity_usd` | | `frontend-dapp/src/pages/ChartsPage.tsx` | Same for selected pair (`getPair` or list field — prefer `getPair` so it does not wait on #655 list JOIN) | | `frontend-dapp/src/utils/formatProtocolStats.ts` | `formatProtocolUsd` | | `frontend-dapp/src/components/ui/__tests__/PairTokenLinks.test.tsx` | Chip present / omitted / hostile | | `frontend-dapp/src/pages/TradePage.test.tsx` | `#541` still holds; new LP cases | | `frontend-dapp/src/pages/ChartsPage.test.tsx` | Identity LP; stats Vol USD unchanged | | `frontend-dapp/src/components/pool/PoolPairsTable.tsx` | Must **not** pass `liquidityUsd` | | `frontend-dapp/e2e/token-identity-541.spec.ts` | Chip on `/trade/{pair}` + `/charts` | | `docs/frontend.md` | Token identity + trade pair chrome | | `docs/indexer-invariants.md` | Single-pair `liquidity_usd` | | `skills/AGENTS_FRONTEND_TOKEN_IDENTITY.md` | Do/don’t + verify | | `scripts/qa/verify-issue-NNN.sh` + `Makefile` | `make verify-issue-NNN` | ## Recommended direction 1. **Indexer:** After #655 stamp exists (or land the stamp in the same MR if this ships first), `get_pair` JOINs `pair_liquidity_usd` and sets `liquidity_usd: Option<String>` (`skip_serializing_if`). 404 still 404. Do not populate `volume_quote_24h` as a side effect. Do not compute TVL in the handler. 2. **`PairTokenLinks`:** Optional `liquidityUsd?: string \| null`. When `formatProtocolUsd` ≠ `—`, render a compact sibling `data-testid="token-identity-v2-lp-usd"`: label **v2 LP** + `$…`. When `—` / missing, omit the chip (or show `—` only if the pair row is visible and we need a stable testid — prefer omit-when-unpriced so identity stays quiet). 3. **Trade:** Pass `indexerPairQuery.data?.liquidity_usd`. Pair switch must drop the previous pair’s `$` immediately (key the chip on `pairAddr`). 4. **Charts:** Same chip on the identity row. Do **not** add a 24h Stats “TVL” tile (that would nest chrome and collide with Vol USD). 5. **Docs + `make verify-issue-NNN`.** Vitest + `GET /pairs/{addr}` indexer test + scoped Playwright (5 workers, no e2e-tx). `make verify-issue-541` stays green. Do **not** LCD `getPool` to multiply reserves in the browser. Do **not** `GET /overview` and pretend protocol TVL is this pair. ## Acceptance criteria - [ ] **AC1.** `/trade/{pair}` identity row shows **v2 LP** + compact human USD next to token chips / pair address when `liquidity_usd` is priced. - [ ] **AC2.** `/charts/{pair}` identity row shows the same chip for the selected pair. Charts 24h **Vol (USD)** is unchanged (still `volume_usd`). - [ ] **AC3.** `GET /api/v1/pairs/{addr}` includes optional `liquidity_usd` string when stamped; omitted/`null` when not. Additive. Unknown addr still **404**. - [ ] **AC4.** Value matches #655 / `protocol_pair_tvl` for the same stamp (UST1/cUSTC not `$1`/leg; USTR not `2.5×` USTC). - [ ] **AC5.** Unpriced / missing / `""` / `"Infinity"` / `"NaN"` / hostile → no fake `$0`; chip omitted or `—`. - [ ] **AC6.** #524 invert does not change the USD or swap identity payloads (**T541-5**). - [ ] **AC7.** Invalid / unknown / empty pair: no identity row, no LP chip (**T541-6**). Pair switch does not flash the previous pair’s USD. - [ ] **AC8.** `/pool` table `PairTokenLinks` does **not** show the chip (no double paint vs #655 column). - [ ] **AC9.** No new `getPool` / hub / overview fetch on Trade first paint. Existing `getPair` only. - [ ] **AC10.** No `StatBox` / nested `card-glass` in the pair-select panel. No TVL lecture. - [ ] **AC11.** Explorer / copy behavior unchanged (**T541-2–T541-4**). USD is not the copy payload. - [ ] **AC12.** Docs/skills/invariants + `make verify-issue-NNN` land with the implementation. `make verify-issue-541` still green. ## Test plan (all paths) ### Indexer (`GET /api/v1/pairs/{addr}`) | ID | Path | Expect | |----|------|--------| | I1 | Priced pair with stamp | `liquidity_usd` = stamped `protocol_pair_tvl` string | | I2 | Unstamped / NULL rollup | field omitted/`null`; **200** | | I3 | Unknown bech32 / missing row | **404** (not `liquidity_usd: "0"`) | | I4 | UST1/cUSTC uses hub + USTC — not peg `$1` | | | I5 | UST1/USTR uses hub — not `2.5×` | | | I6 | Handler does not query `pair_reserves` × oracle on GET | stamp JOIN only (or no field) | | I7 | `volume_quote_24h` on single GET stays `None` / unchanged | do not silently start filling it | | I8 | `code_id_frozen` / `lp_token` unchanged | additive JSON | | I9 | Overflow stamp omitted | GET **200**, no Inf | ### Frontend — Trade | ID | Path | Expect | |----|------|--------| | T1 | Factory pair + priced `getPair` | chip **v2 LP** + `$` compact next to identity | | T2 | `getPair` missing field / null / hostile | no chip or `—`; page does not crash | | T3 | Bare `/trade` auto-pick | chip follows selected catalog pair | | T4 | Switch A → B | B’s USD (or omit); A’s `$` gone before B paints | | T5 | Invert pill | USD unchanged; identity payloads unchanged | | T6 | #176 / #175 / unknown route | no `pair-token-links`, no LP chip | | T7 | Indexer outage | existing banner; no leftover `$` | | T8 | Legacy gem notice (LocalTerra) | chip only if stamped; no invented USD | | T9 | Light + dark, 375 / 1280 | wraps with chips; still one chrome layer | | T10 | #541 copy/explorer | still pass; USD not copied | ### Frontend — Charts + shared | ID | Path | Expect | |----|------|--------| | C1 | `/charts/{pair}` identity | same chip semantics as Trade | | C2 | 24h Stats Vol (USD) | still `volume_usd` / `formatIndexedVolumeUsd`; not TVL | | C3 | Invalid / unknown chart pair | no identity, no LP chip | | C4 | `PairTokenLinks` without `liquidityUsd` | identical to #541 (Pool table regression) | | C5 | Priced `"1234.5"` | `$` + compact (`formatProtocolUsd`) | | C6 | Display cap / `<>` | `—` or omit | ### E2E / LocalTerra | ID | Path | Expect | |----|------|--------| | E1 | `/trade/{factoryPair}` | identity + **v2 LP** `$` or quiet omit — not blank garbage | | E2 | `/charts/{same}` | chip present iff Trade would show it | | E3 | Token-identity-541 spec | copy/explorer still pass (5 workers, no e2e-tx) | ## Test plan (attack, hack, abuse) | ID | Vector | Expect | |----|--------|--------| | A1 | **XSS** `liquidity_usd` = `<img onerror=…>` / `javascript:` | text or omit; no HTML / no explorer href from USD | | A2 | **Copy hijack** | Copy buttons still emit contract/denom, never the USD string | | A3 | **Pair switch stale USD** | Previous pair’s `$12M` must not linger on a dust / unpriced pair (spoof size) | | A4 | **Client invents TVL** from `volume_quote_24h` / `stats.volume_usd` / CG `liquidity_in_usd` | **forbidden** | | A5 | **LCD N+1** `getPool` on Trade paint | **forbidden** | | A6 | **GET-path DoS** live reserve×oracle in `get_pair` | **forbidden** — stamp read only | | A7 | **`$1` UST1 / `2.5×` USTR** | **forbidden** | | A8 | **vFDUSD / FDUSD** conversion | **forbidden** | | A9 | **Symbol-spoof native** `symbol=USTR` | not priced | | A10 | **Book escrow / wallet LP share** | **forbidden** (privacy + wrong product) | | A11 | **Unpriced as `$0`** | **forbidden** (looks like an empty real pool) | | A12 | **Stats overload** adding `liquidity_usd` to `/stats` and Charts reading the wrong field | **forbidden** | | A13 | **Pool double paint** identity chip + #655 column | `/pool` identity must not take `liquidityUsd` | | A14 | **Nested chrome / lecture** | no `card-glass` chip, no “this is AMM TVL not book” blurb | | A15 | **Scientific JSON** `1e+19` / huge string | `—` / omit; no tab lock | | A16 | **Invalid pair `javascript:` route** | no identity, no LP (**T541-6** / #430) | | A17 | **Gem wash TVL** on production Trade | pair hidden (#562); deep-link does not invent USD | | A18 | **Overview total as pair TVL** | **forbidden** (would show protocol sum on every pair) | ## Verification criteria Ship is done when **all** of the following hold: 1. `make verify-issue-NNN` (new) is green: indexer `GET /pairs/{addr}` stamp/omit/404; Vitest `PairTokenLinks` + Trade + Charts; static check that Trade identity does not call `getPool`. 2. `make verify-issue-541` still green (identity copy/explorer/invert/hide). 3. `make verify-issue-565` still green (Charts Vol USD is still flow). 4. If #655 has landed: `make verify-issue-655` (or its verify target) still green; Trade/Charts USD equals the `/pool` cell for the same pair. 5. LocalTerra smoke: `/trade/{pair}` and `/charts/{pair}` show **v2 LP** `$…` or quiet omit; pair switch; invert; no LCD storm. 6. Docs updated: `docs/frontend.md` token identity, `docs/indexer-invariants.md` single-pair `liquidity_usd`, `skills/AGENTS_FRONTEND_TOKEN_IDENTITY.md`. AGENTS.md verify one-liner if that is house style. 7. Old indexer without the field: chip omitted, no console throw. **Out of scope:** `/pool` sortable **v2 LP USD** column and list `sort=liquidity_usd` ([#655](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/655)); Protocol global TVL ([#569](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/569)); V3 Grid TVL ([#619](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/619)); per-wallet LP on `/portfolio`; Charts 24h Stats extra tile; CG `liquidity_in_usd` rename.
PlasticDigits commented 2026-08-26 04:15:26 +00:00 (Migrated from gitlab.com)

marked as related to #655

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

mentioned in commit cbb45740f0

mentioned in commit cbb45740f08819ea993dd6d81b8b6e4065e02932
PlasticDigits commented 2026-08-26 07:03:54 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1162

mentioned in merge request !1162
PlasticDigits commented 2026-08-26 08:15:50 +00:00 (Migrated from gitlab.com)

mentioned in commit 211055203d

mentioned in commit 211055203dea0adedbaeeca0ae8fef542f8e9cec
PlasticDigits commented 2026-08-26 08:15:56 +00:00 (Migrated from gitlab.com)

mentioned in commit 72071a1016

mentioned in commit 72071a1016fe557a3da70b086cd8fd008dc0318c
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-26 08:15:57 +00:00
PlasticDigits commented 2026-08-26 08:24:22 +00:00 (Migrated from gitlab.com)

mentioned in commit 638e9ca59c

mentioned in commit 638e9ca59c3ac81160e05f4df685ff5398804b69
PlasticDigits commented 2026-08-26 09:30:57 +00:00 (Migrated from gitlab.com)

Merged to main via !1162. Kept formatPairV2LpUsd plus #667 census formatters. Pool table LP USD is !1173 (#655).

Leftover: Trade/Charts identity v2 LP chip after indexer stamp + frontend rebuild. Unstamped pairs omit $. make verify-issue-664.

Merged to `main` via !1162. Kept `formatPairV2LpUsd` plus #667 census formatters. Pool table LP USD is !1173 (#655). Leftover: Trade/Charts identity v2 LP chip after indexer stamp + frontend rebuild. Unstamped pairs omit `$`. `make verify-issue-664`.
PlasticDigits commented 2026-08-26 09:31:35 +00:00 (Migrated from gitlab.com)

mentioned in issue #673

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

mentioned in issue #692

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

mentioned in issue #701

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