feat(frontend): trader leaderboard and profiles use 4/6 address + blockie PFP #656
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#656
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
Retail trader surfaces still print long
terra1…bech32 as the only identity: Charts Trader leaderboard showsshortenAddress(addr, 10, 6)with no avatar, and/trader/:addr//portfolioheaders useAddressRowat 12/6 with no PFP. Bundle one shared trader identity: visible label is first 4 + last 6 of the bech32, plus a deterministic blockie from the existingreact-blockieslibrary. The Trader Profile header (and any other trader-as-person surface) must show the same blockie.Do not split this into separate “truncate” vs “avatar” issues. They are one identity treatment.
Related (do not regress): #188
AddressRow, #541 token identity (tokens ≠ traders), #553 leaderboard Volume USD, #378 logo allowlist / blockie fallback, #430 explorer href safety, #126 trader payload parse, #186 wallet chip shorten, #489 retail copy, #653 one chrome layer.Current codebase
Charts leaderboard (primary retail list)
ChartsPage.tsxTrader leaderboard (aria-label="Trader leaderboard") mapsGET /api/v1/traders/leaderboardrows (IndexerTrader) and renders the Trader cell as aLinkto/trader/${trader.address}whose only child is:That is first 10 + last 6 (example:
terra16wtm…vhpax3). There is no identicon, notitlewith the full bech32 on the link, and no shared identity component. Volume / P&L / trade-count columns are #553 and stay out of this change except that the Trader cell must keep linking to the full address.Leaderboard tests in
ChartsPage.test.tsxcover USD volume (charts-leaderboard-volume) only. They do not assert address shape or a PFP.Docs:
docs/frontend.md§ Charts trader leaderboard. Playbook:skills/AGENTS_FRONTEND_TRADER_VOLUME_USD.md.Trader profile + portfolio (shared header)
TraderPage.tsx/trader/:addressandPortfolioPage.tsxboth renderTraderSummaryStats.tsx. The header identity is anAddressRowwithstartChars={12}/endChars={6}(data-testid="trader-profile-address-row"). Copy + explorer already use the full bech32 (#188 / #184). There is no blockie.AddressRowdefaults remain 8/6 for contracts / LP / protocol rows. Trader header is the only consumer that currently overrides to 12/6 (AGENTS_FRONTEND_ADDRESS_ROW.mdrule 4).TraderPage.test.tsxandTraderSummaryStats.test.tsxdo not assert a PFP. Playwrighte2e/trader-page.spec.ts(#422) is a positions smoke — it does not check identity chrome.Shorten helper (already exists)
tokenDisplay.tsshortenAddress(addr, startChars = 8, endChars = 6)returns${addr.slice(0, start)}…${addr.slice(-end)}(unicode ellipsis) when the string is longer thanstart + end + 2. Tests:tokenDisplay.test.ts.Blockies already in the dApp — for tokens, not traders
package.jsonalready depends onreact-blockies^1.4.1.TokenLogo.tsxseeds withaddressForBlockie ?? blockieSeed,.toLowerCase(),size={6},scale = max(2, ceil(px/6)), circular clip. RemotelogoURIis allowlisted (#378tokenLogoAllowlist.ts); untrusted hosts fall back to a blockie.TokenLogois the token mark (CW20 / native denom). It must not grow a traderlogo_urlor be reused as a trader PFP — traders have no allowlisted remote avatar.Module shim:
vite-env.d.tsdeclare module 'react-blockies'. Many Vitest files alreadyvi.mock('react-blockies', …).Other address surfaces (scope boundary)
/trader/:addrheaderAddressRow12/6, no PFP/portfolioheaderTraderSummaryStatsTradesTableownerowner.slice(0, 12)onlyWalletButtonAddressRow showFullAddressRowIf a new trader-as-person row is added (leaderboard elsewhere, “top traders”, etc.), it must use the same primitive — that is the “anywhere else traders are displayed” rule.
Why this is needed
A full or near-full
terra1…string is the dominant visual in the leaderboard and profile header. Retail users cannot scan or remember it; two adjacent rows look identical until the last few characters. A 4/6 chip plus a stable blockie gives each wallet a recognizable face without ENS, uploads, or indexer avatar URLs.The profile page currently has copy + explorer but still no visual identity, so a user who clicked a leaderboard row lands on a wall of stats with the same long bech32. The PFP must match the leaderboard so the click-through feels like the same person.
This is display-only. Ranking, volume USD, P&L, and indexer JSON stay #553 / #560 / #126.
Constraints / guardrails
traders.avatar_url, no new API field, no on-chain profile contract.logo_url, ENS, NFTs, Gravatar, or user-uploaded images for wallets. Seed is the bech32 only. Do not extendtokenLogoAllowlist.tsfor traders.react-blockies. Do not add a second identicon library. Seed with lowercase bech32 (same asTokenLogo) soterra1ABC…(if it ever appeared) andterra1abc…paint the same grid. Do not invent a custom color hash.TokenLogoas the trader PFP. Tokens may show an allowlisted bitmap; traders must always be the generated blockie. A dedicatedTraderBlockie/TraderIdentitykeeps the trust boundary (#378).shortenAddress(addr, 4, 6)→terr…vhpax3for a typical Classic bech32. The HRP is six characters (terra1); the visible prefix is intentionallyterr, not anotherterra1…essay. Use the existing unicode…. Do not changeshortenAddressdefaults (8/6) used by tokens / contracts.Linkto, copy payload, explorerhref, Reactkey, and/trader/:addressroute param are the full validated bech32. Never navigate to a truncated string.isValidTerraAddress(constants.ts^terra1[a-z0-9]{38,}$) passes. Invalid indexer strings: no blockie, no/trader/link with junk, nojavascript:href (#430). Explorer still only viagetExplorerAddressUrl.title/ accessible name on the chip must include the full address. Do not imply the short form is a unique handle. Do not add a “username” layer.alt=""/aria-hidden). The accessible name of the leaderboard link is the short label (or “Trader {short}”) with the full address intitle. Do not hide the text and leave only a canvas. Leaderboard table stays a real<table>with#/ Trader / metric / Trades.card-glassaround each leaderboard row or around the profile blockie (#653 C653-1). Circular clip + existing mint link color is enough.AddressRow. Light + dark; 375px must not wrap the short label under the rank or clip the blockie.AddressRowcopy + explorer from the trader header. Leaderboard may stay link-only (click → profile) — do not dump copy/explorer on every row (#489).#1866/6 / 4/4 trigger or the full-address wallet menu.dangerouslySetInnerHTML. Address is text /title/hrefvia helpers only.docs/frontend.md(leaderboard + trader profile + AddressRow note) and add a short agent playbook (or a section on the AddressRow skill) so the next agent does not hand-roll a second PFP.Relevant files
frontend-dapp/src/pages/ChartsPage.tsxfrontend-dapp/src/pages/ChartsPage.test.tsxfrontend-dapp/src/components/trader/TraderSummaryStats.tsxfrontend-dapp/src/components/trader/TraderSummaryStats.test.tsxfrontend-dapp/src/pages/TraderPage.tsx/trader/:addr(consumes summary)frontend-dapp/src/pages/PortfolioPage.tsxfrontend-dapp/src/components/ui/AddressRow.tsxfrontend-dapp/src/utils/tokenDisplay.tsshortenAddress(keep defaults; add trader helper if needed)frontend-dapp/src/components/ui/TokenLogo.tsxfrontend-dapp/src/utils/constants.tsisValidTerraAddressfrontend-dapp/src/utils/terraExplorer.tsgetExplorerAddressUrlfrontend-dapp/src/services/indexer/traderProfilePayload.tsdocs/frontend.mdskills/AGENTS_FRONTEND_ADDRESS_ROW.mdfrontend-dapp/e2e/trader-page.spec.tsfrontend-dapp/package.jsonreact-blockiesalready presentRecommended direction
TraderBlockie(seed = address.toLowerCase(),react-blockies, circular clip,aria-hidden) andTraderIdentity:[blockie] [short 4/6]with optionalLinkto/trader/${full}(leaderboard) or compose withAddressRow(profile).shortenTraderAddress(addr) => shortenAddress(addr, 4, 6), plus exported constantsTRADER_ADDR_START_CHARS = 4/TRADER_ADDR_END_CHARS = 6so Charts andAddressRowcannot drift.TraderIdentityas theLinkchild;title={full address};to={/trader/${trader.address}}only whenisValidTerraAddress.TraderSummaryStats: blockie (larger) +AddressRowat 4/6. Portfolio inherits.vi.mock('react-blockies')in page tests; add a small unit test that the real component receivesseed={addr.toLowerCase()}.make verify-issue-<iid>script: Vitest for helper + Charts leaderboard +TraderSummaryStats(no LocalTerra / Postgres required).Acceptance criteria
shortenAddress(addr, 4, 6)(e.g.terr…0abcd) — not 10/6, not the full bech32 as the visible label.href/ React Routertois/trader/{full bech32}. Click still opens that profile./trader/:addrheader shows the same-style blockie (larger) next to the address row./portfolioheader matches whenTraderSummaryStatsis used.AddressRowvisible label is 4/6. Copy still copies the full address. Explorer still usesgetExplorerAddressUrl(full)and is omitted when the helper returnsnull.terra1indexer strings do not render a blockie and do not become a/trader/or explorer link.logo_url. No change toGET /api/v1/traders/*JSON.Test plan (functional paths)
Unit / Vitest
shortenTraderAddress/ 4+6: known Classic bech32 → exactterr…+ last 6; string shorter than threshold unchanged; empty → empty / no throw.TraderBlockie:react-blockiesseedisaddr.toLowerCase(); no<img src="http…">;aria-hidden.seed; mixed-case input (if passed) still lowercased.getLeaderboardwith a valid address — row text is 4/6, not 10/6 / not full bech32;Linkhrefcontains the full address; blockie present (data-testide.g.trader-identity-blockie/charts-leaderboard-trader).total_volume_usd.TraderSummaryStats: blockie + 4/6 label; copy button still receives full address;Youbadge still works forisOwnProfile.AddressRow: trader call site 4/6; default 8/6 consumers still pass existing tests.{ address: 'javascript:alert(1)' },{ address: 'not-terra' },{ address: 'TERRA1…' }— no blockie, no explorer//trader/link with that string.parseIndexerTraderPayloadstill rejects non-objects / bad addresses; do not render identity from a thrown parse.Integration / page
charts-leaderboard-volume) together./trader/{full}: profile heading + matching blockie seed./trader/{wallet}for the same wallet./tradersearch box: still requiresisValidTerraAddressbefore navigate; no blockie on the empty-state search panel.Visual / a11y
card-glasson the row.Linkis tabbable; profile copy / explorer still reachable.aria-labelon the table stays “Trader leaderboard”.Playwright (optional, no new tx)
e2e/trader-page.spec.tsto assert a blockie node + 4/6 text on/trader/{dev wallet}when the indexer has that trader. Do not require a new e2e-tx flow.Test plan (attack, spoof, and abuse)
Treat the short label + blockie as cosmetic. The full bech32 remains the only identifier for navigation, copy, and explorer.
title/ copy /tostay full distinct addresses. Tests use two fixtures that collide on 4/6 and assert differenthrefs and seeds.TERRA1…or mixed-case bech32 in indexer JSON.isValidTerraAddressis lowercase-only today — reject; no blockie; no link. Do not “fix” by uppercasing in the URL.javascript:…,data:,<img>,"><script>.hrefwith that string; nodangerouslySetInnerHTML; text content escaped by React.getExplorerAddressUrlstill returnsnullfor non-bech32 (#430).address = "terra1…/../admin"or?x=/#in the field.isValidTerraAddress(charset[a-z0-9]). Router must not see a relative path.avatar_url/logo_urlonIndexerTrader.<img src={trader.logo_url}>. Regression: evenhttps://evil.example/p.pngis not requested.TokenLogowith a trader field. Token allowlist tests (#378) unchanged.getExplorerAddressUrl.hrefbuilder; invalid → omit icon. Do not concatenate Finder hosts with raw strings.CopyButtonstill gets the full address (existingAddressRow+ Clipboard tests). Leaderboard does not add a copy control that copies the short form./logo.pngon trader rows. No “verified” badge from a blockie.addressstring.Link(leaderboard) so the hit target cannot send the user to a different wallet than the label.logoURIfor a trader even if the strings match a listed CW20.isValidTerraAddressgate; no identity render until navigate to a valid/trader/:addr./trader/A→/trader/B.trader.address/ route param. Route error reset already keyed on address (#126).Verification criteria
Done when all of the following are true:
/charts, every leaderboard trader row shows a blockie +shortenAddress(addr, 4, 6)and links to/trader/{full}./trader/:addr(and/portfolioviaTraderSummaryStats), the header shows the same blockie family (larger) and a 4/6AddressRowwith full-address copy + safe explorer.make test-frontend(or the scoped Vitest list inmake verify-issue-<iid>) covers T-ID-1–T-ID-7 and A1, A3, A5, A8.#553leaderboard volume tests,#188AddressRowdefaults,#378TokenLogoallowlist, and#186wallet chip tests still pass.docs/frontend.mdand the AddressRow (or new trader-identity) playbook describe 4/6 + blockie and the token/wallet exclusions.Out of scope / do not fold in: wallet-chip avatars, tape trader column, order-book owner PFP, ENS/nameservice, indexer-hosted avatars, changing
total_volume_usdranking.mentioned in issue #665
marked as related to #665
mentioned in merge request !1170
Implemented on !1170 (
feat/656-trader-identity).make verify-issue-656passed twice.Acceptance (this MR): T-ID-1–T-ID-8, T-ID-10, A1/A3/A5/A8 covered in Vitest.
Not in this MR (optional / visual): Playwright identity assertion on
e2e/trader-page.spec.ts; live 375px/1280px light+dark pass (T-ID-9) — CSS useswhitespace-nowrap+ nocard-glasson the row, but needs a human/browser check with indexer data.Out of scope unchanged: wallet chip, tape, order-book owner, ENS/indexer avatars, volume ranking.
mentioned in commit
5d1647c208mentioned in commit
183a53de1bmentioned in commit
9a82f40580Merged to
mainvia !1170. Identity is inside the sharedTraderLeaderboard(4/6 + blockie), including pair-scoped Charts ranks after !1176.Leftover:
/traderand/chartsleaderboard chips after frontend rebuild.make verify-issue-656.mentioned in issue #673