feat: /charts pair-scoped 24h stats + leaderboard (drop global overview) #666
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:implement
agent:implement
agent:open_issues
agent:ready
agent:research
agent:security_audit
agent:verify
architecture
backend
blocker:hybrid
blocker:launch
blocker:limit-orders
blocker:v2
block:log_only
block:security
bug
ci
contracts
correctness
deploy
dev
devops
docs
documentation
duplicate
e2e
enhancement
epic
feature
frontend
functional-completion
gas
good first issue
governance
help wanted
high-risk
hooks
hybrid
indexer
infra
infrastructure
integrators
invalid
launch-blocker
limit-orders
localnet
localterra
low priority
missing-implementation
needs-design
ops
performance
priority
high
priority
medium
product
qa
QA
question
ready
ready
research
scripts
security
security-hardening
smartcontracts
tech-debt
testing
ux
UX
v2
verification
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-dex-terraclassic#666
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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
/chartspair-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. UnscopedGET /api/v1/overviewand unscopedGET /api/v1/traders/leaderboardstay 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.tsxlayout today:shell-panellg:grid-cols-5getOverview()→GET /api/v1/overviewchart-pair-search/chart-pair-select)getPairs+ optionalgetPairPriceChartactivePairAddrcharts-pair-24h-stats)getPairStats(activePairAddr)getTwapPrices/getOracleInfogetTrades(activePairAddr)getLeaderboard(sort, 20)— no pairOverview boxes (testids
charts-overview-*):overview.total_volume_24h_usd(all factory swaps)overview.ustc_price_usdoverview.total_trades_24hoverview.pair_countoverview.token_countPair 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.overviewQueryis also wired intodetectMarketDataOutage(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; playbookskills/AGENTS_FRONTEND_CHARTS_OVERVIEW.md(C1–C9). Pair content: § Charts pair 24h stats +skills/AGENTS_FRONTEND_CHARTS_PAIR_STATS.md./protocolalready 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
getLeaderboardcallsGET /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_leaderboardisSELECT * FROM traders ORDER BY <allowlisted column>. Thetradersrow is wallet-lifetime across every indexed pair. There is nopair_idon that table.Per-pair facts already exist elsewhere:
GET /api/v1/pairs/{addr}/stats→swap_events::get_24h_stats_for_pairswap_events(pair_id,sender,volume_usd,block_timestamp); indexidx_swaps_pair_timetrader_positionsunique(trader_address, pair_id)— raw base (realized_pnl,trade_count)best_trade_pnl/worst_trade_pnlontradersare 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.tsxoverview strip (GitLab #548) assertscharts-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
/chartsor 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
/chartsand/charts/:pairAddrmust not render DEX-wide Pairs or Tokens. Do not rendercharts-overview-pairs/charts-overview-tokens.activePairAddronly (GET /api/v1/pairs/{addr}/stats). Never bind those labels togetOverview()total_volume_24h_usd/total_trades_24h.charts-pair-24h-stats) sits immediately below Find pair and abovePriceChart. Do not keep a second vol/trades strip (no duplicate global + pair). TWAP stays below the chart./protocol. Do not breakGET /api/v1/overviewor Protocol tiles (C1–C9, #548).formatIndexedVolumeUsd/formatChartsOverviewVolumeUsdfor USD;formatChartsPairTokenVolumefor token vols; unpriced + trades →—; idletrade_count === 0→$0; neverformatNumrawvolume_base/total_volume. Trailing-window copy (#576). Invert (#524) does not swap stats legs.shell-panelaround the pair stats grid; tilesStatBox variant="flat". Noshell-panelinsideshell-panel.python3 scripts/check_chrome_nesting.pystays green.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.”GET /api/v1/traders/leaderboard(nopair) stays the DEX-wide board: same allowlisted sorts,limit1…200, 60s cache. Required for ops runbooks,#657, andGET ?sort=volume_24h. Default API sort remainstotal_volume.traders.total_volume_usd/traders.total_realized_pnl/traders.best_trade_pnl/traders.worst_trade_pnl(those are all-pairs). Volume + trade counts fromswap_eventsfor thatpair_id. Most Profit / Most Loss fromtrader_positions.realized_pnlfor that pair (raw base, samePnlValuehonesty as today — do not invent USD P&L here; #560 stays portfolio/trader header). Best Trade has no per-pair store — hide that tab whenpair=is set (do not show a global best).pair=is an indexed pair contract address. Unknown → 404 (same as trader historypair=). Bind with sqlx$n— never interpolate. Sort staysVALID_SORTS. Cache key `{sort}/charts/:pairAddrstays #547 /chartsPairHref: invalid bech32 is not aLink; symbols/addresses are text-only (P565-7). NodangerouslySetInnerHTML. NoVITE_*/ indexer URL in retail copy.getOverview, outage detection must use remaining indexer queries (pairs,pair-stats, trades, and/or leaderboard). Banner still hides internals./protocol. Do not new-route a second leaderboard path unlesspair=on the existing route is insufficient.TraderLeaderboardis extracted (#657): Charts passespairAddress; 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.pair_id-selective (useidx_swaps_pair_time/trader_positions). Do not seq-scantradersthen filter in Rust. Keeplimitclamp. Statement cost on a hot pair is a cache + index problem — do not bind-mount indexer cargo as root to “fix” it.Relevant files
frontend-dapp/src/pages/ChartsPage.tsxcharts-pair-24h-stats; pass pair into leaderboardfrontend-dapp/src/pages/ChartsPage.test.tsxfrontend-dapp/src/services/indexer/client.tsgetLeaderboard(sort, limit, pair?)frontend-dapp/src/utils/chartsOverviewStats.tsfrontend-dapp/src/utils/chartsPairStats.tsfrontend-dapp/src/utils/trailingWindowCopy.tsfrontend-dapp/src/utils/marketDataOutage.tsfrontend-dapp/src/components/trader/PnlValue.tsxfrontend-dapp/src/pages/ProtocolPage.tsxindexer/src/api/traders.rsLeaderboardQuery.pair, cache key, 404indexer/src/db/queries/traders.rsindexer/src/db/queries/positions.rsrealized_pnlindexer/src/db/queries/swap_events.rs(or volume)indexer/tests/api_traders.rs/indexer/tests/security.rs/indexer/tests/trader_volume_usd.rsdocs/frontend.md§ Charts overview + pair stats + leaderboardskills/AGENTS_FRONTEND_CHARTS_OVERVIEW.md/protocol+GET /overview, not a Charts stripskills/AGENTS_FRONTEND_CHARTS_PAIR_STATS.mdskills/AGENTS_FRONTEND_TRADER_VOLUME_USD.md#657globaldocs/indexer-invariants.mdpair=rowfrontend-dapp/e2e/price-chart-smoke.spec.tsLikely new:
get_leaderboard_for_pairin traders/positions/swap_events; optionalTraderLeaderboardextract if landing with #657. Prefer a composite index(pair_id, sender)onswap_eventsonly if EXPLAIN shows the pair-time index is not enough — measure in the indexer test DB, do not guess.Recommended direction
StatBoxgrid fromChartsPage. Move the existingcharts-pair-24h-statsblock to immediately after the Find pairshell-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$0DEX).getOverviewon Charts unless something else needs it. Retarget#215topairsQuery+statsQuery(and leaderboard / trades if useful). Protocol keepsgetOverview.pairtoLeaderboardQuery. Resolve viaget_pair_by_address→ 404. Unscoped path unchanged. Pair path:GROUP BY senderonswap_eventsfor thatpair_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). Returntotal_volume_usd/total_tradesfor that pair only in the existing JSON shape so the dApp formatters stay.trader_positionsforpair_id, sortrealized_pnlDESC / ASC. Map intototal_realized_pnl(pair-scoped). Hide Best Trade tab in the Charts UI when pair-scoped.best_trade_pnlsort whenpairis set (400 with the existing invalid-sort pattern) so clients cannot request a global best under a pair key.-) in the #280 key. Keep TTL 60s and entry cap; pair cardinality will evict more — acceptable. Do not cache 404.getLeaderboard(sort, limit, pairAddr?). Charts:enabled: !!activePairAddr && !unknownDeepLink, key['leaderboard', sort, activePairAddr].pairAddress?: stringprop. Trader omits it.pair=vs global.make verify-issue-548: keep Protocol / overview API coverage. Addmake verify-issue-<this>for Charts layout + pair leaderboard (Vitest + indexerapi_traders/ security). Do not require LocalTerra for the unit/API slice.Acceptance criteria
/chartsafter a pair is selected: no Pairs box, no Tokens box, nocharts-overview-*census strip above Find pair.GET /api/v1/pairs/{active}/stats(charts-pair-volume-usd+ trades), notGET /api/v1/overview./charts/{other}) updates stats and leaderboard to the new pair; no stale previous-pair numbers (React Query key includes pair).$0vol whentrade_count === 0; unpriced with trades →—; “No trades yet” / empty board as appropriate.trader_positions.realized_pnl. Best Trade tab is absent on Charts (pair-scoped).GET /api/v1/traders/leaderboardwithoutpairis unchanged (global).?pair=<unknown>→ 404.?pair=<valid>&sort=best_trade_pnl→ 400./protocolstill shows global volume / oracle.#548/#550formatters unchanged./charts/javascript:…still shows the invalid-pair notice; no leaderboard fetch with that string.variant="flat".Test plan (all paths)
Frontend (Vitest —
ChartsPage.test.tsx+ client)/charts: after pairs load, Find pair is abovecharts-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.getPairStatscalled with the active pair;getOverviewis not called (or not used for those tiles). Volume/trades match the pair mock, not a distinct overview mock.volume_usd/trade_count; afteruserEventselect, stats andgetLeaderboardargs update; no leftover first-pair USD./charts/{valid}: stats + leaderboard use that address even when it is not on page 0 ofgetPairs(getPairextra row).charts-invalid-pair-notice): nogetPairStats/getLeaderboardwith the raw hostile segment.charts-unknown-pair-notice): no pair stats panel; leaderboard not fetched (or empty + no global rows).$0,—, compact$— same P565-4 / T553-1 cases, now on the moved panel.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”;RetryErrorrefetch; Volume cell stillformatIndexedVolumeUsd.VITE_INDEXER_URL.ProtocolPage.test.tsxoverview / oracle tests stay green.getLeaderboardomitspairwhen undefined; encodespairas a query param (not a path injection).Indexer (
cargo test --test api_traders --test security --test trader_volume_usd -- --test-threads=1)total_volume, USD NULLS LAST (T553-5).?pair=A&sort=total_volume_usdreturns W1 not W2; unscoped can return both.total_tradescounts only that pair’sswap_events.total_realized_pnlmatchestrader_positionsfor that pair, nottraders.total_realized_pnl.?pair=unknown→ 404, no SQL leak.?pair=A&sort=best_trade_pnl(andhacked_column,id) → 400.?limit=999still clamps;limit=0→ 1.[]200, not 404.Docs / static
docs/frontend.mdCharts overview no longer claims a global strip on/charts.make verify-issue-548still documents Protocol/overview, not the removed strip.python3 scripts/check_chrome_nesting.py+ scopedChartsPagechrome.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 whenPLAYWRIGHT_SKIP_CHAIN=1./chartsvs/protocol— Protocol still shows DEX volume / USTC; Charts does not.Test plan (attack, hack, and abuse)
sortorpair('; DROP TABLE traders;--,1 OR 1=1)security.rsstyle — body has nosqlx/SELECT/postgres.pair=Athenpair=Bwithin 60s)[]that later hides a newly indexed pair for 60s.limit≤ 200; 60s cache; no unboundedSELECT * FROM swap_events. Optional: reject non-bech32pairwith 400 without a DB hit if that matches other routes.pair_id.pair=+sort=best_trade_pnlcannot return globaltraders.best_trade_pnl.#657/ curl withoutpairstill global.Link to={/trader/${addr}}with existing shorten; no HTML embed. Invalid route segment is status text, nothref.javascript:pairpairURLSearchParams);pathSegmentif any path use.—, not$0(C3 / T553-1).getPairmust not keep previous pair’s stats/board (existingsetSelectedPairAddr/ unknown-deep-link guards).internal_err).Verification criteria
Issue is done when:
/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.GET /api/v1/overviewand unscoped leaderboard still satisfy existing #548 / #553 / Protocol tests.pair=injection, unknown pair 404, and rejected pair+best_trade_pnl.make verify-issue-<iid>(or the listed Vitest +cargo testslice) 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 pairGROUP BYis proven too slow in CI (then a follow-up, not a silent scope add).marked as related to #548
marked as related to #553
marked as related to #565
marked as related to #564
marked as related to #657
marked as related to #215
marked as related to #280
marked as related to #653
marked as related to #576
mentioned in merge request !1174
mentioned in merge request !1176
mentioned in commit
8af5563c69mentioned in issue #657
Merged to
mainvia !1176. Closed draft !1174 was skipped (same #666). Charts no longer callsgetOverview()/ census tiles. Leaderboard is sharedTraderLeaderboardwithpairAddress(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
/chartslayout Find pair → 24h Stats → chart;/protocolstill has global census.make verify-issue-666. Optional Playwrighte2e/price-chart-smoke.spec.tswhen LocalTerra is up.mentioned in issue #673
mentioned in issue #680