feat: compact token identity links on Pool, Trade, and Charts #541

Closed
opened 2026-08-17 03:45:35 +00:00 by PlasticDigits · 21 comments
PlasticDigits commented 2026-08-17 03:45:35 +00:00 (Migrated from gitlab.com)

Summary

Pool (/pool), Trade (/trade), and Charts (/charts) show pair symbols (and logos on Pool) but give retailers no clear way to inspect the actual tokens they are looking at. A look-alike ticker is indistinguishable from the real CW20. This issue ships one shared, compact token-identity control on those three pages — explorer + copy for CW20s, copy-only for native denoms, plus a compact pair-contract chip — without dumping always-on address essays.

Related (do not re-implement): #188 AddressRow (LP withdraw + wallet + trader done; pair chips on Pool / Charts still listed as leftover), #184 / #478 explorer URL helpers, #430 SEC-E10 href validation, #183 CopyButton, #489 anti-cognitive-overload, #378 trust boundaries, #524 display invert.

Current codebase

Tokens on these pages are display-only. Identity (contract / denom / explorer) is implemented elsewhere and never mounted on the pair chrome.

Surface What the user sees today Identity today
/pool pair card header TokenDisplay logo + symbol for asset_infos[0] / [1], plus a mono pairInfoMenuLabel line No explorer, no copy. TokenDisplay is a <span> only.
/pool reserve cards Same TokenDisplay over each reserve amount Same — symbol only.
/pool withdraw LP CW20 via AddressRow (pool-lp-token-address-row) Done (#188). Keep it.
/pool pair contract Factory-router badge + indexer/factory provenance copy No pair-contract chip (explicit #188 leftover).
/trade PairSearchSelect + invert pill (#524). Ticket / book / chart use symbols from indexer asset_0 / asset_1 or getTokenDisplaySymbol No token or pair explorer/copy on the page chrome.
/charts Pair search + MenuSelect, 24h stats titled with indexerPairMenuLabel, PriceChart invert pill Same — symbols only. No AddressRow.
/protocol Factory + router AddressRow (audit surface) Not a token surface; do not copy this block onto Pool/Trade/Charts.
Swap confirm Pair contract(s) on SwapPreSubmitSummary (#449) Out of scope. Do not move confirm chrome onto these pages.

Primitives that already exist (reuse, do not fork):

Gap: native denoms (uluna, uusd) have no explorer helper. getExplorerAddressUrl('uluna') is correctly null. Pair/token pickers must stay comboboxes — identity chrome must not live inside the listbox options.

Why this is needed

  1. Symbol is not identity. Terra Classic has look-alike CW20s. Indexer asset_0.symbol / logo can be wrong or hostile (#378). Retailers on Pool / Trade / Charts cannot verify the contract without leaving the dApp and guessing.
  2. #188 leftover is still open for pair chips. LP withdraw has AddressRow; the two legs and the pair contract on Pool / Trade / Charts do not.
  3. Support / audit cost. “Which UST1 am I looking at?” should be one click, not a Protocol-page scavenger hunt.
  4. Must stay quiet. #489 forbids always-on address essays and cross-nav banners. The control has to be visible (icon + copy next to the symbol) and not a new information panel.

Constraints / guardrails

  1. One primitive, three pages. Do not hand-roll shorten/copy/explorer markup in PoolPage / TradePage / ChartsPage. Extend TokenDisplay or add a thin TokenIdentity that composes TokenDisplay + CopyButton + the same explorer icon/getExplorerAddressUrl path as AddressRow. Pair contract stays AddressRow (or the same dense chip).
  2. Non-obtrusive. Default chrome = logo + symbol + small copy control + explorer icon (CW20 / pair only). Full bech32 is title / aria-label / optional <details>, not an always-on mono dump. Labels ≤ ~5 words. No “View token on explorer” paragraphs, no “use Wrap / UST1 / Swap” banners (#489 invariant 9).
  3. Symbol is not the <a>. Do not wrap TokenDisplay / pair-select labels in a navigation link. That fights PairSearchSelect, invert pills (#524), and keyboard listbox behavior. Explorer is a sibling control.
  4. Href safety (SEC-E10). Every href goes through getExplorerAddressUrl. Invalid / javascript: / HTML / empty / non-bech32 → null → omit the anchor. Do not interpolate indexer contract_addr into a URL string. Keep #430 adversarial tests intact.
  5. Native ≠ CW20. uluna / uusd (and any native_token denom): copy denom only. Do not invent a Finder “token” URL, do not pass the denom to getExplorerAddressUrl, do not render a dead explorer icon.
  6. Provenance. Copy/explorer targets are factory / pair AssetInfo (or indexer contract_addr / denom after the same bech32/denom checks). Never use symbol as the copy payload or URL segment. Display invert changes labels, not which contract the icon points at.
  7. No new token route. Do not add /token/:id. Do not link CoinGecko, CoinMarketCap, Twitter, or indexer-supplied “website” / logo_url hosts. Logo allowlist (tokenLogoAllowlist.ts) is unchanged.
  8. No third-party token pages in pickers. Do not put identity icons inside PairSearchSelect / TokenSearchSelect option rows (noise + nested interactive).
  9. Pair contract chip uses a checksummed pair bech32 (pair.contract_addr / pair_address). Hide explorer when the helper returns null. Factory-unknown / invalid route segments stay on existing #175 / #176 notices — do not render identity chrome for garbage.
  10. External links: target="_blank" rel="noopener noreferrer". Explicit copyAriaLabel / explorerAriaLabel per token (“Copy cLUNC address”, “View cLUNC on explorer”). :focus-visible rings (#144).
  11. Do not put factory/router AddressRows on these pages (/protocol only, #378).
  12. Do not change swap confirm, wallet menu, trader header, or LP withdraw AddressRow behavior except to share CSS if needed.
  13. Swap page is out of scope unless a shared primitive is extracted — do not expand this issue to / token pickers.
  14. Docs + skill in the same MR: docs/frontend.md section + skills/AGENTS_FRONTEND_TOKEN_IDENTITY.md + AGENTS.md playbook row. Invariants T541-1–T541-8.
  15. LocalTerra not required for the core unit/UI work; optional visual QA on make dev.

Relevant files

File Role
frontend-dapp/src/components/ui/TokenDisplay.tsx Logo + symbol; extend or wrap
frontend-dapp/src/hooks/useTokenDisplayInfo.ts Symbol / logo resolution
frontend-dapp/src/components/ui/AddressRow.tsx Pair / LP / dense address chip
frontend-dapp/src/components/ui/CopyButton.tsx Clipboard
frontend-dapp/src/utils/terraExplorer.ts getExplorerAddressUrl only
frontend-dapp/src/utils/terraAddressValidation.ts Bech32 gate
frontend-dapp/src/pages/PoolPage.tsx Card header + reserves; pair chip; keep LP AddressRow
frontend-dapp/src/pages/TradePage.tsx Mount under pair select panel (trade-pair-select-panel)
frontend-dapp/src/pages/ChartsPage.tsx Mount under pair select / above chart
frontend-dapp/src/components/charts/PriceChart.tsx Invert pill only — do not bury identity inside the chart canvas
frontend-dapp/src/components/trade/PairSearchSelect.tsx Do not add per-option explorer icons
frontend-dapp/src/hooks/usePairDisplayOrientation.ts Invert labels vs factory asset order
frontend-dapp/src/components/ui/__tests__/AddressRow.explorerSafety.test.tsx SEC-E10 rendering
frontend-dapp/src/utils/__tests__/terraExplorer.test.ts URL matrix + adversarial inputs
docs/frontend.md AddressRow / explorer sections to extend
skills/AGENTS_FRONTEND_ADDRESS_ROW.md Mark pair/token chips done via this issue
skills/AGENTS_FRONTEND_TERRA_EXPLORER.md Href rules
skills/AGENTS_FRONTEND_COPY_COGNITIVE_LOAD.md No address essays
  1. Helper tokenIdentityTarget(info) → { kind: 'cw20', address, explorerUrl } | { kind: 'native', denom } | null. CW20 explorer URL is getExplorerAddressUrl(address) or omit. Reject native denoms and invalid bech32.
  2. UI — TokenIdentity (preferred over making every TokenDisplay clickable):
    • Existing logo + symbol (text only).
    • CopyButton of address or denom.
    • Explorer icon only when explorerUrl is non-null.
    • data-testids: token-identity-{base,quote,pair} (and pool reserve variants).
  3. PairTokenLinks — one compact row: base identity, quote identity, pair AddressRow (short 8/6). Place:
    • Pool: under the existing TokenDisplay header (or replace those two displays with TokenIdentity); pair chip beside / under the factory badge. Reserve cards may keep plain TokenDisplay or the same dense identity — pick one and test both breakpoints.
    • Trade: one row under trade-pair-select-panel, outside the combobox.
    • Charts: one row under the pair MenuSelect, outside the combobox.
  4. Invert: visible order / labels follow usePairDisplayOrientation; copy/explorer payloads stay asset_0 / asset_1 (factory order). Tests must catch a swapped href after invert.
  5. Empty / loading / invalid pair: hide the row (or disable controls) — no terra1 placeholders, no links on #176 / #175 notice states.
  6. Docs: invariants T541-1–T541-8, make verify-issue-541, playbook skills/AGENTS_FRONTEND_TOKEN_IDENTITY.md.

Invariants:

ID Meaning
T541-1 Pool, Trade, and Charts each expose copy + (when applicable) explorer for both pair legs and the pair contract without opening a wallet menu.
T541-2 CW20 / pair href is only getExplorerAddressUrl; native is copy-only.
T541-3 Symbol / logo never wrap an <a>; explorer is a sibling control.
T541-4 Copy payload is contract or denom, never the display symbol.
T541-5 Display invert does not swap explorer/copy targets.
T541-6 Invalid / missing pair: no identity links.
T541-7 No new outbound hosts, no /token route, no picker-option icons, no Protocol factory/router clone.
T541-8 #489 / #378: no always-on address essay or cross-nav banner.

Acceptance criteria

  • AC1 — /pool selected pair: both legs have copy; CW20 legs have an explorer control whose href matches getExplorerAddressUrl. Native legs have copy and no explorer anchor.
  • AC2 — /pool shows a compact pair-contract AddressRow (or equivalent) for the card’s contract_addr. LP withdraw pool-lp-token-address-row still works.
  • AC3 — /trade with a known factory pair: same leg + pair identity row under the pair select, not inside the listbox. Invalid/unknown pair notices (#176 / #175): row absent.
  • AC4 — /charts with a selected pair: same row under the pair select. Empty “No pairs yet”: no dead links.
  • AC5 — #524 invert on Trade and Charts: labels/pill may flip; copied addresses and explorer hrefs stay on the same factory assets.
  • AC6 — All explorer anchors: target="_blank" rel="noopener noreferrer"; omitted when helper is null (local without LCD shape, bad address, native).
  • AC7 — No CoinGecko/CMC/website links; no /token/:id; no factory/router rows on these pages; Swap confirm unchanged.
  • AC8 — Light + dark, mobile + desktop: identity row does not wrap onto the pair combobox, invert pill, or ticket CTA; :focus-visible on copy/explorer.
  • AC9 — docs/frontend.md + skill + AGENTS.md + make verify-issue-541.
  • AC10 — Adversarial href / invert-swap / native-no-link tests in CI.

Test plan (all paths)

Unit

ID Path
T1 tokenIdentityTarget CW20 valid → address + explorer URL per VITE_NETWORK (mainnet /columbus-5/address/…, testnet Hexxagon, local LCD accounts).
T2 Native uluna / uusd → kind: 'native', no URL.
T3 Invalid / empty / javascript:alert(1) / HTML / wrong checksum → null, no URL.
T4 Indexer brief with spoofed symbol: 'UST1' and attacker contract_addr → copy/href use the address, not UST1.
T5 Invert helper: display order flips, identity targets do not.

UI / integration

ID Path
U1 Pool card: two leg identities + pair chip; LP AddressRow still present on withdraw.
U2 Pool native-wrap pair (cLUNC/cUSTC): CW20 legs link; if a native option label is shown, it does not get a fake explorer href.
U3 Trade: row under trade-pair-select-panel; listbox options have no extra explorer buttons.
U4 Trade #176 invalid and #175 unknown: no identity links.
U5 Charts: row present when a pair is selected; hidden when no pairs.
U6 Invert toggle: href and clipboard text unchanged for each asset.
U7 CopyButton success live region still announces (reuse #183).
U8 Light + dark; reduced-width: row wraps under the select, does not overlap the invert pill or chart.
U9 Keyboard: tab to copy and explorer; :focus-visible ring; Enter on explorer does not change the selected pair.

Playwright smoke (5 workers; no tx required)

ID Path
P1 /pool selected factory pair: token-identity-* + pair explorer (or AddressRow) visible.
P2 /trade/<known-pair>: identity row visible; pair select still changes pair.
P3 /charts: select pair → identity row; explorer href is Finder/LCD, not the dApp origin.
P4 /trade/lilwayne%20babyyy (or current #176 fixture): no explorer anchors in the identity slot.

Test plan (attack, hack, and abuse)

ID Vector Expect
A1 javascript: / data: / HTML in indexer contract_addr No href. Regression alongside AddressRow.explorerSafety / terraExplorer adversarial cases.
A2 Open redirect — craft address so a naive `${explorerUrl}/${addr}` breaks out of Finder Impossible if only getExplorerAddressUrl is used; test that components never concatenate hosts.
A3 Tabnabbing — omit noopener rel includes noopener noreferrer.
A4 Look-alike ticker — symbol UST1, contract attacker CW20 Copy + explorer show the attacker address so the user can see the mismatch (do not “fix” the symbol into a registry address).
A5 Phishing outbound — indexer logo_url or website used as explorer href Forbidden. Logos still allowlist-only; identity links are Finder/LCD only.
A6 Native denom as URL — href="…/address/uluna" No explorer control.
A7 Invert swap — UI shows cUSTC/UST1 inverted; copy of “base” silently copies the other contract Tests fail if payloads follow display order instead of factory assets.
A8 Nested interactive — wrapping the pair combobox label in <a> steals clicks / breaks listbox Symbol is not a link; explorer is a sibling.
A9 Invalid deep link — identity chrome rendered for terra1 + 38× x Hidden; existing unknown-pair notice only.
A10 XSS in symbol as link text Symbol remains text node (existing TokenDisplay rule); do not dangerouslySetInnerHTML.
A11 CSP / new hosts — adding finder.* via raw <a href> hardcoded in a page Must use helper + chainlist.json. Do not widen production CSP connect-src for this (navigation is not connect-src).
A12 Clickjacking / overlay — identity icons covering Place / Add CTA Layout tests / visual: row is in the select panel, not position: fixed over the ticket.
A13 Clipboard mismatch — visible symbol A, clipboard B, because of stale pair switch Identity row keyed on pairAddr; after switch, copy matches the new pair.
A14 Social-engineering chrome — “Verified on CoinGecko”, APR, “safe token” badge next to the link Out of scope / reject. Identity ≠ endorsement.
A15 Protocol address leak — factory/router AddressRow cloned onto Trade “for convenience” Forbidden (#378).

Verification criteria

Close when all of the following are true:

  1. AC1–AC10 checked on the MR.
  2. make verify-issue-541 is green (unit + scoped frontend + Playwright smoke).
  3. A1 / A4 / A6 / A7 have automated tests (not a manual note).
  4. #188 skill leftover line (“pair chips on Pool / Charts”) is updated to point here.
  5. No new /token route, no new outbound product hosts, Swap confirm and LP withdraw unchanged except shared styles.
make verify-issue-541
make test-frontend
make lint-frontend
# scoped (adjust to shipped modules):
#   tokenIdentity + TokenIdentity + PoolPage + TradePage + ChartsPage
#   terraExplorer + AddressRow.explorerSafety
# Playwright smoke (5 workers), no e2e-tx required
## Summary Pool (`/pool`), Trade (`/trade`), and Charts (`/charts`) show pair **symbols** (and logos on Pool) but give retailers **no clear way to inspect the actual tokens** they are looking at. A look-alike ticker is indistinguishable from the real CW20. This issue ships **one shared, compact token-identity control** on those three pages — explorer + copy for CW20s, copy-only for native denoms, plus a compact pair-contract chip — without dumping always-on address essays. Related (do not re-implement): [#188](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/188) `AddressRow` (LP withdraw + wallet + trader done; **pair chips on Pool / Charts still listed as leftover**), [#184](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/184) / [#478](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/478) explorer URL helpers, [#430](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/430) SEC-E10 href validation, [#183](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/183) `CopyButton`, [#489](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/489) anti-cognitive-overload, [#378](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/378) trust boundaries, [#524](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/524) display invert. ## Current codebase Tokens on these pages are **display-only**. Identity (contract / denom / explorer) is implemented elsewhere and never mounted on the pair chrome. | Surface | What the user sees today | Identity today | |---------|--------------------------|----------------| | **`/pool` pair card header** | [`TokenDisplay`](frontend-dapp/src/components/ui/TokenDisplay.tsx) logo + symbol for `asset_infos[0]` / `[1]`, plus a mono `pairInfoMenuLabel` line | No explorer, no copy. `TokenDisplay` is a `<span>` only. | | **`/pool` reserve cards** | Same `TokenDisplay` over each reserve amount | Same — symbol only. | | **`/pool` withdraw** | LP CW20 via [`AddressRow`](frontend-dapp/src/components/ui/AddressRow.tsx) (`pool-lp-token-address-row`) | **Done** (#188). Keep it. | | **`/pool` pair contract** | Factory-router badge + indexer/factory provenance copy | No pair-contract chip (explicit #188 leftover). | | **`/trade`** | [`PairSearchSelect`](frontend-dapp/src/components/trade/PairSearchSelect.tsx) + invert pill ([#524](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/524)). Ticket / book / chart use **symbols** from indexer `asset_0` / `asset_1` or `getTokenDisplaySymbol` | No token or pair explorer/copy on the page chrome. | | **`/charts`** | Pair search + `MenuSelect`, 24h stats titled with `indexerPairMenuLabel`, [`PriceChart`](frontend-dapp/src/components/charts/PriceChart.tsx) invert pill | Same — symbols only. No `AddressRow`. | | **`/protocol`** | Factory + router `AddressRow` (audit surface) | Not a token surface; do **not** copy this block onto Pool/Trade/Charts. | | **Swap confirm** | Pair contract(s) on [`SwapPreSubmitSummary`](frontend-dapp/src/components/swap/SwapPreSubmitSummary.tsx) (#449) | Out of scope. Do not move confirm chrome onto these pages. | **Primitives that already exist (reuse, do not fork):** - [`getExplorerAddressUrl`](frontend-dapp/src/utils/terraExplorer.ts) — network-aware Finder / local LCD; **`null`** unless the segment is a valid Terra bech32 ([#184](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/184), [#478](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/478), [#430](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/430)). - [`AddressRow`](frontend-dapp/src/components/ui/AddressRow.tsx) — shortened label + [`CopyButton`](frontend-dapp/src/components/ui/CopyButton.tsx) + explorer icon (`target="_blank"` `rel="noopener noreferrer"`). - [`useTokenDisplayInfo`](frontend-dapp/src/hooks/useTokenDisplayInfo.ts) / [`tokenDisplay.ts`](frontend-dapp/src/utils/tokenDisplay.ts) — symbol resolution (registry wrap product symbols cLUNC/cUSTC, indexer, CW20 query, shorten fallback). - [`isValidTerraBech32Address`](frontend-dapp/src/utils/terraAddressValidation.ts) — checksummed `terra1…` only. **Gap:** native denoms (`uluna`, `uusd`) have **no** explorer helper. `getExplorerAddressUrl('uluna')` is correctly `null`. Pair/token pickers must stay comboboxes — identity chrome must not live **inside** the listbox options. ## Why this is needed 1. **Symbol is not identity.** Terra Classic has look-alike CW20s. Indexer `asset_0.symbol` / logo can be wrong or hostile ([#378](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/378)). Retailers on Pool / Trade / Charts cannot verify the contract without leaving the dApp and guessing. 2. **#188 leftover is still open for pair chips.** LP withdraw has `AddressRow`; the two **legs** and the **pair contract** on Pool / Trade / Charts do not. 3. **Support / audit cost.** “Which UST1 am I looking at?” should be one click, not a Protocol-page scavenger hunt. 4. **Must stay quiet.** [#489](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/489) forbids always-on address essays and cross-nav banners. The control has to be **visible** (icon + copy next to the symbol) and **not** a new information panel. ## Constraints / guardrails 1. **One primitive, three pages.** Do not hand-roll shorten/copy/explorer markup in `PoolPage` / `TradePage` / `ChartsPage`. Extend `TokenDisplay` **or** add a thin `TokenIdentity` that composes `TokenDisplay` + `CopyButton` + the same explorer icon/`getExplorerAddressUrl` path as `AddressRow`. Pair contract stays `AddressRow` (or the same dense chip). 2. **Non-obtrusive.** Default chrome = logo + symbol + small copy control + explorer icon (CW20 / pair only). Full bech32 is `title` / `aria-label` / optional `<details>`, **not** an always-on mono dump. Labels ≤ ~5 words. No “View token on explorer” paragraphs, no “use Wrap / UST1 / Swap” banners ([#489](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/489) invariant **9**). 3. **Symbol is not the `<a>`.** Do not wrap `TokenDisplay` / pair-select labels in a navigation link. That fights `PairSearchSelect`, invert pills ([#524](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/524)), and keyboard listbox behavior. Explorer is a **sibling** control. 4. **Href safety (SEC-E10).** Every `href` goes through `getExplorerAddressUrl`. Invalid / `javascript:` / HTML / empty / non-bech32 → `null` → **omit the anchor**. Do not interpolate indexer `contract_addr` into a URL string. Keep [#430](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/430) adversarial tests intact. 5. **Native ≠ CW20.** `uluna` / `uusd` (and any `native_token` denom): **copy denom only**. Do **not** invent a Finder “token” URL, do not pass the denom to `getExplorerAddressUrl`, do not render a dead explorer icon. 6. **Provenance.** Copy/explorer targets are factory / pair `AssetInfo` (or indexer `contract_addr` / `denom` **after** the same bech32/denom checks). Never use `symbol` as the copy payload or URL segment. Display invert changes **labels**, not which contract the icon points at. 7. **No new token route.** Do not add `/token/:id`. Do not link CoinGecko, CoinMarketCap, Twitter, or indexer-supplied “website” / `logo_url` hosts. Logo allowlist ([`tokenLogoAllowlist.ts`](frontend-dapp/src/utils/tokenLogoAllowlist.ts)) is unchanged. 8. **No third-party token pages in pickers.** Do not put identity icons inside `PairSearchSelect` / `TokenSearchSelect` option rows (noise + nested interactive). 9. **Pair contract chip** uses a checksummed pair bech32 (`pair.contract_addr` / `pair_address`). Hide explorer when the helper returns `null`. Factory-unknown / invalid route segments stay on existing [#175](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/175) / [#176](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/176) notices — do not render identity chrome for garbage. 10. **External links:** `target="_blank"` `rel="noopener noreferrer"`. Explicit `copyAriaLabel` / `explorerAriaLabel` per token (“Copy cLUNC address”, “View cLUNC on explorer”). `:focus-visible` rings ([#144](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/144)). 11. **Do not** put factory/router AddressRows on these pages (`/protocol` only, [#378](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/378)). 12. **Do not** change swap confirm, wallet menu, trader header, or LP withdraw `AddressRow` behavior except to share CSS if needed. 13. **Swap page is out of scope** unless a shared primitive is extracted — do not expand this issue to `/` token pickers. 14. **Docs + skill** in the same MR: `docs/frontend.md` section + `skills/AGENTS_FRONTEND_TOKEN_IDENTITY.md` + `AGENTS.md` playbook row. Invariants **T541-1–T541-8**. 15. **LocalTerra** not required for the core unit/UI work; optional visual QA on `make dev`. ## Relevant files | File | Role | |------|------| | [`frontend-dapp/src/components/ui/TokenDisplay.tsx`](frontend-dapp/src/components/ui/TokenDisplay.tsx) | Logo + symbol; extend or wrap | | [`frontend-dapp/src/hooks/useTokenDisplayInfo.ts`](frontend-dapp/src/hooks/useTokenDisplayInfo.ts) | Symbol / logo resolution | | [`frontend-dapp/src/components/ui/AddressRow.tsx`](frontend-dapp/src/components/ui/AddressRow.tsx) | Pair / LP / dense address chip | | [`frontend-dapp/src/components/ui/CopyButton.tsx`](frontend-dapp/src/components/ui/CopyButton.tsx) | Clipboard | | [`frontend-dapp/src/utils/terraExplorer.ts`](frontend-dapp/src/utils/terraExplorer.ts) | `getExplorerAddressUrl` only | | [`frontend-dapp/src/utils/terraAddressValidation.ts`](frontend-dapp/src/utils/terraAddressValidation.ts) | Bech32 gate | | [`frontend-dapp/src/pages/PoolPage.tsx`](frontend-dapp/src/pages/PoolPage.tsx) | Card header + reserves; pair chip; keep LP `AddressRow` | | [`frontend-dapp/src/pages/TradePage.tsx`](frontend-dapp/src/pages/TradePage.tsx) | Mount under pair select panel (`trade-pair-select-panel`) | | [`frontend-dapp/src/pages/ChartsPage.tsx`](frontend-dapp/src/pages/ChartsPage.tsx) | Mount under pair select / above chart | | [`frontend-dapp/src/components/charts/PriceChart.tsx`](frontend-dapp/src/components/charts/PriceChart.tsx) | Invert pill only — do not bury identity inside the chart canvas | | [`frontend-dapp/src/components/trade/PairSearchSelect.tsx`](frontend-dapp/src/components/trade/PairSearchSelect.tsx) | Do **not** add per-option explorer icons | | [`frontend-dapp/src/hooks/usePairDisplayOrientation.ts`](frontend-dapp/src/hooks/usePairDisplayOrientation.ts) | Invert labels vs factory asset order | | [`frontend-dapp/src/components/ui/__tests__/AddressRow.explorerSafety.test.tsx`](frontend-dapp/src/components/ui/__tests__/AddressRow.explorerSafety.test.tsx) | SEC-E10 rendering | | [`frontend-dapp/src/utils/__tests__/terraExplorer.test.ts`](frontend-dapp/src/utils/__tests__/terraExplorer.test.ts) | URL matrix + adversarial inputs | | [`docs/frontend.md`](docs/frontend.md) | AddressRow / explorer sections to extend | | [`skills/AGENTS_FRONTEND_ADDRESS_ROW.md`](skills/AGENTS_FRONTEND_ADDRESS_ROW.md) | Mark pair/token chips done via this issue | | [`skills/AGENTS_FRONTEND_TERRA_EXPLORER.md`](skills/AGENTS_FRONTEND_TERRA_EXPLORER.md) | Href rules | | [`skills/AGENTS_FRONTEND_COPY_COGNITIVE_LOAD.md`](skills/AGENTS_FRONTEND_COPY_COGNITIVE_LOAD.md) | No address essays | ## Recommended direction 1. **Helper** `tokenIdentityTarget(info)` → `{ kind: 'cw20', address, explorerUrl } | { kind: 'native', denom } | null`. CW20 explorer URL is `getExplorerAddressUrl(address)` or omit. Reject native denoms and invalid bech32. 2. **UI** — `TokenIdentity` (preferred over making every `TokenDisplay` clickable): - Existing logo + symbol (text only). - `CopyButton` of **address** or **denom**. - Explorer icon **only** when `explorerUrl` is non-null. - `data-testid`s: `token-identity-{base,quote,pair}` (and pool reserve variants). 3. **`PairTokenLinks`** — one compact row: base identity, quote identity, pair `AddressRow` (short 8/6). Place: - **Pool:** under the existing `TokenDisplay` header (or replace those two displays with `TokenIdentity`); pair chip beside / under the factory badge. Reserve cards may keep plain `TokenDisplay` **or** the same dense identity — pick one and test both breakpoints. - **Trade:** one row under `trade-pair-select-panel`, outside the combobox. - **Charts:** one row under the pair `MenuSelect`, outside the combobox. 4. **Invert:** visible order / labels follow `usePairDisplayOrientation`; copy/explorer payloads stay `asset_0` / `asset_1` (factory order). Tests must catch a swapped href after invert. 5. **Empty / loading / invalid pair:** hide the row (or disable controls) — no `terra1` placeholders, no links on `#176` / `#175` notice states. 6. **Docs:** invariants **T541-1–T541-8**, `make verify-issue-541`, playbook `skills/AGENTS_FRONTEND_TOKEN_IDENTITY.md`. Invariants: | ID | Meaning | |----|---------| | **T541-1** | Pool, Trade, and Charts each expose copy + (when applicable) explorer for **both pair legs** and the **pair contract** without opening a wallet menu. | | **T541-2** | CW20 / pair `href` is only `getExplorerAddressUrl`; native is copy-only. | | **T541-3** | Symbol / logo never wrap an `<a>`; explorer is a sibling control. | | **T541-4** | Copy payload is contract or denom, never the display symbol. | | **T541-5** | Display invert does not swap explorer/copy targets. | | **T541-6** | Invalid / missing pair: no identity links. | | **T541-7** | No new outbound hosts, no `/token` route, no picker-option icons, no Protocol factory/router clone. | | **T541-8** | `#489` / `#378`: no always-on address essay or cross-nav banner. | ## Acceptance criteria - [ ] **AC1** — `/pool` selected pair: both legs have copy; CW20 legs have an explorer control whose `href` matches `getExplorerAddressUrl`. Native legs have copy and **no** explorer anchor. - [ ] **AC2** — `/pool` shows a compact pair-contract `AddressRow` (or equivalent) for the card’s `contract_addr`. LP withdraw `pool-lp-token-address-row` still works. - [ ] **AC3** — `/trade` with a known factory pair: same leg + pair identity row under the pair select, not inside the listbox. Invalid/unknown pair notices (#176 / #175): row absent. - [ ] **AC4** — `/charts` with a selected pair: same row under the pair select. Empty “No pairs yet”: no dead links. - [ ] **AC5** — `#524` invert on Trade and Charts: labels/pill may flip; copied addresses and explorer `href`s stay on the same factory assets. - [ ] **AC6** — All explorer anchors: `target="_blank"` `rel="noopener noreferrer"`; omitted when helper is `null` (local without LCD shape, bad address, native). - [ ] **AC7** — No CoinGecko/CMC/website links; no `/token/:id`; no factory/router rows on these pages; Swap confirm unchanged. - [ ] **AC8** — Light + dark, mobile + desktop: identity row does not wrap onto the pair combobox, invert pill, or ticket CTA; `:focus-visible` on copy/explorer. - [ ] **AC9** — `docs/frontend.md` + skill + `AGENTS.md` + `make verify-issue-541`. - [ ] **AC10** — Adversarial href / invert-swap / native-no-link tests in CI. ## Test plan (all paths) ### Unit | ID | Path | |----|------| | **T1** | `tokenIdentityTarget` CW20 valid → address + explorer URL per `VITE_NETWORK` (mainnet `/columbus-5/address/…`, testnet Hexxagon, local LCD accounts). | | **T2** | Native `uluna` / `uusd` → `kind: 'native'`, no URL. | | **T3** | Invalid / empty / `javascript:alert(1)` / HTML / wrong checksum → `null`, no URL. | | **T4** | Indexer brief with spoofed `symbol: 'UST1'` and attacker `contract_addr` → copy/href use the **address**, not `UST1`. | | **T5** | Invert helper: display order flips, identity targets do not. | ### UI / integration | ID | Path | |----|------| | **U1** | Pool card: two leg identities + pair chip; LP `AddressRow` still present on withdraw. | | **U2** | Pool native-wrap pair (cLUNC/cUSTC): CW20 legs link; if a native option label is shown, it does not get a fake explorer href. | | **U3** | Trade: row under `trade-pair-select-panel`; listbox options have no extra explorer buttons. | | **U4** | Trade `#176` invalid and `#175` unknown: no identity links. | | **U5** | Charts: row present when a pair is selected; hidden when no pairs. | | **U6** | Invert toggle: `href` and clipboard text unchanged for each asset. | | **U7** | CopyButton success live region still announces (reuse #183). | | **U8** | Light + dark; reduced-width: row wraps under the select, does not overlap the invert pill or chart. | | **U9** | Keyboard: tab to copy and explorer; `:focus-visible` ring; Enter on explorer does not change the selected pair. | ### Playwright smoke (5 workers; no tx required) | ID | Path | |----|------| | **P1** | `/pool` selected factory pair: `token-identity-*` + pair explorer (or `AddressRow`) visible. | | **P2** | `/trade/<known-pair>`: identity row visible; pair select still changes pair. | | **P3** | `/charts`: select pair → identity row; explorer `href` is Finder/LCD, not the dApp origin. | | **P4** | `/trade/lilwayne%20babyyy` (or current #176 fixture): no explorer anchors in the identity slot. | ## Test plan (attack, hack, and abuse) | ID | Vector | Expect | |----|--------|--------| | **A1** | **javascript: / data: / HTML** in indexer `contract_addr` | No `href`. Regression alongside `AddressRow.explorerSafety` / `terraExplorer` adversarial cases. | | **A2** | **Open redirect** — craft address so a naive `` `${explorerUrl}/${addr}` `` breaks out of Finder | Impossible if only `getExplorerAddressUrl` is used; test that components never concatenate hosts. | | **A3** | **Tabnabbing** — omit `noopener` | `rel` includes `noopener noreferrer`. | | **A4** | **Look-alike ticker** — symbol UST1, contract attacker CW20 | Copy + explorer show the **attacker** address so the user can see the mismatch (do not “fix” the symbol into a registry address). | | **A5** | **Phishing outbound** — indexer `logo_url` or `website` used as explorer `href` | Forbidden. Logos still allowlist-only; identity links are Finder/LCD only. | | **A6** | **Native denom as URL** — `href="…/address/uluna"` | No explorer control. | | **A7** | **Invert swap** — UI shows cUSTC/UST1 inverted; copy of “base” silently copies the other contract | Tests fail if payloads follow display order instead of factory assets. | | **A8** | **Nested interactive** — wrapping the pair combobox label in `<a>` steals clicks / breaks listbox | Symbol is not a link; explorer is a sibling. | | **A9** | **Invalid deep link** — identity chrome rendered for `terra1` + 38× `x` | Hidden; existing unknown-pair notice only. | | **A10** | **XSS in symbol** as link text | Symbol remains text node (existing TokenDisplay rule); do not `dangerouslySetInnerHTML`. | | **A11** | **CSP / new hosts** — adding `finder.*` via raw `<a href>` hardcoded in a page | Must use helper + `chainlist.json`. Do not widen production CSP `connect-src` for this (navigation is not `connect-src`). | | **A12** | **Clickjacking / overlay** — identity icons covering Place / Add CTA | Layout tests / visual: row is in the select panel, not `position: fixed` over the ticket. | | **A13** | **Clipboard mismatch** — visible symbol A, clipboard B, because of stale pair switch | Identity row keyed on `pairAddr`; after switch, copy matches the new pair. | | **A14** | **Social-engineering chrome** — “Verified on CoinGecko”, APR, “safe token” badge next to the link | Out of scope / reject. Identity ≠ endorsement. | | **A15** | **Protocol address leak** — factory/router `AddressRow` cloned onto Trade “for convenience” | Forbidden (#378). | ## Verification criteria Close when **all** of the following are true: 1. AC1–AC10 checked on the MR. 2. `make verify-issue-541` is green (unit + scoped frontend + Playwright smoke). 3. A1 / A4 / A6 / A7 have automated tests (not a manual note). 4. `#188` skill leftover line (“pair chips on Pool / Charts”) is updated to point here. 5. No new `/token` route, no new outbound product hosts, Swap confirm and LP withdraw unchanged except shared styles. ```bash make verify-issue-541 make test-frontend make lint-frontend # scoped (adjust to shipped modules): # tokenIdentity + TokenIdentity + PoolPage + TradePage + ChartsPage # terraExplorer + AddressRow.explorerSafety # Playwright smoke (5 workers), no e2e-tx required ```
PlasticDigits commented 2026-08-17 03:45:37 +00:00 (Migrated from gitlab.com)

marked as related to #188

marked as related to #188
PlasticDigits commented 2026-08-17 03:45:46 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
PlasticDigits commented 2026-08-17 04:19:20 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1070

mentioned in merge request !1070
PlasticDigits commented 2026-08-17 04:45:59 +00:00 (Migrated from gitlab.com)

mentioned in commit 64c6dd9623

mentioned in commit 64c6dd962314440fa313af38bfb44d24ad97777b
PlasticDigits commented 2026-08-17 04:46:22 +00:00 (Migrated from gitlab.com)

mentioned in commit c8ab55bcbe

mentioned in commit c8ab55bcbeb2aaa0aabbdd2e4d85e43d2ea433b4
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-17 04:46:22 +00:00
PlasticDigits commented 2026-08-17 04:47:06 +00:00 (Migrated from gitlab.com)

mentioned in commit d7aa1c07e1

mentioned in commit d7aa1c07e1f21416a60c226a09bceb15b9b88db4
PlasticDigits commented 2026-08-17 04:53:34 +00:00 (Migrated from gitlab.com)

Merged via !1070 onto main (c8ab55bc) after rebasing through !1069. Conflict resolution kept both verify-issue-541 and verify-issue-543 Makefile/playbook rows.

Sanity check on main: make verify-issue-541 — 11/11 pass (T1–T5 / A1 / A4 / A6 / A7 unit, TokenIdentity + PairTokenLinks explorer safety, Pool/Trade/Charts #541 describes, docs T541-1–T541-8). Playwright: P4 (#176 no identity anchors) passed; P1–P3 skipped (PLAYWRIGHT_SKIP_CHAIN=1 / no factory pair row in that smoke).

Remaining (not a merge blocker): AC8 light/dark + mobile/desktop wrap and keyboard :focus-visible still need a visual pass. Re-run P1–P3 against a live pair list. Tracked in the post-merge follow-up.

Merged via !1070 onto `main` (`c8ab55bc`) after rebasing through !1069. Conflict resolution kept both `verify-issue-541` and `verify-issue-543` Makefile/playbook rows. **Sanity check on `main`:** `make verify-issue-541` — 11/11 pass (T1–T5 / A1 / A4 / A6 / A7 unit, TokenIdentity + PairTokenLinks explorer safety, Pool/Trade/Charts #541 describes, docs T541-1–T541-8). Playwright: P4 (#176 no identity anchors) passed; **P1–P3 skipped** (`PLAYWRIGHT_SKIP_CHAIN=1` / no factory pair row in that smoke). **Remaining (not a merge blocker):** AC8 light/dark + mobile/desktop wrap and keyboard `:focus-visible` still need a visual pass. Re-run P1–P3 against a live pair list. Tracked in the post-merge follow-up.
PlasticDigits commented 2026-08-17 04:53:46 +00:00 (Migrated from gitlab.com)

mentioned in issue #542

mentioned in issue #542
PlasticDigits commented 2026-08-17 04:53:57 +00:00 (Migrated from gitlab.com)

mentioned in issue #545

mentioned in issue #545
PlasticDigits commented 2026-08-17 04:53:58 +00:00 (Migrated from gitlab.com)

marked as related to #545

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

mentioned in issue #547

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

marked as related to #547

marked as related to #547
PlasticDigits commented 2026-08-18 00:28:53 +00:00 (Migrated from gitlab.com)

mentioned in issue #556

mentioned in issue #556
PlasticDigits commented 2026-08-19 01:03:03 +00:00 (Migrated from gitlab.com)

mentioned in issue #570

mentioned in issue #570
PlasticDigits commented 2026-08-19 01:03:04 +00:00 (Migrated from gitlab.com)

marked as related to #570

marked as related to #570
PlasticDigits commented 2026-08-22 10:59:13 +00:00 (Migrated from gitlab.com)

mentioned in issue #593

mentioned in issue #593
PlasticDigits commented 2026-08-25 01:55:13 +00:00 (Migrated from gitlab.com)

mentioned in issue #630

mentioned in issue #630
PlasticDigits commented 2026-08-25 06:01:21 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1141

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

mentioned in merge request !1142

mentioned in merge request !1142
PlasticDigits commented 2026-08-26 03:07:39 +00:00 (Migrated from gitlab.com)

mentioned in issue #656

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

mentioned in issue #664

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