Trader: show Charts leaderboard at bottom of /trader and /trader/:address #657

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

Summary

Show the existing Charts Trader leaderboard at the bottom of /trader and /trader/:address page content (last section in app-main-content, above the Layout legal footer). Reuse the Charts table — do not invent a second ranking or USD formula.

This is one product change: empty lookup (/trader) and profile (/trader/:address) both need the same board so Trader is a discovery surface, not only a paste-an-address form. /portfolio stays wallet-home and is out of scope.

Current codebase

Trader routes have no leaderboard

TraderPage.tsx is the only UI for:

Route When What renders today
/trader paramAddr empty Title + search / My Profile / My Portfolio + empty prompt. No board.
/trader/:address valid bech32 getTrader + TraderSummaryStats + TraderPositionsTable + Trade History (getTraderTrades limit 100). No board.

Routes are wired in App.tsx as TraderRouteShell (/trader and /trader/:address) with resetKeys on the address segment (#126). Nav label Trader is under More (navItems.ts).

Profile queries:

  • GET /api/v1/traders/{addr} via getTrader → parseIndexerTraderPayload (malformed JSON is a React Query error, not a crash).
  • Positions + trades are separate queries. 404 → “Trader not found…” (#177). Indexer transport failure → MarketDataServiceOutageBanner (#215).

The page root is <div className="space-y-4">. There is no leaderboard query and getLeaderboard is not mocked in TraderPage.test.tsx. E2E smoke e2e/trader-page.spec.ts only asserts heading + positions.

Leaderboard already exists on Charts

ChartsPage.tsx mounts a Leaderboard shell-panel-strong after Recent Trades:

  • useQuery key ['leaderboard', leaderboardSort], getLeaderboard(sort, 20), refetchInterval: 30_000.
  • Tabs (LEADERBOARD_TABS, private to Charts): total_volume_usd / best_trade_pnl / total_realized_pnl / worst_trade_pnl — labels Volume (USD) / Best Trade / Most Profit / Most Loss.
  • Volume uses formatIndexedVolumeUsd on total_volume_usd (T553-1). Never formatNum(total_volume).
  • P&L tabs use PnlValue.
  • Row link: /trader/${trader.address} via shortenAddress(..., 10, 6).
  • States: Skeleton, RetryError (“Failed to load leaderboard”), “No traders yet”, table aria-label="Trader leaderboard".
  • Volume cell data-testid="charts-leaderboard-volume".

Client: getLeaderboard → GET /api/v1/traders/leaderboard?sort=&limit=. Default sort in the helper is total_volume_usd; Charts always passes an explicit sort.

Indexer API (reuse only)

indexer/src/api/traders.rs GET /api/v1/traders/leaderboard:

  • Valid sorts include total_volume, total_volume_usd, rolling volumes, trade count, P&L fields, total_fees_paid.
  • API default sort is still total_volume (raw). The dApp must request total_volume_usd for the Volume tab (T553-5).
  • limit clamped 1…200. Charts uses 20.
  • 60s in-process cache keyed (sort, limit) (#280). Do not add an uncached fan-out.
  • getLeaderboard does not run parseIndexerTraderPayload per row (profile-only). Do not regress Charts; if you add row parsing, keep Charts tests green.

Layout.tsx: header → <main className="app-main-shell"> (Outlet) → <footer className="app-footer-shell"> (EnvironmentRibbon + legal notice) → mobile bottom nav. On small viewports .app-footer-shell already pads above --app-mobile-nav-stack. The board must live in page content (last sibling in TraderPage), never inside app-footer-shell or the mobile nav.

/portfolio shares TraderSummaryStats / positions / trades but is the connected wallet home (#212). Do not add the board there in this issue.

Why this is needed

Trader in More is a dead-end unless the user already knows a bech32. The only retail leaderboard is buried at the bottom of /charts after pair overview, candles, and tape. Users who open Trader to find wallets (or who land on a 404 / empty lookup) cannot browse ranks without leaving the page.

Charts already implements the product: USD volume rank, three P&L tabs, 20 rows, links into /trader/:addr. Duplicating that markup on Trader without extracting a shared component will fork T553-1–T553-6 (raw total_volume printing 10,000,000T, $0 vs — for unpriced activity).

Constraints / guardrails

ID Rule
TL-1 Leaderboard is the last section of Trader page content. Document order: search → profile/empty/error/outage → (when loaded) stats → positions → trade history → leaderboard. Then Layout footer. Never nest the board in the footer or in the Trade History shell-panel-strong.
TL-2 Render the board on both /trader and /trader/:address, including loading, 404, and profile outage. Profile query failure must not unmount the board. Board has its own loading / empty / RetryError.
TL-3 Same product as Charts: same four tabs, same default total_volume_usd, same limit=20, same refetchInterval: 30_000, same formatters (formatIndexedVolumeUsd, PnlValue). Prefer React Query key ['leaderboard', sort] so Charts and Trader share cache.
TL-4 T553-1 / T553-5: never display raw total_volume as Volume. Volume tab sorts and displays total_volume_usd. Unpriced → —. Idle (total_trades === 0) → $0. Do not add rolling volume_24h / 7d / 30d columns (raw API-only).
TL-5 One chrome layer (C653-1): one shell-panel-strong around the table + tabs. No shell-panel* inside shell-panel*. No card-glass per row. Flat metric tiles stay on TraderSummaryStats only. python3 scripts/check_chrome_nesting.py must stay green.
TL-6 Copy (#489): heading Leaderboard (match Charts). No Sybil lecture, no “see also Charts”, no indexer URL / VITE_* in retail strings. Outage copy stays the existing banner + board RetryError.
TL-7 No indexer / contract change. Do not new-route, change #280 TTL, raise Charts’ 20, or “fix” Sybil/wash ranking (POS-02). Document the known limitation in docs/frontend.md only if you mention ranking.
TL-8 /portfolio unchanged. Do not add a second USD formula. Do not show total_fees_paid (still — / unused on Charts). Links stay /trader/{addr} with encodeURIComponent / existing Link. Address text is shortenAddress (text node, not dangerouslySetInnerHTML).
TL-9 Optional: highlight the current :address row when it appears in the top 20 (aria-current="page" or a row class). Do not invent “you are rank N” if the wallet is not in the page — that needs another API.
TL-10 #126 / #215 / #177 stay: parse-or-fail profile, outage banner vs 404, resetKeys on address change. Board tab state may reset on address change (same page remount via Outlet key={pathname}).

Relevant files

File Role
frontend-dapp/src/pages/TraderPage.tsx Mount board as last content section
frontend-dapp/src/pages/ChartsPage.tsx Source table; extract shared component
frontend-dapp/src/pages/TraderPage.test.tsx Empty / profile / 404 / outage + board
frontend-dapp/src/pages/ChartsPage.test.tsx Keep #553 Volume USD tests
frontend-dapp/src/services/indexer/client.ts getLeaderboard — reuse
frontend-dapp/src/utils/chartsOverviewStats.ts formatIndexedVolumeUsd
frontend-dapp/src/components/trader/PnlValue.tsx P&L cells
frontend-dapp/src/components/common/Layout.tsx Footer / mobile nav — do not edit for this
frontend-dapp/e2e/trader-page.spec.ts Smoke: board visible above footer
docs/frontend.md § Charts trader leaderboard + § Trader profile Document Trader placement
skills/AGENTS_FRONTEND_TRADER_VOLUME_USD.md Note shared component / Trader surface
indexer/src/api/traders.rs Existing API + cache (read-only)

Likely new: frontend-dapp/src/components/trader/TraderLeaderboard.tsx (+ unit tests). Charts becomes a thin wrapper.

  1. Extract the Charts leaderboard block (tabs + query + table + empty/error) into TraderLeaderboard.
  2. Charts keeps the same heading, testids (charts-leaderboard-volume or a shared testid that Charts tests are updated to), and visual order after Recent Trades.
  3. TraderPage renders <TraderLeaderboard /> (optional highlightAddress={traderAddr \|\| undefined}) as the last child of the page root — outside {trader && (…)} so empty / 404 / outage still show the board.
  4. Mock getLeaderboard in TraderPage.test.tsx (same pattern as Charts).
  5. Extend e2e/trader-page.spec.ts (and empty /trader if cheap): heading Leaderboard, table or empty copy, section above footer.app-footer-shell.
  6. Docs: one paragraph under Trader profile + a cross-link from Charts trader leaderboard. No new playbook unless the extract is large enough to deserve AGENTS_FRONTEND_TRADER_LEADERBOARD.md.

Acceptance criteria

  • /trader (no address): after the empty prompt, a Leaderboard section appears; Layout footer is still below it.
  • /trader/:address (found): board is below Trade History and above the footer.
  • /trader/:address 404 and indexer outage: board still mounts; profile RetryError / outage banner unchanged; board errors are independent.
  • Volume tab: getLeaderboard('total_volume_usd', 20); USD compact; no raw T from total_volume; unpriced —; idle $0.
  • P&L tabs match Charts labels and PnlValue fields.
  • Row links navigate to /trader/{address}; current-profile row may be highlighted if in the list.
  • Empty indexer: “No traders yet”. Load failure: RetryError, no VITE_INDEXER_URL / 127.0.0.1 in copy.
  • Charts leaderboard unchanged in behavior; #553 tests still pass.
  • /portfolio has no new board.
  • make verify-issue-653 / chrome nesting still passes. Light + dark, ~375px and ~1280px: table scrolls horizontally if needed; last rows are not hidden under footer or mobile nav.
  • Docs/skills updated as above.

Test plan — functional paths

Path Setup Expect
Empty lookup /trader, getLeaderboard → rows Board below empty prompt; Search / My Profile unchanged
Empty lookup, no traders [] “No traders yet”
Empty lookup, board fail reject Board RetryError; search still works
Profile found /trader/{addr}, profile + trades + board Order: stats → positions → history → board → footer
Profile 404 getTrader 404, board OK “Trader not found”; no outage banner; board visible
Profile outage getTrader 502, board OK Outage banner; board visible
Both fail profile 502 + board reject Banner + board RetryError; no env/host leak
Volume USD USTR-scale total_volume + priced total_volume_usd $ compact; not 10,000,000T
Unpriced total_volume_usd null, total_trades > 0 — not $0
Idle row total_trades === 0 $0
Tab: Best Trade / Most Profit / Most Loss click tabs getLeaderboard called with matching sort; P&L cells
Row click click shortened addr /trader/{addr}
Highlight board contains current :address Row marked current; others not
Address switch A → B Profile resetKeys; board still last; shared query cache OK
Invalid search garbage in search box No navigate; board unaffected
Charts regression /charts Same board after Recent Trades; #553 tests
Portfolio /portfolio No Trader leaderboard / aria-label="Trader leaderboard" unless it already existed (it does not)
Mobile 375 /trader Board above footer; footer padding clears bottom nav
Desktop 1280 /trader/:addr Full table; no nested chrome
Theme light + dark --ink on --panel-bg (C653-8)
E2E smoke connected wallet /trader/{dev} Board visible; no new console errors beyond allowlist

Vitest: TraderPage.test.tsx, extracted TraderLeaderboard tests, existing ChartsPage.test.tsx #553 cases. Playwright: extend e2e/trader-page.spec.ts; workers stay 5 for smoke.

Test plan — attack, hack, and abuse

Leaderboard is unauthenticated read of indexed ranks. This issue must not widen write surface or weaken #280 / #126 guards.

Vector Attack Expect
XSS in address Indexer/proxy returns <script> / javascript: in address Render as text via shortenAddress + React Link to={/trader/${addr}}. No dangerouslySetInnerHTML. Prefer skip/omit rows that fail isValidTerraAddress / parse. Click must not become javascript:.
Open redirect address = https://evil or //evil Link stays path-absolute /trader/…. No window.location = address.
HTML in P&L / volume strings "<img onerror=…>" PnlValue / formatIndexedVolumeUsd text only.
Prototype pollution JSON __proto__ / constructor rows Treat as invalid row or query error; page does not crash (#126 class).
Sort injection UI or forged query sort=total_volume;drop / unknown key Client only sends tab keys. Server already 400s unknown sort — do not add a free-text sort control.
Limit abuse Client limit=1e9 or 0 Keep Charts’ 20. Do not expose a limit input. Server clamps 1–200.
Cache stampede Hammer /trader + /charts Shared React Query key; indexer 60s cache unchanged. No per-row getTrader N+1.
Cache poisoning (UI) Stale board after tab change Query key includes sort; Volume vs P&L cannot show the other tab’s numbers.
Sybil / wash (POS-02) Many wallets cycle volume Known limitation. Do not claim anti-Sybil. Do not hide the board. Optional docs note only — no retail banner.
Rank spoof Client-only reorder Rank # is list index of server order. Do not re-sort in the UI.
Mixed-unit lie Show raw total_volume as USD Forbidden (T553-1). Attacker-sized USTR legs must not print T as dollars.
Stale / poisoned USD Oracle/hub poison in total_volume_usd Display-only; same as Charts. No new oracle fetch on Trader.
Outage copy leak 502 / timeout No VITE_INDEXER_URL, host:port, or stack in banner/RetryError.
Clickjack / tabnab Board links Same-origin SPA Link only. rel not required for internal routes.
CSRF / write GET leaderboard Read-only. No wallet signature. Do not add POST.
CSV / formula injection N/A Board is not a CSV export. Do not pipe ranks through #432 CSV helpers.
IDOR Viewing /trader/{other} Public indexed stats already. Board is global, not “this wallet’s private rank”.
Gem / hidden markets Wash on soft-launch gems Ranking stays indexer-global (same as Charts). Do not filter gems in this issue (discovery filter is #562 on pickers, not trader ranks).
Route crash Partial array body from proxy Board query error → RetryError. Must not take down search or (when present) profile. If you parse rows, drop bad rows or fail the query — no throw during render.

Verification criteria

Done when all of the following are true:

  1. Manual: /trader and /trader/:address (found, 404, disconnect) show Leaderboard as the last main-content block; legal footer + mobile nav unchanged and not overlapped.
  2. getLeaderboard is called with total_volume_usd + 20 on first paint; tab changes request the matching sort only.
  3. make test-frontend (or scoped Vitest) covers Trader board paths + Charts #553.
  4. e2e/trader-page.spec.ts (or added spec) asserts the board on the connected profile path.
  5. make verify-issue-553 and make verify-issue-653 still pass.
  6. docs/frontend.md states Trader hosts the same board; skill #553 mentions the shared component.
  7. git grep / review: no second formatNum(total_volume) on the new surface; no board on PortfolioPage.tsx; no indexer diff required.

Out of scope

  • /portfolio leaderboard
  • New sort tabs (24h volume, fees, trade count)
  • “Your rank” outside the top 20
  • Indexer Sybil / min-liquidity filters (POS-02)
  • Changing #280 cache, default API sort, or limit clamp
  • Wallet signatures, CSV, or notifications

Dependencies

  • Blocked by: none (API + Charts UI already shipped: #553, #280)
  • Related: #126 profile parse, #215 outage copy, #177 404 vs outage, #653 chrome, #489 copy, #551 / #560 P&L (do not restyle summary tiles here)

Labels / owner / priority

frontend UX enhancement missing-implementation docs testing e2e

Owner: frontend
Priority: P2

## Summary Show the existing Charts **Trader leaderboard** at the bottom of `/trader` and `/trader/:address` page content (last section in `app-main-content`, above the Layout legal footer). Reuse the Charts table — do not invent a second ranking or USD formula. This is one product change: empty lookup (`/trader`) and profile (`/trader/:address`) both need the same board so Trader is a discovery surface, not only a paste-an-address form. `/portfolio` stays wallet-home and is **out of scope**. ## Current codebase ### Trader routes have no leaderboard [`TraderPage.tsx`](frontend-dapp/src/pages/TraderPage.tsx) is the only UI for: | Route | When | What renders today | |-------|------|--------------------| | `/trader` | `paramAddr` empty | Title + search / My Profile / My Portfolio + empty prompt. **No board.** | | `/trader/:address` | valid bech32 | `getTrader` + `TraderSummaryStats` + `TraderPositionsTable` + Trade History (`getTraderTrades` limit 100). **No board.** | Routes are wired in [`App.tsx`](frontend-dapp/src/App.tsx) as `TraderRouteShell` (`/trader` and `/trader/:address`) with `resetKeys` on the address segment ([#126](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/126)). Nav label **Trader** is under More ([`navItems.ts`](frontend-dapp/src/components/common/navItems.ts)). Profile queries: - `GET /api/v1/traders/{addr}` via `getTrader` → [`parseIndexerTraderPayload`](frontend-dapp/src/services/indexer/traderProfilePayload.ts) (malformed JSON is a React Query error, not a crash). - Positions + trades are separate queries. 404 → “Trader not found…” ([#177](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/177)). Indexer transport failure → `MarketDataServiceOutageBanner` ([#215](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/215)). The page root is `<div className="space-y-4">`. There is no leaderboard query and `getLeaderboard` is not mocked in [`TraderPage.test.tsx`](frontend-dapp/src/pages/TraderPage.test.tsx). E2E smoke [`e2e/trader-page.spec.ts`](frontend-dapp/e2e/trader-page.spec.ts) only asserts heading + positions. ### Leaderboard already exists on Charts [`ChartsPage.tsx`](frontend-dapp/src/pages/ChartsPage.tsx) mounts a **Leaderboard** `shell-panel-strong` **after** Recent Trades: - `useQuery` key `['leaderboard', leaderboardSort]`, `getLeaderboard(sort, 20)`, `refetchInterval: 30_000`. - Tabs (`LEADERBOARD_TABS`, private to Charts): `total_volume_usd` / `best_trade_pnl` / `total_realized_pnl` / `worst_trade_pnl` — labels **Volume (USD)** / **Best Trade** / **Most Profit** / **Most Loss**. - Volume uses [`formatIndexedVolumeUsd`](frontend-dapp/src/utils/chartsOverviewStats.ts) on `total_volume_usd` (**T553-1**). Never `formatNum(total_volume)`. - P&L tabs use [`PnlValue`](frontend-dapp/src/components/trader/PnlValue.tsx). - Row link: `/trader/${trader.address}` via `shortenAddress(..., 10, 6)`. - States: Skeleton, `RetryError` (“Failed to load leaderboard”), “No traders yet”, table `aria-label="Trader leaderboard"`. - Volume cell `data-testid="charts-leaderboard-volume"`. Client: [`getLeaderboard`](frontend-dapp/src/services/indexer/client.ts) → `GET /api/v1/traders/leaderboard?sort=&limit=`. Default sort in the helper is `total_volume_usd`; Charts always passes an explicit sort. ### Indexer API (reuse only) [`indexer/src/api/traders.rs`](indexer/src/api/traders.rs) `GET /api/v1/traders/leaderboard`: - Valid sorts include `total_volume`, `total_volume_usd`, rolling volumes, trade count, P&L fields, `total_fees_paid`. - **API default sort is still `total_volume` (raw).** The dApp must request `total_volume_usd` for the Volume tab (**T553-5**). - `limit` clamped `1…200`. Charts uses **20**. - 60s in-process cache keyed `(sort, limit)` ([#280](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/280)). Do not add an uncached fan-out. - `getLeaderboard` does **not** run `parseIndexerTraderPayload` per row (profile-only). Do not regress Charts; if you add row parsing, keep Charts tests green. ### Shell placement (footer is Layout, not the page) [`Layout.tsx`](frontend-dapp/src/components/common/Layout.tsx): `header` → `<main className="app-main-shell">` (`Outlet`) → `<footer className="app-footer-shell">` (EnvironmentRibbon + legal notice) → mobile bottom nav. On small viewports `.app-footer-shell` already pads above `--app-mobile-nav-stack`. The board must live in **page** content (last sibling in `TraderPage`), never inside `app-footer-shell` or the mobile nav. `/portfolio` shares `TraderSummaryStats` / positions / trades but is the connected wallet home ([#212](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/212)). **Do not** add the board there in this issue. ## Why this is needed **Trader** in More is a dead-end unless the user already knows a bech32. The only retail leaderboard is buried at the bottom of `/charts` after pair overview, candles, and tape. Users who open Trader to find wallets (or who land on a 404 / empty lookup) cannot browse ranks without leaving the page. Charts already implements the product: USD volume rank, three P&L tabs, 20 rows, links into `/trader/:addr`. Duplicating that markup on Trader without extracting a shared component will fork **T553-1–T553-6** (raw `total_volume` printing `10,000,000T`, `$0` vs `—` for unpriced activity). ## Constraints / guardrails | ID | Rule | |----|------| | **TL-1** | Leaderboard is the **last** section of Trader page content. Document order: search → profile/empty/error/outage → (when loaded) stats → positions → trade history → **leaderboard**. Then Layout footer. Never nest the board in the footer or in the Trade History `shell-panel-strong`. | | **TL-2** | Render the board on **both** `/trader` and `/trader/:address`, including loading, 404, and profile outage. Profile query failure must not unmount the board. Board has its own loading / empty / `RetryError`. | | **TL-3** | Same product as Charts: same four tabs, same default `total_volume_usd`, same `limit=20`, same `refetchInterval: 30_000`, same formatters (`formatIndexedVolumeUsd`, `PnlValue`). Prefer React Query key `['leaderboard', sort]` so Charts and Trader share cache. | | **TL-4** | **T553-1 / T553-5:** never display raw `total_volume` as Volume. Volume tab **sorts and displays** `total_volume_usd`. Unpriced → `—`. Idle (`total_trades === 0`) → `$0`. Do not add rolling `volume_24h` / `7d` / `30d` columns (raw API-only). | | **TL-5** | One chrome layer (**C653-1**): one `shell-panel-strong` around the table + tabs. No `shell-panel*` inside `shell-panel*`. No `card-glass` per row. Flat metric tiles stay on `TraderSummaryStats` only. `python3 scripts/check_chrome_nesting.py` must stay green. | | **TL-6** | Copy (**#489**): heading **Leaderboard** (match Charts). No Sybil lecture, no “see also Charts”, no indexer URL / `VITE_*` in retail strings. Outage copy stays the existing banner + board `RetryError`. | | **TL-7** | **No indexer / contract change.** Do not new-route, change `#280` TTL, raise Charts’ 20, or “fix” Sybil/wash ranking ([POS-02](audits/INTERNAL_KIMIK3_1785897304.md)). Document the known limitation in `docs/frontend.md` only if you mention ranking. | | **TL-8** | `/portfolio` unchanged. Do not add a second USD formula. Do not show `total_fees_paid` (still `—` / unused on Charts). Links stay `/trader/{addr}` with `encodeURIComponent` / existing `Link`. Address text is `shortenAddress` (text node, not `dangerouslySetInnerHTML`). | | **TL-9** | Optional: highlight the current `:address` row when it appears in the top 20 (`aria-current="page"` or a row class). **Do not** invent “you are rank N” if the wallet is not in the page — that needs another API. | | **TL-10** | `#126` / `#215` / `#177` stay: parse-or-fail profile, outage banner vs 404, `resetKeys` on address change. Board tab state may reset on address change (same page remount via `Outlet key={pathname}`). | ## Relevant files | File | Role | |------|------| | [`frontend-dapp/src/pages/TraderPage.tsx`](frontend-dapp/src/pages/TraderPage.tsx) | Mount board as last content section | | [`frontend-dapp/src/pages/ChartsPage.tsx`](frontend-dapp/src/pages/ChartsPage.tsx) | Source table; extract shared component | | [`frontend-dapp/src/pages/TraderPage.test.tsx`](frontend-dapp/src/pages/TraderPage.test.tsx) | Empty / profile / 404 / outage + board | | [`frontend-dapp/src/pages/ChartsPage.test.tsx`](frontend-dapp/src/pages/ChartsPage.test.tsx) | Keep **#553** Volume USD tests | | [`frontend-dapp/src/services/indexer/client.ts`](frontend-dapp/src/services/indexer/client.ts) | `getLeaderboard` — reuse | | [`frontend-dapp/src/utils/chartsOverviewStats.ts`](frontend-dapp/src/utils/chartsOverviewStats.ts) | `formatIndexedVolumeUsd` | | [`frontend-dapp/src/components/trader/PnlValue.tsx`](frontend-dapp/src/components/trader/PnlValue.tsx) | P&L cells | | [`frontend-dapp/src/components/common/Layout.tsx`](frontend-dapp/src/components/common/Layout.tsx) | Footer / mobile nav — do not edit for this | | [`frontend-dapp/e2e/trader-page.spec.ts`](frontend-dapp/e2e/trader-page.spec.ts) | Smoke: board visible above footer | | [`docs/frontend.md`](docs/frontend.md) § Charts trader leaderboard + § Trader profile | Document Trader placement | | [`skills/AGENTS_FRONTEND_TRADER_VOLUME_USD.md`](skills/AGENTS_FRONTEND_TRADER_VOLUME_USD.md) | Note shared component / Trader surface | | [`indexer/src/api/traders.rs`](indexer/src/api/traders.rs) | Existing API + cache (read-only) | **Likely new:** `frontend-dapp/src/components/trader/TraderLeaderboard.tsx` (+ unit tests). Charts becomes a thin wrapper. ## Recommended direction 1. Extract the Charts leaderboard block (tabs + query + table + empty/error) into `TraderLeaderboard`. 2. Charts keeps the same heading, testids (`charts-leaderboard-volume` **or** a shared testid that Charts tests are updated to), and visual order after Recent Trades. 3. `TraderPage` renders `<TraderLeaderboard />` (optional `highlightAddress={traderAddr \|\| undefined}`) as the **last** child of the page root — **outside** `{trader && (…)}` so empty / 404 / outage still show the board. 4. Mock `getLeaderboard` in `TraderPage.test.tsx` (same pattern as Charts). 5. Extend `e2e/trader-page.spec.ts` (and empty `/trader` if cheap): heading **Leaderboard**, table or empty copy, section above `footer.app-footer-shell`. 6. Docs: one paragraph under Trader profile + a cross-link from Charts trader leaderboard. No new playbook unless the extract is large enough to deserve `AGENTS_FRONTEND_TRADER_LEADERBOARD.md`. ## Acceptance criteria - [ ] `/trader` (no address): after the empty prompt, a **Leaderboard** section appears; Layout footer is still below it. - [ ] `/trader/:address` (found): board is below Trade History and above the footer. - [ ] `/trader/:address` 404 and indexer outage: board still mounts; profile RetryError / outage banner unchanged; board errors are independent. - [ ] Volume tab: `getLeaderboard('total_volume_usd', 20)`; USD compact; no raw `T` from `total_volume`; unpriced `—`; idle `$0`. - [ ] P&L tabs match Charts labels and `PnlValue` fields. - [ ] Row links navigate to `/trader/{address}`; current-profile row may be highlighted if in the list. - [ ] Empty indexer: “No traders yet”. Load failure: RetryError, no `VITE_INDEXER_URL` / `127.0.0.1` in copy. - [ ] Charts leaderboard unchanged in behavior; `#553` tests still pass. - [ ] `/portfolio` has no new board. - [ ] `make verify-issue-653` / chrome nesting still passes. Light + dark, ~375px and ~1280px: table scrolls horizontally if needed; last rows are not hidden under footer or mobile nav. - [ ] Docs/skills updated as above. ## Test plan — functional paths | Path | Setup | Expect | |------|--------|--------| | Empty lookup | `/trader`, `getLeaderboard` → rows | Board below empty prompt; Search / My Profile unchanged | | Empty lookup, no traders | `[]` | “No traders yet” | | Empty lookup, board fail | reject | Board RetryError; search still works | | Profile found | `/trader/{addr}`, profile + trades + board | Order: stats → positions → history → board → footer | | Profile 404 | `getTrader` 404, board OK | “Trader not found”; **no** outage banner; board visible | | Profile outage | `getTrader` 502, board OK | Outage banner; board visible | | Both fail | profile 502 + board reject | Banner + board RetryError; no env/host leak | | Volume USD | USTR-scale `total_volume` + priced `total_volume_usd` | `$` compact; not `10,000,000T` | | Unpriced | `total_volume_usd` null, `total_trades > 0` | `—` not `$0` | | Idle row | `total_trades === 0` | `$0` | | Tab: Best Trade / Most Profit / Most Loss | click tabs | `getLeaderboard` called with matching sort; P&L cells | | Row click | click shortened addr | `/trader/{addr}` | | Highlight | board contains current `:address` | Row marked current; others not | | Address switch | A → B | Profile `resetKeys`; board still last; shared query cache OK | | Invalid search | garbage in search box | No navigate; board unaffected | | Charts regression | `/charts` | Same board after Recent Trades; `#553` tests | | Portfolio | `/portfolio` | No `Trader leaderboard` / `aria-label="Trader leaderboard"` unless it already existed (it does not) | | Mobile 375 | `/trader` | Board above footer; footer padding clears bottom nav | | Desktop 1280 | `/trader/:addr` | Full table; no nested chrome | | Theme | light + dark | `--ink` on `--panel-bg` (**C653-8**) | | E2E smoke | connected wallet `/trader/{dev}` | Board visible; no new console errors beyond allowlist | Vitest: `TraderPage.test.tsx`, extracted `TraderLeaderboard` tests, existing `ChartsPage.test.tsx` #553 cases. Playwright: extend `e2e/trader-page.spec.ts`; workers stay **5** for smoke. ## Test plan — attack, hack, and abuse Leaderboard is **unauthenticated read** of indexed ranks. This issue must not widen write surface or weaken `#280` / `#126` guards. | Vector | Attack | Expect | |--------|--------|--------| | XSS in `address` | Indexer/proxy returns `<script>` / `javascript:` in `address` | Render as text via `shortenAddress` + React `Link` `to={/trader/${addr}}`. No `dangerouslySetInnerHTML`. Prefer skip/omit rows that fail `isValidTerraAddress` / parse. Click must not become `javascript:`. | | Open redirect | `address` = `https://evil` or `//evil` | `Link` stays path-absolute `/trader/…`. No `window.location = address`. | | HTML in P&L / volume strings | `"<img onerror=…>"` | `PnlValue` / `formatIndexedVolumeUsd` text only. | | Prototype pollution | JSON `__proto__` / `constructor` rows | Treat as invalid row or query error; page does not crash (#126 class). | | Sort injection | UI or forged query `sort=total_volume;drop` / unknown key | Client only sends tab keys. Server already 400s unknown sort — do not add a free-text sort control. | | Limit abuse | Client `limit=1e9` or `0` | Keep Charts’ **20**. Do not expose a limit input. Server clamps 1–200. | | Cache stampede | Hammer `/trader` + `/charts` | Shared React Query key; indexer 60s cache unchanged. No per-row `getTrader` N+1. | | Cache poisoning (UI) | Stale board after tab change | Query key includes `sort`; Volume vs P&L cannot show the other tab’s numbers. | | Sybil / wash (POS-02) | Many wallets cycle volume | **Known limitation.** Do not claim anti-Sybil. Do not hide the board. Optional docs note only — no retail banner. | | Rank spoof | Client-only reorder | Rank `#` is list index of server order. Do not re-sort in the UI. | | Mixed-unit lie | Show raw `total_volume` as USD | Forbidden (**T553-1**). Attacker-sized USTR legs must not print `T` as dollars. | | Stale / poisoned USD | Oracle/hub poison in `total_volume_usd` | Display-only; same as Charts. No new oracle fetch on Trader. | | Outage copy leak | 502 / timeout | No `VITE_INDEXER_URL`, host:port, or stack in banner/RetryError. | | Clickjack / tabnab | Board links | Same-origin SPA `Link` only. `rel` not required for internal routes. | | CSRF / write | GET leaderboard | Read-only. No wallet signature. Do not add POST. | | CSV / formula injection | N/A | Board is not a CSV export. Do not pipe ranks through `#432` CSV helpers. | | IDOR | Viewing `/trader/{other}` | Public indexed stats already. Board is global, not “this wallet’s private rank”. | | Gem / hidden markets | Wash on soft-launch gems | Ranking stays indexer-global (same as Charts). Do not filter gems in this issue (discovery filter is #562 on pickers, not trader ranks). | | Route crash | Partial array body from proxy | Board query error → RetryError. Must not take down search or (when present) profile. If you parse rows, drop bad rows or fail the query — no throw during render. | ## Verification criteria Done when all of the following are true: 1. Manual: `/trader` and `/trader/:address` (found, 404, disconnect) show **Leaderboard** as the last main-content block; legal footer + mobile nav unchanged and not overlapped. 2. `getLeaderboard` is called with `total_volume_usd` + `20` on first paint; tab changes request the matching sort only. 3. `make test-frontend` (or scoped Vitest) covers Trader board paths + Charts #553. 4. `e2e/trader-page.spec.ts` (or added spec) asserts the board on the connected profile path. 5. `make verify-issue-553` and `make verify-issue-653` still pass. 6. `docs/frontend.md` states Trader hosts the same board; skill #553 mentions the shared component. 7. `git grep` / review: no second `formatNum(total_volume)` on the new surface; no board on `PortfolioPage.tsx`; no indexer diff required. ## Out of scope - `/portfolio` leaderboard - New sort tabs (24h volume, fees, trade count) - “Your rank” outside the top 20 - Indexer Sybil / min-liquidity filters (POS-02) - Changing `#280` cache, default API sort, or `limit` clamp - Wallet signatures, CSV, or notifications ## Dependencies - Blocked by: none (API + Charts UI already shipped: [#553](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/553), [#280](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/280)) - Related: [#126](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/126) profile parse, [#215](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/215) outage copy, [#177](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/177) 404 vs outage, [#653](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/653) chrome, [#489](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/489) copy, [#551](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/551) / [#560](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/560) P&L (do not restyle summary tiles here) ## Labels / owner / priority `frontend` `UX` `enhancement` `missing-implementation` `docs` `testing` `e2e` **Owner:** frontend **Priority:** P2
PlasticDigits commented 2026-08-26 03:08:07 +00:00 (Migrated from gitlab.com)

marked as related to #553

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

marked as related to #126

marked as related to #126
PlasticDigits commented 2026-08-26 03:08:09 +00:00 (Migrated from gitlab.com)

marked as related to #215

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

marked as related to #177

marked as related to #177
PlasticDigits commented 2026-08-26 03:08:10 +00:00 (Migrated from gitlab.com)

marked as related to #653

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

marked as related to #489

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

mentioned in issue #665

mentioned in issue #665
PlasticDigits commented 2026-08-26 04:16:16 +00:00 (Migrated from gitlab.com)

marked as related to #665

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

mentioned in issue #666

mentioned in issue #666
PlasticDigits commented 2026-08-26 04:17:25 +00:00 (Migrated from gitlab.com)

marked as related to #666

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

mentioned in commit 52fbb4286c

mentioned in commit 52fbb4286c95699d2ccec11f848037087c8bd8c9
PlasticDigits commented 2026-08-26 07:12:39 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1166

mentioned in merge request !1166
PlasticDigits commented 2026-08-26 07:13:18 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1167

mentioned in merge request !1167
PlasticDigits commented 2026-08-26 08:25:06 +00:00 (Migrated from gitlab.com)

mentioned in commit bd6484d95d

mentioned in commit bd6484d95d85344ad2513bb7f2d80e4e403ab645
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-26 08:25:08 +00:00
PlasticDigits commented 2026-08-26 08:46:28 +00:00 (Migrated from gitlab.com)

mentioned in commit 5d1647c208

mentioned in commit 5d1647c208c2f7a12e276fc54fc4a7303e8218f4
PlasticDigits commented 2026-08-26 08:51:55 +00:00 (Migrated from gitlab.com)

mentioned in commit 9a82f40580

mentioned in commit 9a82f40580ec38509feefce19e1b1fd63fda61b7
PlasticDigits commented 2026-08-26 09:31:00 +00:00 (Migrated from gitlab.com)

Merged to main via !1167. Later !1176 (#666) mounts the same TraderLeaderboard on /charts with pairAddress (global board stays on /trader; Charts hides Best Trade).

Leftover: /trader still shows the DEX-wide board (four tabs including Best Trade). make verify-issue-657. Pair ranks are #666.

Merged to `main` via !1167. Later !1176 (#666) mounts the same `TraderLeaderboard` on `/charts` with `pairAddress` (global board stays on `/trader`; Charts hides Best Trade). Leftover: `/trader` still shows the DEX-wide board (four tabs including Best Trade). `make verify-issue-657`. Pair ranks are #666.
PlasticDigits commented 2026-08-26 09:31:35 +00:00 (Migrated from gitlab.com)

mentioned in issue #673

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