feat: /charts UST1/USD hero — default UST1/cUSTC, ?price= token, page-wide invert #680

Closed
opened 2026-08-27 00:20:52 +00:00 by PlasticDigits · 5 comments
PlasticDigits commented 2026-08-27 00:20:52 +00:00 (Migrated from gitlab.com)

Summary

Product feedback: UST1/USD should be the hero on Charts — first chart on /charts, not buried behind volume rank or the #524 other-side default. Bundle as one Charts product surface:

  1. Bare /charts opens the UST1/cUSTC pair (retail “UST1/USTC”) as the default chart.
  2. That chart shows USD of 1 UST1 by default, not USD of 1 cUSTC / USTC.
  3. /charts and /charts/:pairAddr accept a URL parameter naming which pair leg is the priced token.
  4. Every price-facing block on the page follows that token (today several stay factory-oriented after the invert pill).

Do not split default pair, URL param, and leftover invert into separate issues. They share one orientation state.

Related: #524 (display invert + sessionStorage), #543 (invertUsd not 1/x USD), #547 (/charts/:pairAddr deep links), #564 / #565 (24h OHLC + TWAP), #666 (pair-scoped layout), #534 (catalog rank), #226 (finite candles), #151 (non-negative scale), #665 (share URLs strip query), #508 (U1 AMM ≠ mint).

Current codebase

Factory pairs keep a fixed asset_0 / asset_1. Columbus-5 UST1/cUSTC and UST1/USTR were created with UST1 = asset_0. Indexer #466 / #522 price is human quote-per-base; price_usd / candle USD / pair-stats *_usd are USD of 1 human asset_0.

Surface Behavior today
Bare /charts ChartsPage starts with empty selectedPairAddr. activePairAddr = selectedPairAddr || pairOptions[0]. The menu is the current GET /pairs page (sort=volume_24h, limit 50) after sortIndexerPairsByCatalog. There is no UST1/cUSTC pin. Catalog rank groups UST1 hubs then sorts by human quote volume / other-hub symbol — cLUNC/UST1 often ranks ahead of UST1/cUSTC when volumes are 0. Deep link /charts/:pairAddr still works (#547).
Default orientation (T524-3 / C543-3) defaultDisplayInverted is true when factory base is UST1. First visit of UST1/cUSTC shows USD of 1 cUSTC (~$0.005 class). Switch-side shows UST1 USD (~$1). /trade and /charts share usePairDisplayOrientation + sessionStorage key cl8y-dex-trade-pair-invert:{pairAddr}. No query string.
Invert pill PriceChart pair pill toggles the hook. Candles + Last use applyChartDisplayInvert / resolveDisplayTapeLastPriceUsd (invertUsd per bar). Tape TradesTable Price + buy/sell color follow invert. PairTokenLinks reorders chips (payloads stay factory, T541-5).
24h Stats that do not follow invert High / Low / Open / Close (USD) bind factory *_usd via pairStatsUsdField + formatPairStatsUsdOhlc (S564-7, P565-5). Price Change is indexer price_change_pct of factory human open→close (not display-USD). Heading 24h Stats — {label} uses factory indexerPairMenuLabel (no displayInverted).
TWAP formatTwapHumanPrice is factory token1-per-token0 only (S564-5). Invert does not reciprocal.
Volume Vol (USD) is one notional. Vol ({asset_0}) / Vol ({asset_1}) stay factory legs. Tests assert invert must not swap them (CS-5 / P565-5 / Charts V5).
URL helpers chartsPairHref returns /charts/{bech32} only — no search. Hostile javascript: / HTML segments are rejected (chartsPairRoute.ts). buildCanonicalShareUrl({ kind: 'charts' }) is path-only and strips query (TS-2).
Pair stats API GET /api/v1/pairs/{addr}/stats has factory high / low / open_price / close_price / price_change_pct plus *_usd. No *_human OHLC on stats (candles already have additive *_human for #543).

Live columbus-5 hero pair (from #524): UST1/cUSTC terra1ceprjsxp86ggftf5e38wwt34l83e5gq7penkdnv4wsatkwcs8v6qccw55f. LocalTerra address comes from deploy env, not this pin.

Why this is needed

#524 made UST1-as-base markets default to the floating token’s dollars so /trade is a cUSTC/USTR chart. Charts is a different job: visitors opening Charts & Analytics expect the protocol’s UST1/USD print as the first thing they see. Today they can land on another UST1 hub pair (cLUNC/UST1) and/or a cUSTC-priced candle, then click invert and still see factory ~$1 High/Low/Open/Close and an unflipped TWAP. Orientation is also not shareable or bookmarkable — only sessionStorage for this tab.

Without this change:

  1. The Charts landing buries UST1/USD behind catalog/volume order and the other-side default.
  2. The invert control looks broken: headline/candles move, 24h USD OHLC / % / TWAP do not.
  3. Support and social links cannot say “open this chart as UST1” vs “as cUSTC”.

Constraints / guardrails

  1. Indexer + CG/CMC stay factory-oriented. Do not rewrite swap_events.price / price_usd, candle storage, or aggregator last_price. Additive stats *_human (if added) are allowed so the dApp can invertUsd — do not store a reciprocal as factory USD (T524-1, C543).
  2. USD invert is invertUsd(factoryUsd, human) per value (price_usd / price). Never 1 / price_usd on a USD series. Human TWAP / tape Price may use invertFinitePositive / invertOhlc. Drop ≤ 0 / non-finite. No NaN / Infinity into lightweight-charts (T524-6, C543-9, #226). Y-axis stays non-negative (#151). Invert rewrite → setData, not series.update() from the oldest bar.
  3. Do not client-stitch CoinGecko or GET /oracle/history onto pair charts (#568 C568-7). /oracle/price/ust1 stays 400.
  4. Charts product default ≠ Trade default. /charts UST1/cUSTC (and other UST1-as-asset_0 pairs when no ?price=) default to UST1 USD (factory / not inverted). /trade keeps T524-3 (other-side default). Do not let Charts’ new default leak into Trade via the shared cl8y-dex-trade-pair-invert: key. Use a Charts-specific storage prefix or treat URL as Charts source of truth and stop writing Charts defaults into the Trade key.
  5. Hero pair is UST1/cUSTC, not native uusd and not “first catalog pair”. Detect both legs by known UST1 + cUSTC contracts (isUst1Leg + cUSTC allowlist / exact symbol CUSTC / cUSTC after trim/casefold). Never substring-match cUSTC as UST1. Retail copy may say UST1/USTC; identity chips stay cUSTC (#630 natives are a different pair).
  6. Bare /charts only. Valid /charts/:pairAddr deep links must not snap back to the hero (#357 class). Hostile / unknown pair notices stay (CS-11). If UST1/cUSTC is missing (fresh LocalTerra before seed), fall back to firstCatalogPairAddress / first economic pair — do not hang or invent an address.
  7. ?price= allowlist = the two legs of the selected pair only. Accept exact symbol (trim/casefold) or that leg’s terra1 contract. Optional aliases only when that leg is on the pair: USTC → cUSTC; LUNC → cLUNC. Unknown / HTML / javascript: / overlong / path-like values → ignore (product default), do not 404 the pair, do not echo the raw param into HTML or toasts.
  8. Query is canonical on Charts. Pill toggle and ?price= stay in sync (replace: true). Pair switch re-keys the param to a valid leg or drops it. Do not add a second pair contract to the path (T524-9).
  9. Share / clipboard. If Charts grows a Share control, include a validated price query only (allowlisted token). Keep TS-2 “no window.location.href” (no utm, WC URI, mnemonic). Do not hard-code dex.cl8y.com.
  10. Volume is not a price. Invert does not swap Vol (USD) or factory Vol (token) values/decimals (P565-5, C543-8). Optional: reorder volume labels to display order without swapping numbers — if you do, update V5 tests; default is keep factory volume rows.
  11. Leaderboard / freeze / outage / gems unchanged. Pair-scoped board (CS-7–CS-15), #215 banner, F6 freeze, production gem hide (P562) stay. Do not restore charts-overview-*.
  12. U1. Never describe invert or the hero chart as mint/redeem (#508).
  13. Indexer cargo stays on the host. Do not bind-mount indexer/ into root Docker to add stats fields.
  14. /limits standalone stays factory-oriented (T524-10).

Relevant files

Must touch

  • frontend-dapp/src/pages/ChartsPage.tsx — hero auto-pick + navigate; useSearchParams; pass orientation into stats / TWAP / headings; stop binding factory OHLC/% when inverted
  • frontend-dapp/src/utils/chartsPairRoute.ts — parse/serialize price (or agreed name); keep bech32 href hardening
  • frontend-dapp/src/hooks/usePairDisplayOrientation.ts — Charts URL + page-specific default; do not clobber Trade storage
  • frontend-dapp/src/utils/tradePairDisplayOrientation.ts — Charts default helper; param ↔ invert; allowlist match
  • frontend-dapp/src/utils/pairPriceUsd.ts / chartsPairStats.ts — display-USD OHLC + display % (invertUsd / recompute from inverted open/close; not 1/x of USD; not “just negate” price_change_pct)
  • frontend-dapp/src/utils/pairCatalogRank.ts — optional firstUst1CustcPairAddress (do not change /trade firstCatalogPairAddress semantics)
  • frontend-dapp/src/utils/ust1SecondaryMarket.ts — existing UST1 / cUSTC contract pins
  • frontend-dapp/src/utils/pairMenuOptions.ts — Charts headings may pass displayInverted
  • frontend-dapp/src/utils/sharePageLink.ts — only if Share is wired; validated query
  • docs/frontend.md — Charts hero + ?price= + page-wide invert; revise T524-10 / C543-3 for Charts-only default; P565-5 / S564-5 / S564-7 for price tiles
  • skills/AGENTS_FRONTEND_TRADE_PAIR_INVERT.md, skills/AGENTS_FRONTEND_CHARTS_PAIR_SCOPED.md, skills/AGENTS_FRONTEND_CHARTS_PAIR_STATS.md, skills/AGENTS_FRONTEND_USD_CANDLE_INVERT.md

Maybe

  • indexer/src/db/queries/swap_events.rs + frontend-dapp/src/types/index.ts — additive open_human / high_human / low_human / close_human on pair stats so OHLC can invertUsd without guessing. Alternative: derive 24h display OHLC from already-fetched candles. Prefer additive fields or candles — do not 1/x factory *_usd.
  • frontend-dapp/src/pages/TradePage.tsx — optional same ?price= parser for share parity; do not change Trade’s UST1 other-side default.
  • frontend-dapp/src/pages/ChartsPage.test.tsx, chartsPairRoute.test.ts, tradePairDisplayOrientation.test.ts, chartsPairStats.test.ts, pairPriceUsd.test.ts

Do not change (unless adding additive JSON)

  • Indexer candle / CG/CMC meaning, /protocol census, factory wasm, place_limit_order units.
  1. Hero resolve (bare /charts only): find UST1/cUSTC in the loaded pair list (or a one-shot getPairs / known columbus-5 address when VITE_NETWORK=mainnet). navigate('/charts/{addr}?price=UST1', { replace: true }). LocalTerra: match legs from env/UST1_TOKEN_ADDRESS + cUSTC wrap pin, not the mainnet bech32.

  2. Param name: price (token shown as Price USD / display base). Examples: /charts/{ust1custc}?price=UST1, ?price=cUSTC, ?price=USTC (alias). Boolean invert= is a worse retail API.

  3. Orientation resolve order on Charts: valid ?price= → else Charts session key → else Charts product default (UST1-as-base → not inverted). Trade keeps existing order: storage → T524-3.

  4. Page-wide price map (one flag):

    Block Follow price token
    Pair pill, identity chip order, Last, candles, SMA/RSI already / keep invertUsd
    Tape Price + buy/sell color already
    24h High/Low/Open/Close (USD) display USD of 1 priced token
    Price Change % % of that display-USD (or inverted human) series — recompute; do not negate factory %
    TWAP 5m/1h/24h human quote-per-display-base (reciprocal when inverted); still not USD
    24h / TWAP headings display BASE/QUOTE
    Vol (USD) + token vols not prices — unchanged values
    Leaderboard unchanged
  5. Docs: new invariants (suggest C674-1–C674-n) + make verify-issue-<iid>. Update #524/#543 Charts default sentences so agents do not “fix” UST1/cUSTC back to cUSTC USD.

Acceptance criteria

  • C674-1 Bare /charts (no pair segment) selects UST1/cUSTC when that pair exists and replace-navigates to /charts/{addr}?price=UST1 (or equivalent allowlisted form). Chart Last + candles are USD of 1 UST1 (~$1 class), not cUSTC (~$0.005 class).
  • C674-2 /charts/{ust1custc} without price uses the Charts product default (UST1 USD), not T524-3.
  • C674-3 ?price=cUSTC / ?price=USTC / quote-leg contract on that pair shows cUSTC USD on Last, candles, 24h USD OHLC, Price Change, TWAP, headings, tape Price. Pill matches.
  • C674-4 Invert pill updates the URL (replace) and all rows in C674-3. A second click / ?price=UST1 restores UST1 USD everywhere.
  • C674-5 Deep link to another listed pair is kept. ?price= that is not a leg of that pair is ignored (that pair’s Charts default). Invalid pairAddr still shows the existing notice; no stats/leaderboard fetch with the hostile string (CS-11).
  • C674-6 Volume USD and factory token volumes do not change with invert (unless labels-only reorder is explicitly chosen and tested).
  • C674-7 /trade UST1/cUSTC first visit (no Charts storage write) still defaults other-side (T524-3 / C543-3). Convert-on-submit unchanged.
  • C674-8 Missing hero pair → economic fallback, no spinner lock. Production still hides gems (P562).
  • C674-9 No 1/x of factory USD; no CoinGecko stitch; no U1 mint copy; chrome nesting still green.

Test plan (all paths)

Unit / component (Vitest)

  1. chartsPairRoute / new parser: UST1, ust1, cUSTC, USTC alias, leg contract, empty, javascript:alert(1), <script>, //evil, 4k string, token not on pair, price=UST1 on UST1/USTR (valid), price=cUSTC on UST1/USTR (ignore).
  2. Bare /charts with UST1/cUSTC + cLUNC/UST1 + high-volume gem in the page → navigates to UST1/cUSTC + price=UST1; headline ~$1 class.
  3. Bare /charts without UST1/cUSTC → fallback economic pair; no throw.
  4. /charts/{other} does not replace with hero.
  5. Invert pill ↔ ?price= sync; browser back/forward (if implemented) does not desync pill vs tiles.
  6. 24h OHLC + %: fixture factory UST1 ~$1 / human ~206 → inverted tiles are cUSTC dollars and a recomputed %, not 1/1 and not -(factory %).
  7. TWAP 6/6 and 6/18: inverted shows reciprocal human; missing/non-finite → —.
  8. Charts V5 volume unchanged; CS-1–CS-4 still no overview strip.
  9. TradePage #524 describe still other-side default; shared storage isolation test (Charts visit must not flip Trade default).
  10. chartsPairHref still null for javascript: / HTML. Share helper (if touched) only appends allowlisted price.

Indexer (only if additive human stats)

  1. cargo test pair-stats JSON: factory *_usd unchanged; new *_human optional; unknown pair 404; bind $n (no string concat).

Manual / browser

  1. Production-shaped env: open /charts → UST1/cUSTC UST1 USD → copy URL → new session same orientation.
  2. Toggle to cUSTC → 24h High/Low/Open/Close, %, TWAP, tape, heading, Last all move; volume does not.
  3. Desktop + phone; light + dark. Hostile /charts/<script>?price=<script> → invalid-pair notice, no script exec.

Test plan (attack / hack / abuse)

  1. XSS / HTML injection: ?price=<script>, "><img>, encoded %3Cscript%3E. Must never become innerHTML, href, or unescaped text. Match allowlist only; ignore otherwise.
  2. javascript: / data: / protocol-relative: ?price=javascript:alert(1), price=data:text/html,…, price=//evil. Ignore. chartsPairHref / Share still reject non-bech32 pair ids.
  3. Open redirect: ?price=https://evil, ?next=, hash price. Orientation parser must not assign window.location. Share must not use raw location.href (TS-2).
  4. Param smuggling: extra keys mnemonic, WalletConnect URI, showGems=1 (P562-1 forbids). Ignore; do not persist into Share.
  5. Token spoof: ?price=UST1 on a pair whose legs are not UST1; gem symbol spoofing UST1 still hidden in browse (P562-2). Do not treat arbitrary strings as a third priced asset.
  6. Bech32 confusion: price= set to the pair contract or an unrelated terra1. Ignore unless it equals a leg contract.
  7. Length / DoS: 10k+ query, repeated price keys (last vs first — pick one, document, ignore the rest). No unbounded regex on the raw string.
  8. Storage isolation: do not write Charts orientation into Trade’s invert key. Quota / private-mode sessionStorage throw → URL + default still work.
  9. Stale invert / pair race: switch pair while inverted; old ?price=cUSTC must not apply to a pair without cUSTC. Candle setData on rewrite (#524 / #226).
  10. Finance abuse: inverted High/Low must swap after invertUsd so attackers cannot advertise a fake 24h range. Do not treat inverted TWAP as USD or as a limit/settlement price (S564-11).
  11. Indexer injection (if adding fields): pair address bind with sqlx $n. Do not cache unknown-pair 404 as [] (CS-10).
  12. Clickjacking / overlay: invert control stays a real button with an aria-label naming both symbols (T524-11). No new nested card-glass (#653).

Verification criteria

  • make verify-issue-<this-iid> (new script) + existing make verify-issue-524 and make verify-issue-543 still green (Trade other-side default preserved).
  • Charts Vitest: ChartsPage.test.tsx, route/orientation/stats helpers. Chrome nesting: python3 scripts/check_chrome_nesting.py.
  • If indexer stats fields added: cd indexer && cargo test --test api_pairs -- --test-threads=1 (or the suite that covers GET .../stats) on the host (make setup-indexer-postgres). No root Docker cargo bind-mount.
  • Docs/skills updated so #524 C543-3 is explicitly Trade-only; Charts default is UST1 USD.
  • Manual: bare /charts on a stack with UST1/cUSTC listed → UST1 hero; ?price=cUSTC flips all price tiles; /trade first visit still other-side.

Out of scope

  • Changing indexer factory USD meaning, CG/CMC, or /protocol hub card layout.
  • Making /trade default to UST1 USD (separate product call).
  • Native USTC (uusd) as the hero pair.
  • Restoring Charts DEX-wide overview tiles (#666).
## Summary Product feedback: **UST1/USD should be the hero** on Charts — first chart on `/charts`, not buried behind volume rank or the #524 other-side default. Bundle as **one** Charts product surface: 1. Bare `/charts` opens the **UST1/cUSTC** pair (retail “UST1/USTC”) as the default chart. 2. That chart shows **USD of 1 UST1** by default, not USD of 1 cUSTC / USTC. 3. `/charts` and `/charts/:pairAddr` accept a **URL parameter** naming which pair leg is the priced token. 4. **Every price-facing block** on the page follows that token (today several stay factory-oriented after the invert pill). Do **not** split default pair, URL param, and leftover invert into separate issues. They share one orientation state. Related: [#524](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/524) (display invert + sessionStorage), [#543](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/543) (`invertUsd` not `1/x` USD), [#547](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/547) (`/charts/:pairAddr` deep links), [#564](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/564) / [#565](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/565) (24h OHLC + TWAP), [#666](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/666) (pair-scoped layout), [#534](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/534) (catalog rank), [#226](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/226) (finite candles), [#151](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/151) (non-negative scale), [#665](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/665) (share URLs strip query), [#508](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/508) (**U1** AMM ≠ mint). ## Current codebase Factory pairs keep a fixed `asset_0` / `asset_1`. Columbus-5 UST1/cUSTC and UST1/USTR were created with **UST1 = `asset_0`**. Indexer `#466` / `#522` `price` is human quote-per-base; `price_usd` / candle USD / pair-stats `*_usd` are **USD of 1 human `asset_0`**. | Surface | Behavior today | |---------|----------------| | Bare `/charts` | `ChartsPage` starts with empty `selectedPairAddr`. `activePairAddr = selectedPairAddr \|\| pairOptions[0]`. The menu is the current `GET /pairs` page (`sort=volume_24h`, limit 50) after `sortIndexerPairsByCatalog`. There is **no** UST1/cUSTC pin. Catalog rank groups UST1 hubs then sorts by human quote volume / other-hub symbol — **cLUNC/UST1 often ranks ahead of UST1/cUSTC** when volumes are 0. Deep link `/charts/:pairAddr` still works ([#547](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/547)). | | Default orientation (**T524-3** / **C543-3**) | `defaultDisplayInverted` is **true** when factory base is UST1. First visit of UST1/cUSTC shows **USD of 1 cUSTC** (`~$0.005` class). Switch-side shows UST1 USD (`~$1`). `/trade` and `/charts` share `usePairDisplayOrientation` + `sessionStorage` key `cl8y-dex-trade-pair-invert:{pairAddr}`. **No query string.** | | Invert pill | `PriceChart` pair pill toggles the hook. Candles + **Last** use `applyChartDisplayInvert` / `resolveDisplayTapeLastPriceUsd` (`invertUsd` per bar). Tape `TradesTable` Price + buy/sell color follow invert. `PairTokenLinks` reorders chips (payloads stay factory, **T541-5**). | | 24h Stats that **do not** follow invert | High / Low / Open / Close (USD) bind factory `*_usd` via `pairStatsUsdField` + `formatPairStatsUsdOhlc` (**S564-7**, **P565-5**). **Price Change** is indexer `price_change_pct` of factory human open→close (not display-USD). Heading `24h Stats — {label}` uses factory `indexerPairMenuLabel` (no `displayInverted`). | | TWAP | `formatTwapHumanPrice` is factory token1-per-token0 only (**S564-5**). Invert does not reciprocal. | | Volume | **Vol (USD)** is one notional. **Vol ({asset_0}) / Vol ({asset_1})** stay factory legs. Tests assert invert must **not** swap them (**CS-5** / **P565-5** / Charts V5). | | URL helpers | `chartsPairHref` returns `/charts/{bech32}` only — no search. Hostile `javascript:` / HTML segments are rejected (`chartsPairRoute.ts`). `buildCanonicalShareUrl({ kind: 'charts' })` is path-only and **strips query** (**TS-2**). | | Pair stats API | `GET /api/v1/pairs/{addr}/stats` has factory `high` / `low` / `open_price` / `close_price` / `price_change_pct` plus `*_usd`. **No** `*_human` OHLC on stats (candles already have additive `*_human` for #543). | **Live columbus-5 hero pair (from #524):** UST1/cUSTC `terra1ceprjsxp86ggftf5e38wwt34l83e5gq7penkdnv4wsatkwcs8v6qccw55f`. LocalTerra address comes from deploy env, not this pin. ## Why this is needed #524 made UST1-as-base markets default to the **floating** token’s dollars so `/trade` is a cUSTC/USTR chart. Charts is a different job: visitors opening **Charts & Analytics** expect the protocol’s **UST1/USD** print as the first thing they see. Today they can land on another UST1 hub pair (cLUNC/UST1) and/or a cUSTC-priced candle, then click invert and still see factory ~$1 High/Low/Open/Close and an unflipped TWAP. Orientation is also not shareable or bookmarkable — only `sessionStorage` for this tab. Without this change: 1. The Charts landing buries UST1/USD behind catalog/volume order and the other-side default. 2. The invert control looks broken: headline/candles move, 24h USD OHLC / % / TWAP do not. 3. Support and social links cannot say “open this chart as UST1” vs “as cUSTC”. ## Constraints / guardrails 1. **Indexer + CG/CMC stay factory-oriented.** Do not rewrite `swap_events.price` / `price_usd`, candle storage, or aggregator `last_price`. Additive stats `*_human` (if added) are allowed so the dApp can `invertUsd` — do **not** store a reciprocal as factory USD (**T524-1**, **C543**). 2. **USD invert is `invertUsd(factoryUsd, human)` per value** (`price_usd / price`). Never `1 / price_usd` on a USD series. Human TWAP / tape Price may use `invertFinitePositive` / `invertOhlc`. Drop ≤ 0 / non-finite. No `NaN` / `Infinity` into lightweight-charts (**T524-6**, **C543-9**, [#226](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/226)). Y-axis stays non-negative ([#151](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/151)). Invert rewrite → `setData`, not `series.update()` from the oldest bar. 3. **Do not client-stitch CoinGecko or `GET /oracle/history` onto pair charts** ([#568](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/568) **C568-7**). `/oracle/price/ust1` stays **400**. 4. **Charts product default ≠ Trade default.** `/charts` UST1/cUSTC (and other UST1-as-`asset_0` pairs when no `?price=`) default to **UST1 USD** (factory / not inverted). `/trade` **keeps T524-3** (other-side default). Do **not** let Charts’ new default leak into Trade via the shared `cl8y-dex-trade-pair-invert:` key. Use a Charts-specific storage prefix **or** treat URL as Charts source of truth and stop writing Charts defaults into the Trade key. 5. **Hero pair is UST1/cUSTC**, not native `uusd` and not “first catalog pair”. Detect both legs by known UST1 + cUSTC contracts (`isUst1Leg` + cUSTC allowlist / exact symbol `CUSTC` / `cUSTC` after trim/casefold). Never substring-match `cUSTC` as UST1. Retail copy may say **UST1/USTC**; identity chips stay cUSTC (**#630** natives are a different pair). 6. **Bare `/charts` only.** Valid `/charts/:pairAddr` deep links must not snap back to the hero (**#357** class). Hostile / unknown pair notices stay (**CS-11**). If UST1/cUSTC is missing (fresh LocalTerra before seed), fall back to `firstCatalogPairAddress` / first economic pair — do not hang or invent an address. 7. **`?price=` allowlist = the two legs of the selected pair only.** Accept exact symbol (trim/casefold) or that leg’s `terra1` contract. Optional aliases **only when that leg is on the pair**: `USTC` → cUSTC; `LUNC` → cLUNC. Unknown / HTML / `javascript:` / overlong / path-like values → **ignore** (product default), do not 404 the pair, do not echo the raw param into HTML or toasts. 8. **Query is canonical on Charts.** Pill toggle and `?price=` stay in sync (`replace: true`). Pair switch re-keys the param to a valid leg or drops it. Do not add a second pair contract to the path (**T524-9**). 9. **Share / clipboard.** If Charts grows a Share control, include a **validated** `price` query only (allowlisted token). Keep **TS-2** “no `window.location.href`” (no utm, WC URI, mnemonic). Do not hard-code `dex.cl8y.com`. 10. **Volume is not a price.** Invert does **not** swap Vol (USD) or factory Vol (token) values/decimals (**P565-5**, **C543-8**). Optional: reorder volume **labels** to display order without swapping numbers — if you do, update V5 tests; default is keep factory volume rows. 11. **Leaderboard / freeze / outage / gems unchanged.** Pair-scoped board (**CS-7–CS-15**), #215 banner, F6 freeze, production gem hide (**P562**) stay. Do not restore `charts-overview-*`. 12. **U1.** Never describe invert or the hero chart as mint/redeem ([#508](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/508)). 13. **Indexer cargo** stays on the host. Do not bind-mount `indexer/` into root Docker to add stats fields. 14. **`/limits` standalone** stays factory-oriented (**T524-10**). ## Relevant files **Must touch** - `frontend-dapp/src/pages/ChartsPage.tsx` — hero auto-pick + navigate; `useSearchParams`; pass orientation into stats / TWAP / headings; stop binding factory OHLC/% when inverted - `frontend-dapp/src/utils/chartsPairRoute.ts` — parse/serialize `price` (or agreed name); keep bech32 href hardening - `frontend-dapp/src/hooks/usePairDisplayOrientation.ts` — Charts URL + page-specific default; do not clobber Trade storage - `frontend-dapp/src/utils/tradePairDisplayOrientation.ts` — Charts default helper; param ↔ invert; allowlist match - `frontend-dapp/src/utils/pairPriceUsd.ts` / `chartsPairStats.ts` — display-USD OHLC + display `%` (`invertUsd` / recompute from inverted open/close; **not** `1/x` of USD; **not** “just negate” `price_change_pct`) - `frontend-dapp/src/utils/pairCatalogRank.ts` — optional `firstUst1CustcPairAddress` (do not change `/trade` `firstCatalogPairAddress` semantics) - `frontend-dapp/src/utils/ust1SecondaryMarket.ts` — existing UST1 / cUSTC contract pins - `frontend-dapp/src/utils/pairMenuOptions.ts` — Charts headings may pass `displayInverted` - `frontend-dapp/src/utils/sharePageLink.ts` — only if Share is wired; validated query - `docs/frontend.md` — Charts hero + `?price=` + page-wide invert; revise **T524-10** / **C543-3** for Charts-only default; **P565-5** / **S564-5** / **S564-7** for price tiles - `skills/AGENTS_FRONTEND_TRADE_PAIR_INVERT.md`, `skills/AGENTS_FRONTEND_CHARTS_PAIR_SCOPED.md`, `skills/AGENTS_FRONTEND_CHARTS_PAIR_STATS.md`, `skills/AGENTS_FRONTEND_USD_CANDLE_INVERT.md` **Maybe** - `indexer/src/db/queries/swap_events.rs` + `frontend-dapp/src/types/index.ts` — additive `open_human` / `high_human` / `low_human` / `close_human` on pair stats so OHLC can `invertUsd` without guessing. Alternative: derive 24h display OHLC from already-fetched candles. Prefer additive fields or candles — do not `1/x` factory `*_usd`. - `frontend-dapp/src/pages/TradePage.tsx` — optional same `?price=` parser for share parity; **do not** change Trade’s UST1 other-side default. - `frontend-dapp/src/pages/ChartsPage.test.tsx`, `chartsPairRoute.test.ts`, `tradePairDisplayOrientation.test.ts`, `chartsPairStats.test.ts`, `pairPriceUsd.test.ts` **Do not change (unless adding additive JSON)** - Indexer candle / CG/CMC meaning, `/protocol` census, factory wasm, `place_limit_order` units. ## Recommended direction 1. **Hero resolve (bare `/charts` only):** find UST1/cUSTC in the loaded pair list (or a one-shot `getPairs` / known columbus-5 address when `VITE_NETWORK=mainnet`). `navigate('/charts/{addr}?price=UST1', { replace: true })`. LocalTerra: match legs from env/`UST1_TOKEN_ADDRESS` + cUSTC wrap pin, not the mainnet bech32. 2. **Param name:** `price` (token shown as Price USD / display base). Examples: `/charts/{ust1custc}?price=UST1`, `?price=cUSTC`, `?price=USTC` (alias). Boolean `invert=` is a worse retail API. 3. **Orientation resolve order on Charts:** valid `?price=` → else Charts session key → else **Charts product default** (UST1-as-base → not inverted). Trade keeps existing order: storage → **T524-3**. 4. **Page-wide price map** (one flag): | Block | Follow `price` token | |-------|----------------------| | Pair pill, identity chip **order**, **Last**, candles, SMA/RSI | already / keep `invertUsd` | | Tape Price + buy/sell color | already | | 24h High/Low/Open/Close (USD) | display USD of 1 priced token | | Price Change % | % of that display-USD (or inverted human) series — recompute; do not negate factory % | | TWAP 5m/1h/24h | human quote-per-**display**-base (reciprocal when inverted); still not USD | | 24h / TWAP headings | display `BASE/QUOTE` | | Vol (USD) + token vols | **not** prices — unchanged values | | Leaderboard | unchanged | 5. **Docs:** new invariants (suggest **C674-1–C674-n**) + `make verify-issue-<iid>`. Update #524/#543 Charts default sentences so agents do not “fix” UST1/cUSTC back to cUSTC USD. ## Acceptance criteria - [ ] **C674-1** Bare `/charts` (no pair segment) selects UST1/cUSTC when that pair exists and replace-navigates to `/charts/{addr}?price=UST1` (or equivalent allowlisted form). Chart **Last** + candles are USD of 1 UST1 (`~$1` class), not cUSTC (`~$0.005` class). - [ ] **C674-2** `/charts/{ust1custc}` without `price` uses the Charts product default (UST1 USD), not T524-3. - [ ] **C674-3** `?price=cUSTC` / `?price=USTC` / quote-leg contract on that pair shows cUSTC USD on **Last, candles, 24h USD OHLC, Price Change, TWAP, headings, tape Price**. Pill matches. - [ ] **C674-4** Invert pill updates the URL (`replace`) and **all** rows in C674-3. A second click / `?price=UST1` restores UST1 USD everywhere. - [ ] **C674-5** Deep link to another listed pair is kept. `?price=` that is not a leg of **that** pair is ignored (that pair’s Charts default). Invalid `pairAddr` still shows the existing notice; no stats/leaderboard fetch with the hostile string (**CS-11**). - [ ] **C674-6** Volume USD and factory token volumes do not change with invert (unless labels-only reorder is explicitly chosen and tested). - [ ] **C674-7** `/trade` UST1/cUSTC first visit (no Charts storage write) still defaults other-side (**T524-3** / **C543-3**). Convert-on-submit unchanged. - [ ] **C674-8** Missing hero pair → economic fallback, no spinner lock. Production still hides gems (**P562**). - [ ] **C674-9** No `1/x` of factory USD; no CoinGecko stitch; no U1 mint copy; chrome nesting still green. ## Test plan (all paths) **Unit / component (Vitest)** 1. `chartsPairRoute` / new parser: `UST1`, `ust1`, `cUSTC`, `USTC` alias, leg contract, empty, `javascript:alert(1)`, `<script>`, `//evil`, 4k string, token **not** on pair, `price=UST1` on UST1/USTR (valid), `price=cUSTC` on UST1/USTR (ignore). 2. Bare `/charts` with UST1/cUSTC + cLUNC/UST1 + high-volume gem in the page → navigates to UST1/cUSTC + `price=UST1`; headline ~$1 class. 3. Bare `/charts` without UST1/cUSTC → fallback economic pair; no throw. 4. `/charts/{other}` does not replace with hero. 5. Invert pill ↔ `?price=` sync; browser back/forward (if implemented) does not desync pill vs tiles. 6. 24h OHLC + %: fixture factory UST1 ~$1 / human ~206 → inverted tiles are cUSTC dollars and a **recomputed** %, not `1/1` and not `-(factory %)`. 7. TWAP 6/6 and 6/18: inverted shows reciprocal human; missing/non-finite → `—`. 8. Charts V5 volume unchanged; **CS-1–CS-4** still no overview strip. 9. TradePage #524 describe still other-side default; shared storage isolation test (Charts visit must not flip Trade default). 10. `chartsPairHref` still null for `javascript:` / HTML. Share helper (if touched) only appends allowlisted `price`. **Indexer (only if additive human stats)** 11. `cargo test` pair-stats JSON: factory `*_usd` unchanged; new `*_human` optional; unknown pair 404; bind `$n` (no string concat). **Manual / browser** 12. Production-shaped env: open `/charts` → UST1/cUSTC UST1 USD → copy URL → new session same orientation. 13. Toggle to cUSTC → 24h High/Low/Open/Close, %, TWAP, tape, heading, Last all move; volume does not. 14. Desktop + phone; light + dark. Hostile `/charts/<script>?price=<script>` → invalid-pair notice, no script exec. ## Test plan (attack / hack / abuse) 1. **XSS / HTML injection:** `?price=<script>`, `"><img>`, encoded `%3Cscript%3E`. Must never become `innerHTML`, `href`, or unescaped text. Match allowlist only; ignore otherwise. 2. **javascript: / data: / protocol-relative:** `?price=javascript:alert(1)`, `price=data:text/html,…`, `price=//evil`. Ignore. `chartsPairHref` / Share still reject non-bech32 pair ids. 3. **Open redirect:** `?price=https://evil`, `?next=`, hash `price`. Orientation parser must not assign `window.location`. Share must not use raw `location.href` (**TS-2**). 4. **Param smuggling:** extra keys `mnemonic`, WalletConnect URI, `showGems=1` (**P562-1** forbids). Ignore; do not persist into Share. 5. **Token spoof:** `?price=UST1` on a pair whose legs are not UST1; gem symbol spoofing `UST1` still hidden in browse (**P562-2**). Do not treat arbitrary strings as a third priced asset. 6. **Bech32 confusion:** `price=` set to the **pair** contract or an unrelated `terra1`. Ignore unless it equals a **leg** contract. 7. **Length / DoS:** 10k+ query, repeated `price` keys (last vs first — pick one, document, ignore the rest). No unbounded regex on the raw string. 8. **Storage isolation:** do not write Charts orientation into Trade’s invert key. Quota / private-mode `sessionStorage` throw → URL + default still work. 9. **Stale invert / pair race:** switch pair while inverted; old `?price=cUSTC` must not apply to a pair without cUSTC. Candle `setData` on rewrite ([#524](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/524) / [#226](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/226)). 10. **Finance abuse:** inverted High/Low must swap after `invertUsd` so attackers cannot advertise a fake 24h range. Do not treat inverted TWAP as USD or as a limit/settlement price (**S564-11**). 11. **Indexer injection (if adding fields):** pair address bind with sqlx `$n`. Do not cache unknown-pair 404 as `[]` (**CS-10**). 12. **Clickjacking / overlay:** invert control stays a real button with an `aria-label` naming both symbols (**T524-11**). No new nested `card-glass` (**#653**). ## Verification criteria - [ ] `make verify-issue-<this-iid>` (new script) + existing `make verify-issue-524` and `make verify-issue-543` still green (Trade other-side default **preserved**). - [ ] Charts Vitest: `ChartsPage.test.tsx`, route/orientation/stats helpers. Chrome nesting: `python3 scripts/check_chrome_nesting.py`. - [ ] If indexer stats fields added: `cd indexer && cargo test --test api_pairs -- --test-threads=1` (or the suite that covers `GET .../stats`) on the **host** (`make setup-indexer-postgres`). No root Docker cargo bind-mount. - [ ] Docs/skills updated so #524 **C543-3** is explicitly **Trade-only**; Charts default is UST1 USD. - [ ] Manual: bare `/charts` on a stack with UST1/cUSTC listed → UST1 hero; `?price=cUSTC` flips **all** price tiles; `/trade` first visit still other-side. ## Out of scope - Changing indexer factory USD meaning, CG/CMC, or `/protocol` hub card layout. - Making `/trade` default to UST1 USD (separate product call). - Native USTC (`uusd`) as the hero pair. - Restoring Charts DEX-wide overview tiles ([#666](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/666)).
PlasticDigits commented 2026-08-27 00:40:56 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1183

mentioned in merge request !1183
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-27 04:45:26 +00:00
PlasticDigits commented 2026-08-27 04:45:27 +00:00 (Migrated from gitlab.com)

mentioned in commit 479f9012d7

mentioned in commit 479f9012d7e217d4a3ea4b7a65d42530b1ccd474
PlasticDigits commented 2026-08-27 05:29:52 +00:00 (Migrated from gitlab.com)

mentioned in issue #686

mentioned in issue #686
PlasticDigits commented 2026-08-27 05:29:55 +00:00 (Migrated from gitlab.com)

marked as related to #686

marked as related to #686
PlasticDigits commented 2026-08-27 05:30:28 +00:00 (Migrated from gitlab.com)

Merged via !1183 (479f9012) onto main 36d64528.

make verify-issue-680 PASS (6/6). Manual leftover: production-shaped /charts should open UST1/cUSTC with USD of 1 UST1; /trade first visit of that pair stays other-side. Coolify leftover: frontend rebuild. Do not reopen unless C680 is wrong. Post-merge ops: #686.

Merged via !1183 (`479f9012`) onto `main` `36d64528`. `make verify-issue-680` **PASS** (6/6). Manual leftover: production-shaped `/charts` should open UST1/cUSTC with USD of 1 UST1; `/trade` first visit of that pair stays other-side. Coolify leftover: frontend rebuild. Do not reopen unless C680 is wrong. Post-merge ops: #686.
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#680
No description provided.