feat: /charts pair-scoped 24h stats + leaderboard (drop global overview) #666

Closed
opened 2026-08-26 04:17:20 +00:00 by PlasticDigits · 16 comments
PlasticDigits commented 2026-08-26 04:17:20 +00:00 (Migrated from gitlab.com)

Summary

/charts (and /charts/:pairAddr) presents a DEX-wide overview strip (Last 24h vol, USTC/USD, Last 24h trades, Pairs, Tokens) above Find pair, then a second pair 24h Stats block below the candles, then a DEX-wide Trader leaderboard. Retail reading the page after picking a pair cannot tell which numbers belong to the chart they are looking at.

This issue uniquely owns making /charts pair-contextual: headline 24h vol / trades (and the rest of the existing pair 24h Stats) must be for the selected pair, sit below Find pair, must not show Pairs or Tokens (or other DEX-census boxes), and the Leaderboard must rank traders on that pair. Global DEX census stays on /protocol. Unscoped GET /api/v1/overview and unscoped GET /api/v1/traders/leaderboard stay for Protocol, ops, and #657.

Related (do not treat as done): #548 overview USD format; #565 / #564 pair 24h Stats content; #553 leaderboard USD column; #657 Trader page global board; #215 outage banner; #280 leaderboard cache; #653 flat tiles.

Current codebase

Two competing stat strips on one page

frontend-dapp/src/pages/ChartsPage.tsx layout today:

Order Block Data Scope
1 Title Charts & Analytics — —
2 Overview shell-panel lg:grid-cols-5 getOverview() → GET /api/v1/overview Global DEX
3 Find pair (chart-pair-search / chart-pair-select) getPairs + optional getPair Pair picker
4 PriceChart candles for activePairAddr Selected pair
5 24h Stats (charts-pair-24h-stats) getPairStats(activePairAddr) Selected pair
6 TWAP Oracle LCD getTwapPrices / getOracleInfo Selected pair
7 Recent Trades getTrades(activePairAddr) Selected pair
8 Leaderboard getLeaderboard(sort, 20) — no pair Global DEX

Overview boxes (testids charts-overview-*):

  • Last 24h Vol (USD) ← overview.total_volume_24h_usd (all factory swaps)
  • USTC / USD ← overview.ustc_price_usd
  • Last 24h Trades ← overview.total_trades_24h
  • Pairs ← overview.pair_count
  • Tokens ← overview.token_count

Pair 24h Stats already exist and already match the chart (volume_usd, trade_count, OHLC USD, secondary token vols). They render below the chart, not below Find pair. Guard: stats && activePair && activePair.pair_address === activePairAddr.

overviewQuery is also wired into detectMarketDataOutage(overviewQuery, pairsQuery) (#215). Dropping the overview fetch without replacing the outage probes will hide the banner.

Docs still describe the global strip as the Charts overview: docs/frontend.md § Charts overview strip; playbook skills/AGENTS_FRONTEND_CHARTS_OVERVIEW.md (C1–C9). Pair content: § Charts pair 24h stats + skills/AGENTS_FRONTEND_CHARTS_PAIR_STATS.md.

/protocol already owns DEX-wide volume, liquidity, fees, and the USTC/LUNC/vFDUSD oracle card (#550 / #652). Charts should not keep a second census.

Leaderboard is lifetime, all pairs

  const leaderboardQuery = useQuery({
    queryKey: ['leaderboard', leaderboardSort],
    queryFn: () => getLeaderboard(leaderboardSort, 20),
    refetchInterval: 30_000,
  })

getLeaderboard calls GET /api/v1/traders/leaderboard?sort=&limit= with no pair filter. Tabs: total_volume_usd / best_trade_pnl / total_realized_pnl / worst_trade_pnl.

Indexer get_leaderboard is SELECT * FROM traders ORDER BY <allowlisted column>. The traders row is wallet-lifetime across every indexed pair. There is no pair_id on that table.

Per-pair facts already exist elsewhere:

Need Source
Pair 24h vol / trades / OHLC GET /api/v1/pairs/{addr}/stats → swap_events::get_24h_stats_for_pair
Per-wallet swaps on a pair swap_events (pair_id, sender, volume_usd, block_timestamp); index idx_swaps_pair_time
Per-wallet realized P&L on a pair trader_positions unique (trader_address, pair_id) — raw base (realized_pnl, trade_count)

best_trade_pnl / worst_trade_pnl on traders are global per-wallet extrema. They are not stored per pair. Using them on a pair page would rank wallets for activity on other markets.

Leaderboard HTTP cache (#280) is keyed (sort, limit) only ({sort_by}|{limit}), TTL 60s, max 64 entries. A pair filter must join the cache key or pair A can be served pair B’s rows.

#657 wants the same global board on /trader. Charts becoming pair-scoped must not silently change that default.

Vitest: ChartsPage.test.tsx overview strip (GitLab #548) asserts charts-overview-volume-usd / -trades / -pairs / -tokens / -ustc-usd. Those tests must move to Protocol or be replaced with pair-scoped assertions — do not delete the USD/$0/— contracts; relocate them.

Why this is needed

A user who opens /charts or deep-links /charts/{pair} is looking at one market. The first numbers they see (Last 24h vol / trades) are all pairs, and Pairs / Tokens are a factory census that does not describe the chart. The pair’s real 24h Stats sit below the candles, so the mismatch is easy to miss. The Leaderboard then ranks wallets that may never have traded the displayed pair.

That is misleading market data: a quiet pair under a busy DEX still shows large global volume; a leaderboard “winner” may have zero prints on the tape above. Global stats already live on /protocol. Charts should answer “what is this chart doing?” not “how big is the DEX?”

Pair 24h Stats and Recent Trades are already pair-scoped. The leftover work is placement, removing the census boxes, and scoping the board.

Constraints / guardrails

ID Rule
CS-1 /charts and /charts/:pairAddr must not render DEX-wide Pairs or Tokens. Do not render charts-overview-pairs / charts-overview-tokens.
CS-2 Headline Last 24h Vol and Last 24h Trades on Charts are for activePairAddr only (GET /api/v1/pairs/{addr}/stats). Never bind those labels to getOverview() total_volume_24h_usd / total_trades_24h.
CS-3 The pair 24h Stats panel (charts-pair-24h-stats) sits immediately below Find pair and above PriceChart. Do not keep a second vol/trades strip (no duplicate global + pair). TWAP stays below the chart.
CS-4 Drop the Charts overview strip (including USTC / USD). Oracle / census stay on /protocol. Do not break GET /api/v1/overview or Protocol tiles (C1–C9, #548).
CS-5 Keep P565 / S564 / C548 formatters: formatIndexedVolumeUsd / formatChartsOverviewVolumeUsd for USD; formatChartsPairTokenVolume for token vols; unpriced + trades → —; idle trade_count === 0 → $0; never formatNum raw volume_base / total_volume. Trailing-window copy (#576). Invert (#524) does not swap stats legs.
CS-6 One chrome layer (C653-1–C653-4): one shell-panel around the pair stats grid; tiles StatBox variant="flat". No shell-panel inside shell-panel. python3 scripts/check_chrome_nesting.py stays green.
CS-7 Charts Leaderboard is pair-scoped to activePairAddr. Query key must include the pair. Do not fetch the board until a valid pair is selected (skip unknown / invalid deep-link). Empty copy is pair-empty, not “DEX has no traders.”
CS-8 Unscoped GET /api/v1/traders/leaderboard (no pair) stays the DEX-wide board: same allowlisted sorts, limit 1…200, 60s cache. Required for ops runbooks, #657, and GET ?sort=volume_24h. Default API sort remains total_volume.
CS-9 Pair-scoped ranks must not use traders.total_volume_usd / traders.total_realized_pnl / traders.best_trade_pnl / traders.worst_trade_pnl (those are all-pairs). Volume + trade counts from swap_events for that pair_id. Most Profit / Most Loss from trader_positions.realized_pnl for that pair (raw base, same PnlValue honesty as today — do not invent USD P&L here; #560 stays portfolio/trader header). Best Trade has no per-pair store — hide that tab when pair= is set (do not show a global best).
CS-10 pair= is an indexed pair contract address. Unknown → 404 (same as trader history pair=). Bind with sqlx $n — never interpolate. Sort stays VALID_SORTS. Cache key `{sort}
CS-11 Hostile /charts/:pairAddr stays #547 / chartsPairHref: invalid bech32 is not a Link; symbols/addresses are text-only (P565-7). No dangerouslySetInnerHTML. No VITE_* / indexer URL in retail copy.
CS-12 #215: if Charts drops getOverview, outage detection must use remaining indexer queries (pairs, pair-stats, trades, and/or leaderboard). Banner still hides internals.
CS-13 Do not “fix” Sybil / wash ranking (POS-02). Do not change candle ingest, CG/CMC, or /protocol. Do not new-route a second leaderboard path unless pair= on the existing route is insufficient.
CS-14 If a shared TraderLeaderboard is extracted (#657): Charts passes pairAddress; Trader / unscoped callers omit it. Shared React Query key must include pair (['leaderboard', sort, pair ?? 'global']) so the two surfaces do not clobber each other.
CS-15 Pair-scoped aggregation must be pair_id-selective (use idx_swaps_pair_time / trader_positions). Do not seq-scan traders then filter in Rust. Keep limit clamp. Statement cost on a hot pair is a cache + index problem — do not bind-mount indexer cargo as root to “fix” it.

Relevant files

File Role
frontend-dapp/src/pages/ChartsPage.tsx Remove overview strip; move charts-pair-24h-stats; pass pair into leaderboard
frontend-dapp/src/pages/ChartsPage.test.tsx Replace #548 overview-on-Charts asserts; add layout + pair-board tests
frontend-dapp/src/services/indexer/client.ts getLeaderboard(sort, limit, pair?)
frontend-dapp/src/utils/chartsOverviewStats.ts Keep USD formatters (Protocol + pair)
frontend-dapp/src/utils/chartsPairStats.ts Token vol / OHLC / TWAP formatters
frontend-dapp/src/utils/trailingWindowCopy.ts Last 24h labels / titles
frontend-dapp/src/utils/marketDataOutage.ts Outage probe list
frontend-dapp/src/components/trader/PnlValue.tsx P&L cells (reuse)
frontend-dapp/src/pages/ProtocolPage.tsx Unchanged global census (verify no regress)
indexer/src/api/traders.rs LeaderboardQuery.pair, cache key, 404
indexer/src/db/queries/traders.rs Unscoped query stays; pair query is new / sibling
indexer/src/db/queries/positions.rs Per-pair realized_pnl
indexer/src/db/queries/swap_events.rs (or volume) Pair+sender volume aggregates
indexer/tests/api_traders.rs / indexer/tests/security.rs / indexer/tests/trader_volume_usd.rs Unscoped + pair= + injection
docs/frontend.md § Charts overview + pair stats + leaderboard Overview strip leaves Charts; pair layout + pair board
skills/AGENTS_FRONTEND_CHARTS_OVERVIEW.md C1–C9 apply to /protocol + GET /overview, not a Charts strip
skills/AGENTS_FRONTEND_CHARTS_PAIR_STATS.md Placement below Find pair
skills/AGENTS_FRONTEND_TRADER_VOLUME_USD.md Pair-scoped Volume column + #657 global
docs/indexer-invariants.md Leaderboard pair= row
frontend-dapp/e2e/price-chart-smoke.spec.ts Optional: stats below Find pair

Likely new: get_leaderboard_for_pair in traders/positions/swap_events; optional TraderLeaderboard extract if landing with #657. Prefer a composite index (pair_id, sender) on swap_events only if EXPLAIN shows the pair-time index is not enough — measure in the indexer test DB, do not guess.

  1. Frontend layout (no new pair-stats API): delete the overview StatBox grid from ChartsPage. Move the existing charts-pair-24h-stats block to immediately after the Find pair shell-panel, before the chart height wrapper. Keep TWAP / tape / leaderboard order after the chart. Idle pair still shows “No trades yet” in that slot (not the global $0 DEX).
  2. Stop calling getOverview on Charts unless something else needs it. Retarget #215 to pairsQuery + statsQuery (and leaderboard / trades if useful). Protocol keeps getOverview.
  3. Indexer: add optional pair to LeaderboardQuery. Resolve via get_pair_by_address → 404. Unscoped path unchanged. Pair path:
    • Volume tab: GROUP BY sender on swap_events for that pair_id, ORDER BY SUM(volume_usd) DESC NULLS LAST, COUNT(*) as trades. Lifetime-on-this-pair (matches today’s Volume tab, not the 24h stats strip). Return total_volume_usd / total_trades for that pair only in the existing JSON shape so the dApp formatters stay.
    • Most Profit / Most Loss: trader_positions for pair_id, sort realized_pnl DESC / ASC. Map into total_realized_pnl (pair-scoped). Hide Best Trade tab in the Charts UI when pair-scoped.
    • Reject or no-op best_trade_pnl sort when pair is set (400 with the existing invalid-sort pattern) so clients cannot request a global best under a pair key.
  4. Cache: include pair (or -) in the #280 key. Keep TTL 60s and entry cap; pair cardinality will evict more — acceptable. Do not cache 404.
  5. Client: getLeaderboard(sort, limit, pairAddr?). Charts: enabled: !!activePairAddr && !unknownDeepLink, key ['leaderboard', sort, activePairAddr].
  6. #657: if the extract exists or lands in the same window, pairAddress?: string prop. Trader omits it.
  7. Docs / playbooks: Charts overview section becomes “removed; Protocol owns census.” Pair stats section documents below Find pair. Leaderboard section documents pair= vs global.
  8. make verify-issue-548: keep Protocol / overview API coverage. Add make verify-issue-<this> for Charts layout + pair leaderboard (Vitest + indexer api_traders / security). Do not require LocalTerra for the unit/API slice.

Acceptance criteria

  • /charts after a pair is selected: no Pairs box, no Tokens box, no charts-overview-* census strip above Find pair.
  • Last 24h Vol (USD) and Last 24h Trades below Find pair equal GET /api/v1/pairs/{active}/stats (charts-pair-volume-usd + trades), not GET /api/v1/overview.
  • DOM order: Find pair → pair 24h Stats → PriceChart → TWAP → Recent Trades → Leaderboard.
  • Changing Select Pair (or /charts/{other}) updates stats and leaderboard to the new pair; no stale previous-pair numbers (React Query key includes pair).
  • Idle pair: $0 vol when trade_count === 0; unpriced with trades → —; “No trades yet” / empty board as appropriate.
  • Leaderboard Volume values and rank use that pair’s volume; a wallet that only traded pair B does not appear on pair A’s board.
  • Most Profit / Most Loss use that pair’s trader_positions.realized_pnl. Best Trade tab is absent on Charts (pair-scoped).
  • GET /api/v1/traders/leaderboard without pair is unchanged (global). ?pair=<unknown> → 404. ?pair=<valid>&sort=best_trade_pnl → 400.
  • /protocol still shows global volume / oracle. #548 / #550 formatters unchanged.
  • Invalid /charts/javascript:… still shows the invalid-pair notice; no leaderboard fetch with that string.
  • Outage banner still appears when the indexer HTTP API is down; no URL leak.
  • Chrome nesting check green; tiles remain variant="flat".

Test plan (all paths)

Frontend (Vitest — ChartsPage.test.tsx + client)

  • Default /charts: after pairs load, Find pair is above charts-pair-24h-stats; chart container is after stats; queryByTestId('charts-overview-pairs'|'charts-overview-tokens'|'charts-overview-volume-usd'|'charts-overview-ustc-usd') is null.
  • Stats source: getPairStats called with the active pair; getOverview is not called (or not used for those tiles). Volume/trades match the pair mock, not a distinct overview mock.
  • Pair switch: two mocks with different volume_usd / trade_count; after userEvent select, stats and getLeaderboard args update; no leftover first-pair USD.
  • Deep link /charts/{valid}: stats + leaderboard use that address even when it is not on page 0 of getPairs (getPair extra row).
  • Invalid route (charts-invalid-pair-notice): no getPairStats / getLeaderboard with the raw hostile segment.
  • Unknown pair (charts-unknown-pair-notice): no pair stats panel; leaderboard not fetched (or empty + no global rows).
  • Idle / unpriced / priced: $0, —, compact $ — same P565-4 / T553-1 cases, now on the moved panel.
  • #524 invert: toggling invert does not swap Vol (base)/Vol (quote) or change USD (P565-5).
  • Leaderboard: getLeaderboard('total_volume_usd', 20, pairAddr); query not fired without a pair; Best Trade tab not in the tablist; P&L tabs still call pair-scoped sorts; empty “No traders yet”; RetryError refetch; Volume cell still formatIndexedVolumeUsd.
  • #215: pairs + stats (or trades) 502 → banner; retry refetches those queries; body has no VITE_INDEXER_URL.
  • #657 coexistence (if extract landed): Trader page still calls without pair; Charts with pair; caches do not overwrite.
  • Protocol: existing ProtocolPage.test.tsx overview / oracle tests stay green.
  • Client unit: getLeaderboard omits pair when undefined; encodes pair as a query param (not a path injection).

Indexer (cargo test --test api_traders --test security --test trader_volume_usd -- --test-threads=1)

  • Unscoped leaderboard: same sorts, limit clamp 200, default total_volume, USD NULLS LAST (T553-5).
  • Seed pair A (wallet W1 volume) and pair B (wallet W2 volume). ?pair=A&sort=total_volume_usd returns W1 not W2; unscoped can return both.
  • Pair-scoped total_trades counts only that pair’s swap_events.
  • Pair-scoped total_realized_pnl matches trader_positions for that pair, not traders.total_realized_pnl.
  • ?pair=unknown → 404, no SQL leak.
  • ?pair=A&sort=best_trade_pnl (and hacked_column, id) → 400.
  • ?limit=999 still clamps; limit=0 → 1.
  • Cache: two pairs, same sort — responses do not cross. After reset, fresh query.
  • Empty pair (listed, no swaps) → [] 200, not 404.

Docs / static

  • docs/frontend.md Charts overview no longer claims a global strip on /charts.
  • Playbooks updated; make verify-issue-548 still documents Protocol/overview, not the removed strip.
  • python3 scripts/check_chrome_nesting.py + scoped ChartsPage chrome.

E2E / browser (when stack is up)

  • price-chart-smoke (or a small Charts spec): heading, Find pair, pair stats below the Find pair label, chart canvas after stats. 5 Playwright workers per repo convention. Skip when PLAYWRIGHT_SKIP_CHAIN=1.
  • Manual: /charts vs /protocol — Protocol still shows DEX volume / USTC; Charts does not.

Test plan (attack, hack, and abuse)

Vector Expectation
SQL injection in sort or pair ('; DROP TABLE traders;--, 1 OR 1=1) 400 or 404; parameterized SQL only; security.rs style — body has no sqlx / SELECT / postgres.
Cache cross-pair (pair=A then pair=B within 60s) Distinct keys; A’s top wallets never returned for B.
Cache 404 as empty Unknown pair is 404, not a cached [] that later hides a newly indexed pair for 60s.
Cache stampede / pair cardinality DoS Unknown addresses 404 before aggregation; limit ≤ 200; 60s cache; no unbounded SELECT * FROM swap_events. Optional: reject non-bech32 pair with 400 without a DB hit if that matches other routes.
IDOR / pair_id guessing Only contract address, not internal pair_id.
Best-trade smuggle pair= + sort=best_trade_pnl cannot return global traders.best_trade_pnl.
Global/pair mix-up Charts never displays unscoped rows for the selected pair; #657 / curl without pair still global.
XSS in pair symbol / trader address Text nodes / Link to={/trader/${addr}} with existing shorten; no HTML embed. Invalid route segment is status text, not href.
Open redirect / javascript: pair Existing invalid-pair notice; no leaderboard or stats fetch with that string.
Path traversal in client pair Query param only (URLSearchParams); pathSegment if any path use.
Sybil / wash / self-trade Still possible (known). Do not add KYC or hide wash in this ticket. Do not treat leaderboard as settlement.
Unpriced USD as $0 Pair vol and pair-board Volume: trades > 0 and null USD → —, not $0 (C3 / T553-1).
Stale pair after 404 Failed getPair must not keep previous pair’s stats/board (existing setSelectedPairAddr / unknown-deep-link guards).
Error leak 500/404 strings stay generic (internal_err).
Privilege Route stays public read-only; no new auth. Do not expose indexer internals or wallet balances beyond existing trader fields.

Verification criteria

Issue is done when:

  1. A reviewer on /charts (desktop + ~375px) sees Find pair, then this pair’s 24h vol/trades (and existing pair OHLC/token vols), then the chart — no Pairs/Tokens/USTC census strip.
  2. Switching pairs changes those numbers and the leaderboard together; a wallet that only traded the other pair disappears.
  3. GET /api/v1/overview and unscoped leaderboard still satisfy existing #548 / #553 / Protocol tests.
  4. Indexer security tests cover pair= injection, unknown pair 404, and rejected pair+best_trade_pnl.
  5. Docs/playbooks no longer tell agents to put global overview tiles on Charts.
  6. make verify-issue-<iid> (or the listed Vitest + cargo test slice) is green without Docker-bind-mount cargo. Chrome nesting + #215 banner still pass.

Out of scope: redesigning /protocol; Sybil resistance; USD-denominated pair P&L; changing candle USD invert; factory listing rules; new rollup tables unless the pair GROUP BY is proven too slow in CI (then a follow-up, not a silent scope add).

## Summary `/charts` (and `/charts/:pairAddr`) presents a **DEX-wide** overview strip (Last 24h vol, USTC/USD, Last 24h trades, **Pairs**, **Tokens**) **above** Find pair, then a second **pair** 24h Stats block below the candles, then a **DEX-wide** Trader leaderboard. Retail reading the page after picking a pair cannot tell which numbers belong to the chart they are looking at. **This issue uniquely owns making `/charts` pair-contextual:** headline 24h vol / trades (and the rest of the existing pair 24h Stats) must be for the **selected pair**, sit **below Find pair**, must **not** show Pairs or Tokens (or other DEX-census boxes), and the Leaderboard must rank traders on **that pair**. Global DEX census stays on `/protocol`. Unscoped `GET /api/v1/overview` and unscoped `GET /api/v1/traders/leaderboard` stay for Protocol, ops, and [#657](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/657). Related (do **not** treat as done): [#548](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/548) overview USD format; [#565](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/565) / [#564](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/564) pair 24h Stats content; [#553](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/553) leaderboard USD column; [#657](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/657) Trader page **global** board; [#215](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/215) outage banner; [#280](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/280) leaderboard cache; [#653](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/653) flat tiles. ## Current codebase ### Two competing stat strips on one page [`frontend-dapp/src/pages/ChartsPage.tsx`](frontend-dapp/src/pages/ChartsPage.tsx) layout today: | Order | Block | Data | Scope | |-------|--------|------|--------| | 1 | Title **Charts & Analytics** | — | — | | 2 | Overview `shell-panel` `lg:grid-cols-5` | `getOverview()` → `GET /api/v1/overview` | **Global DEX** | | 3 | **Find pair** (`chart-pair-search` / `chart-pair-select`) | `getPairs` + optional `getPair` | Pair picker | | 4 | `PriceChart` | candles for `activePairAddr` | Selected pair | | 5 | **24h Stats** (`charts-pair-24h-stats`) | `getPairStats(activePairAddr)` | Selected pair | | 6 | TWAP Oracle | LCD `getTwapPrices` / `getOracleInfo` | Selected pair | | 7 | Recent Trades | `getTrades(activePairAddr)` | Selected pair | | 8 | **Leaderboard** | `getLeaderboard(sort, 20)` — **no pair** | **Global DEX** | Overview boxes (testids `charts-overview-*`): - **Last 24h Vol (USD)** ← `overview.total_volume_24h_usd` (all factory swaps) - **USTC / USD** ← `overview.ustc_price_usd` - **Last 24h Trades** ← `overview.total_trades_24h` - **Pairs** ← `overview.pair_count` - **Tokens** ← `overview.token_count` Pair 24h Stats already exist and already match the chart (`volume_usd`, `trade_count`, OHLC USD, secondary token vols). They render **below** the chart, not below Find pair. Guard: `stats && activePair && activePair.pair_address === activePairAddr`. `overviewQuery` is also wired into `detectMarketDataOutage(overviewQuery, pairsQuery)` ([#215](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/215)). Dropping the overview fetch without replacing the outage probes will hide the banner. Docs still describe the global strip as the Charts overview: [`docs/frontend.md`](docs/frontend.md) § Charts overview strip; playbook [`skills/AGENTS_FRONTEND_CHARTS_OVERVIEW.md`](skills/AGENTS_FRONTEND_CHARTS_OVERVIEW.md) (**C1–C9**). Pair content: § Charts pair 24h stats + [`skills/AGENTS_FRONTEND_CHARTS_PAIR_STATS.md`](skills/AGENTS_FRONTEND_CHARTS_PAIR_STATS.md). `/protocol` already owns DEX-wide volume, liquidity, fees, and the USTC/LUNC/vFDUSD oracle card ([#550](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/550) / [#652](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/652)). Charts should not keep a second census. ### Leaderboard is lifetime, all pairs ```243:247:frontend-dapp/src/pages/ChartsPage.tsx const leaderboardQuery = useQuery({ queryKey: ['leaderboard', leaderboardSort], queryFn: () => getLeaderboard(leaderboardSort, 20), refetchInterval: 30_000, }) ``` [`getLeaderboard`](frontend-dapp/src/services/indexer/client.ts) calls `GET /api/v1/traders/leaderboard?sort=&limit=` with **no pair filter**. Tabs: `total_volume_usd` / `best_trade_pnl` / `total_realized_pnl` / `worst_trade_pnl`. Indexer [`get_leaderboard`](indexer/src/db/queries/traders.rs) is `SELECT * FROM traders ORDER BY <allowlisted column>`. The `traders` row is **wallet-lifetime across every indexed pair**. There is no `pair_id` on that table. Per-pair facts already exist elsewhere: | Need | Source | |------|--------| | Pair 24h vol / trades / OHLC | `GET /api/v1/pairs/{addr}/stats` → `swap_events::get_24h_stats_for_pair` | | Per-wallet swaps on a pair | `swap_events` (`pair_id`, `sender`, `volume_usd`, `block_timestamp`); index `idx_swaps_pair_time` | | Per-wallet realized P&L on a pair | `trader_positions` unique `(trader_address, pair_id)` — raw **base** (`realized_pnl`, `trade_count`) | `best_trade_pnl` / `worst_trade_pnl` on `traders` are **global** per-wallet extrema. They are **not** stored per pair. Using them on a pair page would rank wallets for activity on *other* markets. Leaderboard HTTP cache ([#280](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/280)) is keyed `(sort, limit)` only (`{sort_by}|{limit}`), TTL 60s, max 64 entries. A pair filter **must** join the cache key or pair A can be served pair B’s rows. [#657](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/657) wants the **same global board** on `/trader`. Charts becoming pair-scoped must not silently change that default. Vitest: [`ChartsPage.test.tsx`](frontend-dapp/src/pages/ChartsPage.test.tsx) **overview strip (GitLab #548)** asserts `charts-overview-volume-usd` / `-trades` / `-pairs` / `-tokens` / `-ustc-usd`. Those tests must move to Protocol or be replaced with pair-scoped assertions — do not delete the USD/`$0`/`—` contracts; relocate them. ## Why this is needed A user who opens `/charts` or deep-links `/charts/{pair}` is looking at **one market**. The first numbers they see (Last 24h vol / trades) are **all pairs**, and Pairs / Tokens are a factory census that does not describe the chart. The pair’s real 24h Stats sit **below** the candles, so the mismatch is easy to miss. The Leaderboard then ranks wallets that may never have traded the displayed pair. That is misleading market data: a quiet pair under a busy DEX still shows large global volume; a leaderboard “winner” may have zero prints on the tape above. Global stats already live on `/protocol`. Charts should answer “what is *this* chart doing?” not “how big is the DEX?” Pair 24h Stats and Recent Trades are already pair-scoped. The leftover work is **placement**, **removing the census boxes**, and **scoping the board**. ## Constraints / guardrails | ID | Rule | |----|------| | **CS-1** | `/charts` and `/charts/:pairAddr` must **not** render DEX-wide **Pairs** or **Tokens**. Do not render `charts-overview-pairs` / `charts-overview-tokens`. | | **CS-2** | Headline **Last 24h Vol** and **Last 24h Trades** on Charts are for `activePairAddr` only (`GET /api/v1/pairs/{addr}/stats`). Never bind those labels to `getOverview()` `total_volume_24h_usd` / `total_trades_24h`. | | **CS-3** | The pair 24h Stats panel (`charts-pair-24h-stats`) sits **immediately below Find pair** and **above** `PriceChart`. Do **not** keep a second vol/trades strip (no duplicate global + pair). TWAP stays below the chart. | | **CS-4** | Drop the Charts **overview** strip (including **USTC / USD**). Oracle / census stay on `/protocol`. Do **not** break `GET /api/v1/overview` or Protocol tiles (**C1–C9**, [#548](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/548)). | | **CS-5** | Keep **P565 / S564 / C548 formatters**: `formatIndexedVolumeUsd` / `formatChartsOverviewVolumeUsd` for USD; `formatChartsPairTokenVolume` for token vols; unpriced + trades → `—`; idle `trade_count === 0` → `$0`; never `formatNum` raw `volume_base` / `total_volume`. Trailing-window copy ([#576](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/576)). Invert ([#524](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/524)) does not swap stats legs. | | **CS-6** | One chrome layer (**C653-1–C653-4**): one `shell-panel` around the pair stats grid; tiles `StatBox variant="flat"`. No `shell-panel` inside `shell-panel`. `python3 scripts/check_chrome_nesting.py` stays green. | | **CS-7** | Charts Leaderboard is **pair-scoped** to `activePairAddr`. Query key **must** include the pair. Do not fetch the board until a valid pair is selected (skip unknown / invalid deep-link). Empty copy is pair-empty, not “DEX has no traders.” | | **CS-8** | Unscoped `GET /api/v1/traders/leaderboard` (no `pair`) stays the **DEX-wide** board: same allowlisted sorts, `limit` 1…200, 60s cache. Required for ops runbooks, `#657`, and `GET ?sort=volume_24h`. Default API sort remains `total_volume`. | | **CS-9** | Pair-scoped ranks **must not** use `traders.total_volume_usd` / `traders.total_realized_pnl` / `traders.best_trade_pnl` / `traders.worst_trade_pnl` (those are all-pairs). Volume + trade counts from `swap_events` for that `pair_id`. Most Profit / Most Loss from `trader_positions.realized_pnl` for that pair (raw base, same `PnlValue` honesty as today — do not invent USD P&L here; [#560](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/560) stays portfolio/trader header). **Best Trade** has no per-pair store — **hide that tab** when `pair=` is set (do not show a global best). | | **CS-10** | `pair=` is an indexed pair **contract address**. Unknown → **404** (same as trader history `pair=`). Bind with sqlx `$n` — never interpolate. Sort stays `VALID_SORTS`. Cache key `{sort}|{limit}|{pair_or_global}`. 404s are not cached as `[]`. | | **CS-11** | Hostile `/charts/:pairAddr` stays [#547](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/547) / `chartsPairHref`: invalid bech32 is not a `Link`; symbols/addresses are text-only (`P565-7`). No `dangerouslySetInnerHTML`. No `VITE_*` / indexer URL in retail copy. | | **CS-12** | [#215](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/215): if Charts drops `getOverview`, outage detection must use remaining indexer queries (`pairs`, `pair-stats`, trades, and/or leaderboard). Banner still hides internals. | | **CS-13** | Do not “fix” Sybil / wash ranking ([POS-02](audits/INTERNAL_KIMIK3_1785897304.md)). Do not change candle ingest, CG/CMC, or `/protocol`. Do not new-route a second leaderboard path unless `pair=` on the existing route is insufficient. | | **CS-14** | If a shared `TraderLeaderboard` is extracted ([#657](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/657)): Charts passes `pairAddress`; Trader / unscoped callers omit it. Shared React Query key must include pair (`['leaderboard', sort, pair ?? 'global']`) so the two surfaces do not clobber each other. | | **CS-15** | Pair-scoped aggregation must be `pair_id`-selective (use `idx_swaps_pair_time` / `trader_positions`). Do not seq-scan `traders` then filter in Rust. Keep `limit` clamp. Statement cost on a hot pair is a cache + index problem — do not bind-mount indexer cargo as root to “fix” it. | ## Relevant files | File | Role | |------|------| | [`frontend-dapp/src/pages/ChartsPage.tsx`](frontend-dapp/src/pages/ChartsPage.tsx) | Remove overview strip; move `charts-pair-24h-stats`; pass pair into leaderboard | | [`frontend-dapp/src/pages/ChartsPage.test.tsx`](frontend-dapp/src/pages/ChartsPage.test.tsx) | Replace #548 overview-on-Charts asserts; add layout + pair-board tests | | [`frontend-dapp/src/services/indexer/client.ts`](frontend-dapp/src/services/indexer/client.ts) | `getLeaderboard(sort, limit, pair?)` | | [`frontend-dapp/src/utils/chartsOverviewStats.ts`](frontend-dapp/src/utils/chartsOverviewStats.ts) | Keep USD formatters (Protocol + pair) | | [`frontend-dapp/src/utils/chartsPairStats.ts`](frontend-dapp/src/utils/chartsPairStats.ts) | Token vol / OHLC / TWAP formatters | | [`frontend-dapp/src/utils/trailingWindowCopy.ts`](frontend-dapp/src/utils/trailingWindowCopy.ts) | Last 24h labels / titles | | [`frontend-dapp/src/utils/marketDataOutage.ts`](frontend-dapp/src/utils/marketDataOutage.ts) | Outage probe list | | [`frontend-dapp/src/components/trader/PnlValue.tsx`](frontend-dapp/src/components/trader/PnlValue.tsx) | P&L cells (reuse) | | [`frontend-dapp/src/pages/ProtocolPage.tsx`](frontend-dapp/src/pages/ProtocolPage.tsx) | Unchanged global census (verify no regress) | | [`indexer/src/api/traders.rs`](indexer/src/api/traders.rs) | `LeaderboardQuery.pair`, cache key, 404 | | [`indexer/src/db/queries/traders.rs`](indexer/src/db/queries/traders.rs) | Unscoped query stays; pair query is new / sibling | | [`indexer/src/db/queries/positions.rs`](indexer/src/db/queries/positions.rs) | Per-pair `realized_pnl` | | [`indexer/src/db/queries/swap_events.rs`](indexer/src/db/queries/swap_events.rs) (or volume) | Pair+sender volume aggregates | | [`indexer/tests/api_traders.rs`](indexer/tests/api_traders.rs) / [`indexer/tests/security.rs`](indexer/tests/security.rs) / [`indexer/tests/trader_volume_usd.rs`](indexer/tests/trader_volume_usd.rs) | Unscoped + pair= + injection | | [`docs/frontend.md`](docs/frontend.md) § Charts overview + pair stats + leaderboard | Overview strip leaves Charts; pair layout + pair board | | [`skills/AGENTS_FRONTEND_CHARTS_OVERVIEW.md`](skills/AGENTS_FRONTEND_CHARTS_OVERVIEW.md) | **C1–C9** apply to `/protocol` + `GET /overview`, not a Charts strip | | [`skills/AGENTS_FRONTEND_CHARTS_PAIR_STATS.md`](skills/AGENTS_FRONTEND_CHARTS_PAIR_STATS.md) | Placement below Find pair | | [`skills/AGENTS_FRONTEND_TRADER_VOLUME_USD.md`](skills/AGENTS_FRONTEND_TRADER_VOLUME_USD.md) | Pair-scoped Volume column + `#657` global | | [`docs/indexer-invariants.md`](docs/indexer-invariants.md) | Leaderboard `pair=` row | | [`frontend-dapp/e2e/price-chart-smoke.spec.ts`](frontend-dapp/e2e/price-chart-smoke.spec.ts) | Optional: stats below Find pair | **Likely new:** `get_leaderboard_for_pair` in traders/positions/swap_events; optional `TraderLeaderboard` extract if landing with #657. Prefer a **composite index** `(pair_id, sender)` on `swap_events` only if EXPLAIN shows the pair-time index is not enough — measure in the indexer test DB, do not guess. ## Recommended direction 1. **Frontend layout (no new pair-stats API):** delete the overview `StatBox` grid from `ChartsPage`. Move the existing `charts-pair-24h-stats` block to immediately after the Find pair `shell-panel`, before the chart height wrapper. Keep TWAP / tape / leaderboard order after the chart. Idle pair still shows “No trades yet” in that slot (not the global `$0` DEX). 2. **Stop calling `getOverview` on Charts** unless something else needs it. Retarget `#215` to `pairsQuery` + `statsQuery` (and leaderboard / trades if useful). Protocol keeps `getOverview`. 3. **Indexer:** add optional `pair` to `LeaderboardQuery`. Resolve via `get_pair_by_address` → 404. Unscoped path unchanged. Pair path: - Volume tab: `GROUP BY sender` on `swap_events` for that `pair_id`, `ORDER BY SUM(volume_usd) DESC NULLS LAST`, `COUNT(*)` as trades. Lifetime-on-this-pair (matches today’s Volume tab, not the 24h stats strip). Return `total_volume_usd` / `total_trades` **for that pair only** in the existing JSON shape so the dApp formatters stay. - Most Profit / Most Loss: `trader_positions` for `pair_id`, sort `realized_pnl` DESC / ASC. Map into `total_realized_pnl` (pair-scoped). Hide Best Trade tab in the Charts UI when pair-scoped. - Reject or no-op `best_trade_pnl` sort when `pair` is set (**400** with the existing invalid-sort pattern) so clients cannot request a global best under a pair key. 4. **Cache:** include pair (or `-`) in the #280 key. Keep TTL 60s and entry cap; pair cardinality will evict more — acceptable. Do not cache 404. 5. **Client:** `getLeaderboard(sort, limit, pairAddr?)`. Charts: `enabled: !!activePairAddr && !unknownDeepLink`, key `['leaderboard', sort, activePairAddr]`. 6. **#657:** if the extract exists or lands in the same window, `pairAddress?: string` prop. Trader omits it. 7. **Docs / playbooks:** Charts overview section becomes “removed; Protocol owns census.” Pair stats section documents **below Find pair**. Leaderboard section documents `pair=` vs global. 8. **`make verify-issue-548`:** keep Protocol / overview API coverage. Add `make verify-issue-<this>` for Charts layout + pair leaderboard (Vitest + indexer `api_traders` / security). Do not require LocalTerra for the unit/API slice. ## Acceptance criteria - [ ] `/charts` after a pair is selected: **no** Pairs box, **no** Tokens box, **no** `charts-overview-*` census strip above Find pair. - [ ] Last 24h Vol (USD) and Last 24h Trades below Find pair equal `GET /api/v1/pairs/{active}/stats` (`charts-pair-volume-usd` + trades), **not** `GET /api/v1/overview`. - [ ] DOM order: Find pair → pair 24h Stats → PriceChart → TWAP → Recent Trades → Leaderboard. - [ ] Changing Select Pair (or `/charts/{other}`) updates stats **and** leaderboard to the new pair; no stale previous-pair numbers (React Query key includes pair). - [ ] Idle pair: `$0` vol when `trade_count === 0`; unpriced with trades → `—`; “No trades yet” / empty board as appropriate. - [ ] Leaderboard Volume values and rank use **that pair’s** volume; a wallet that only traded pair B does not appear on pair A’s board. - [ ] Most Profit / Most Loss use **that pair’s** `trader_positions.realized_pnl`. Best Trade tab is absent on Charts (pair-scoped). - [ ] `GET /api/v1/traders/leaderboard` without `pair` is unchanged (global). `?pair=<unknown>` → 404. `?pair=<valid>&sort=best_trade_pnl` → 400. - [ ] `/protocol` still shows global volume / oracle. `#548` / `#550` formatters unchanged. - [ ] Invalid `/charts/javascript:…` still shows the invalid-pair notice; no leaderboard fetch with that string. - [ ] Outage banner still appears when the indexer HTTP API is down; no URL leak. - [ ] Chrome nesting check green; tiles remain `variant="flat"`. ## Test plan (all paths) ### Frontend (Vitest — `ChartsPage.test.tsx` + client) - [ ] **Default `/charts`:** after pairs load, Find pair is above `charts-pair-24h-stats`; chart container is after stats; `queryByTestId('charts-overview-pairs'|'charts-overview-tokens'|'charts-overview-volume-usd'|'charts-overview-ustc-usd')` is null. - [ ] **Stats source:** `getPairStats` called with the active pair; `getOverview` is **not** called (or not used for those tiles). Volume/trades match the pair mock, not a distinct overview mock. - [ ] **Pair switch:** two mocks with different `volume_usd` / `trade_count`; after `userEvent` select, stats and `getLeaderboard` args update; no leftover first-pair USD. - [ ] **Deep link `/charts/{valid}`:** stats + leaderboard use that address even when it is not on page 0 of `getPairs` (`getPair` extra row). - [ ] **Invalid route** (`charts-invalid-pair-notice`): no `getPairStats` / `getLeaderboard` with the raw hostile segment. - [ ] **Unknown pair** (`charts-unknown-pair-notice`): no pair stats panel; leaderboard not fetched (or empty + no global rows). - [ ] **Idle / unpriced / priced:** `$0`, `—`, compact `$` — same **P565-4** / **T553-1** cases, now on the moved panel. - [ ] **#524 invert:** toggling invert does not swap Vol (base)/Vol (quote) or change USD (**P565-5**). - [ ] **Leaderboard:** `getLeaderboard('total_volume_usd', 20, pairAddr)`; query not fired without a pair; Best Trade tab not in the tablist; P&L tabs still call pair-scoped sorts; empty “No traders yet”; `RetryError` refetch; Volume cell still `formatIndexedVolumeUsd`. - [ ] **#215:** pairs + stats (or trades) 502 → banner; retry refetches those queries; body has no `VITE_INDEXER_URL`. - [ ] **#657 coexistence (if extract landed):** Trader page still calls **without** pair; Charts **with** pair; caches do not overwrite. - [ ] **Protocol:** existing `ProtocolPage.test.tsx` overview / oracle tests stay green. - [ ] **Client unit:** `getLeaderboard` omits `pair` when undefined; encodes `pair` as a query param (not a path injection). ### Indexer (`cargo test --test api_traders --test security --test trader_volume_usd -- --test-threads=1`) - [ ] Unscoped leaderboard: same sorts, limit clamp 200, default `total_volume`, USD NULLS LAST (**T553-5**). - [ ] Seed pair A (wallet W1 volume) and pair B (wallet W2 volume). `?pair=A&sort=total_volume_usd` returns W1 not W2; unscoped can return both. - [ ] Pair-scoped `total_trades` counts only that pair’s `swap_events`. - [ ] Pair-scoped `total_realized_pnl` matches `trader_positions` for that pair, not `traders.total_realized_pnl`. - [ ] `?pair=unknown` → 404, no SQL leak. - [ ] `?pair=A&sort=best_trade_pnl` (and `hacked_column`, `id`) → 400. - [ ] `?limit=999` still clamps; `limit=0` → 1. - [ ] Cache: two pairs, same sort — responses do not cross. After reset, fresh query. - [ ] Empty pair (listed, no swaps) → `[]` 200, not 404. ### Docs / static - [ ] `docs/frontend.md` Charts overview no longer claims a global strip on `/charts`. - [ ] Playbooks updated; `make verify-issue-548` still documents Protocol/overview, not the removed strip. - [ ] `python3 scripts/check_chrome_nesting.py` + scoped `ChartsPage` chrome. ### E2E / browser (when stack is up) - [ ] `price-chart-smoke` (or a small Charts spec): heading, Find pair, pair stats **below** the Find pair label, chart canvas after stats. 5 Playwright workers per repo convention. Skip when `PLAYWRIGHT_SKIP_CHAIN=1`. - [ ] Manual: `/charts` vs `/protocol` — Protocol still shows DEX volume / USTC; Charts does not. ## Test plan (attack, hack, and abuse) | Vector | Expectation | |--------|-------------| | **SQL injection** in `sort` or `pair` (`'; DROP TABLE traders;--`, `1 OR 1=1`) | 400 or 404; parameterized SQL only; `security.rs` style — body has no `sqlx` / `SELECT` / `postgres`. | | **Cache cross-pair** (`pair=A` then `pair=B` within 60s) | Distinct keys; A’s top wallets never returned for B. | | **Cache 404 as empty** | Unknown pair is 404, not a cached `[]` that later hides a newly indexed pair for 60s. | | **Cache stampede / pair cardinality DoS** | Unknown addresses 404 **before** aggregation; `limit` ≤ 200; 60s cache; no unbounded `SELECT * FROM swap_events`. Optional: reject non-bech32 `pair` with 400 without a DB hit if that matches other routes. | | **IDOR / pair_id guessing** | Only contract address, not internal `pair_id`. | | **Best-trade smuggle** | `pair=` + `sort=best_trade_pnl` cannot return global `traders.best_trade_pnl`. | | **Global/pair mix-up** | Charts never displays unscoped rows for the selected pair; `#657` / curl without `pair` still global. | | **XSS** in pair symbol / trader address | Text nodes / `Link to={/trader/${addr}}` with existing shorten; no HTML embed. Invalid route segment is status text, not `href`. | | **Open redirect / `javascript:` pair** | Existing invalid-pair notice; no leaderboard or stats fetch with that string. | | **Path traversal** in client `pair` | Query param only (`URLSearchParams`); `pathSegment` if any path use. | | **Sybil / wash / self-trade** | Still possible (known). Do not add KYC or hide wash in this ticket. Do not treat leaderboard as settlement. | | **Unpriced USD as $0** | Pair vol and pair-board Volume: trades > 0 and null USD → `—`, not `$0` (**C3** / **T553-1**). | | **Stale pair after 404** | Failed `getPair` must not keep previous pair’s stats/board (existing `setSelectedPairAddr` / unknown-deep-link guards). | | **Error leak** | 500/404 strings stay generic (`internal_err`). | | **Privilege** | Route stays public read-only; no new auth. Do not expose indexer internals or wallet balances beyond existing trader fields. | ## Verification criteria Issue is **done** when: 1. A reviewer on `/charts` (desktop + ~375px) sees Find pair, then **this pair’s** 24h vol/trades (and existing pair OHLC/token vols), then the chart — no Pairs/Tokens/USTC census strip. 2. Switching pairs changes those numbers and the leaderboard together; a wallet that only traded the other pair disappears. 3. `GET /api/v1/overview` and unscoped leaderboard still satisfy existing #548 / #553 / Protocol tests. 4. Indexer security tests cover `pair=` injection, unknown pair 404, and rejected pair+`best_trade_pnl`. 5. Docs/playbooks no longer tell agents to put global overview tiles on Charts. 6. `make verify-issue-<iid>` (or the listed Vitest + `cargo test` slice) is green without Docker-bind-mount cargo. Chrome nesting + #215 banner still pass. **Out of scope:** redesigning `/protocol`; Sybil resistance; USD-denominated pair P&L; changing candle USD invert; factory listing rules; new rollup tables unless the pair `GROUP BY` is proven too slow in CI (then a follow-up, not a silent scope add).
PlasticDigits commented 2026-08-26 04:17:22 +00:00 (Migrated from gitlab.com)

marked as related to #548

marked as related to #548
PlasticDigits commented 2026-08-26 04:17:22 +00:00 (Migrated from gitlab.com)

marked as related to #553

marked as related to #553
PlasticDigits commented 2026-08-26 04:17:23 +00:00 (Migrated from gitlab.com)

marked as related to #565

marked as related to #565
PlasticDigits commented 2026-08-26 04:17:24 +00:00 (Migrated from gitlab.com)

marked as related to #564

marked as related to #564
PlasticDigits commented 2026-08-26 04:17:25 +00:00 (Migrated from gitlab.com)

marked as related to #657

marked as related to #657
PlasticDigits commented 2026-08-26 04:17:26 +00:00 (Migrated from gitlab.com)

marked as related to #215

marked as related to #215
PlasticDigits commented 2026-08-26 04:17:26 +00:00 (Migrated from gitlab.com)

marked as related to #280

marked as related to #280
PlasticDigits commented 2026-08-26 04:17:27 +00:00 (Migrated from gitlab.com)

marked as related to #653

marked as related to #653
PlasticDigits commented 2026-08-26 04:17:28 +00:00 (Migrated from gitlab.com)

marked as related to #576

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

mentioned in merge request !1174

mentioned in merge request !1174
PlasticDigits commented 2026-08-26 07:42:24 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1176

mentioned in merge request !1176
PlasticDigits commented 2026-08-26 09:28:53 +00:00 (Migrated from gitlab.com)

mentioned in commit 8af5563c69

mentioned in commit 8af5563c6933f4d9487b3c5af796f80ffcd453e8
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-26 09:28:54 +00:00
PlasticDigits commented 2026-08-26 09:31:00 +00:00 (Migrated from gitlab.com)

mentioned in issue #657

mentioned in issue #657
PlasticDigits commented 2026-08-26 09:31:09 +00:00 (Migrated from gitlab.com)

Merged to main via !1176. Closed draft !1174 was skipped (same #666). Charts no longer calls getOverview() / census tiles. Leaderboard is shared TraderLeaderboard with pairAddress (CS-14); Best Trade hidden; empty copy is pair-empty. Indexer ?pair= 404/400 rules unchanged.

Leftover: Coolify indexer deploy (pair leaderboard query) + frontend rebuild. Confirm /charts layout Find pair → 24h Stats → chart; /protocol still has global census. make verify-issue-666. Optional Playwright e2e/price-chart-smoke.spec.ts when LocalTerra is up.

Merged to `main` via !1176. Closed draft !1174 was skipped (same #666). Charts no longer calls `getOverview()` / census tiles. Leaderboard is shared `TraderLeaderboard` with `pairAddress` (CS-14); Best Trade hidden; empty copy is pair-empty. Indexer `?pair=` 404/400 rules unchanged. Leftover: Coolify indexer deploy (pair leaderboard query) + frontend rebuild. Confirm `/charts` layout Find pair → 24h Stats → chart; `/protocol` still has global census. `make verify-issue-666`. Optional Playwright `e2e/price-chart-smoke.spec.ts` when LocalTerra is up.
PlasticDigits commented 2026-08-26 09:31:34 +00:00 (Migrated from gitlab.com)

mentioned in issue #673

mentioned in issue #673
PlasticDigits commented 2026-08-27 00:20:53 +00:00 (Migrated from gitlab.com)

mentioned in issue #680

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