feat(frontend): Swap from=/to= tokenlist symbols, unique-symbol CI, Share logos #715

Closed
opened 2026-08-31 16:08:46 +00:00 by PlasticDigits · 11 comments
PlasticDigits commented 2026-08-31 16:08:46 +00:00 (Migrated from gitlab.com)

Summary

Follow-up to #711 / #713. One product surface: human swap links.

Bundle (do not split):

  1. Tokenlist unique symbols — tokenlist/tokenlist.json must reject duplicate tickers (case-insensitive: UST1 and ust1 cannot both exist).
  2. Swap from= / to= accept tokenlist symbols — /?from=UST1&to=USTR in addition to native denoms (uluna / uusd) and checksummed terra1 addresses.
  3. Canonical outbound + Share payload prefer symbols — rewrite and Share emit from=UST1 (published tokenlist casing) when a unique symbol exists; otherwise keep denom / bech32.
  4. Swap Share chrome — header control reads Share {pay logo} → {receive logo} and updates when either picker changes.

Land as a new MR on top of #713. Do not reopen #711/#713. Factory gate, gem hide, hostile ignore, reverse-quote execute-is-offer-in, and “never window.location.href” stay.

Related: #542 (bundled tokenlist, no runtime HTTP), #630 (display LUNC/USTC), #665 (Share primitive), #489 (silent fail-closed), #562 (gems / X1), #508 (U1), #653 (no extra chrome), #378 (logo host allowlist).

Current codebase

Inbound tickers are a hardcoded registry, not the tokenlist

resolveSwapQueryTokenValue already maps Q711-5 product tickers via lookupTokenIdByProductTicker: LUNC/uluna → uluna, USTC/UST/uusd → uusd, and hub CW20s (CL8Y, UST1, cLUNC, cUSTC, vFDUSD, USTR, ALPHA, USTRIX, SpaceUSD) through CW20_MAP. Checksummed terra1 still works. Hostile / 0x / ETH / ibc/ / factory/ / gems-as-tickers (RUBY) fail closed.

That map is hand-maintained in tokenRegistry.ts. Swap never reads tokenlist/tokenlist.json. Create Pair does (createPairTokenCatalog.ts, C542-11). A token added only to the published list is selectable on /create but /?from=NEWSYM is ignored until someone copies it into CW20_MAP. Registry vs tokenlist address drift is already asserted in createPairTokenCatalog.test.ts; there is no symbol-uniqueness check.

Inbound still works for from=UST1 today only because UST1 is in the registry. The gap is: tokenlist is not the source of truth, uniqueness is not enforced, and outbound does not emit symbols.

Outbound helpers still write execute ids

canonicalSwapSearch sets from / to to payId / receiveId (execute ids: uluna / uusd / checksummed bech32). Comment on swapDeepLinkPath: “Bech32 / native denoms only — not display tickers.”

Callers that inherit this:

Helper Today
canonicalSwapSearch / SwapPage setSearchParams rewrite (Q713-1) /?from=uluna&to=terra1f0eq…
swapDeepLinkPath same
buildCanonicalSwapShareUrl same (URLSearchParams from resolved ids)
ust1SecondarySwapPath from=<ust1 bech32>&to=<quote bech32>

parseSwapQueryParams is shared with tradeQueryResolve.ts (/trade?from=&to= → unique pair). Trade already benefits if inbound symbols resolve; it does not keep query in the canonical /trade/{pair} path (Q713-7). Do not change Trade identity.

Vitest / Playwright still treat from=uluna&to=uusd as the canonical string (swapQueryParams.test.ts, sharePageLink.test.ts, e2e/swap-url-sync-713.spec.ts, e2e/swap-query-params.spec.ts).

Tokenlist has no unique-symbol gate

tokenlist/README.md requires symbol and SYMBOL.png filenames. It does not say symbols must be unique case-insensitively. Current list includes mixed-case SpaceUSD next to uppercase UST1 / CL8Y. Two rows ust1 and UST1 would both parse, Create Pair would show two labels that collide in a ticker lookup, and from=UST1 would be ambiguous.

buildCreatePairCw20Options drops duplicate addresses (lowercase), not duplicate symbols. scripts/qa/keplr_cw20_registry_validate.py reads the tokenlist for Keplr pack parity, not ticker uniqueness. No make target fails a colliding symbol.

Share button is still the word “Share”

SwapPage.tsx mounts ShareLinkButton next to Settings (data-testid="swap-share-link"). Visible label is SHARE_LINK_BUTTON_LABEL = Share. buttonLabel is a string. Logos are not passed.

Web Share / clipboard text is swapShareText: CL8Y DEX swap UST1 → USTR (display symbols, not logos). aria-label is the generic Share swap link. Trader / Portfolio Share must stay text-only (TS-1–TS-13).

TokenLogo already renders allowlisted logoURI (or blockie fallback). resolveAllowedTokenLogoUri (#378) must stay the only <img src> path.

Why the new implementation is needed

  1. Shared links are unreadable. After #713 the bar and Share copy a 64-character terra1… for every CW20. Humans and Telegram/Twitter previews want /?from=UST1&to=USTR. Addresses remain valid inbound for bots and for factory tokens with no tokenlist row.
  2. Inbound symbols are incomplete. Q711-5 covers the registry, not the published list. Adding a community token to tokenlist.json should make from=SYMBOL work without a second hand-edit in tokenRegistry.ts.
  3. Duplicate tickers are a phishing primitive. If the list can contain UST1 and ust1 pointing at different contracts, from=UST1 is an attacker-chosen contract. Uniqueness is a CI gate, not a runtime “pick the first row” heuristic.
  4. Share chrome does not show which pair you are sending. The control says Share while the pickers already show logos. Share {logo} → {logo} that tracks the live pair makes the copied link match what the user sees, without a second card (C653).

This is frontend query encoding + tokenlist CI + one Share label. No contract migrate, no indexer change, no Coolify env, no runtime HTTP tokenlist fetch.

Constraints / guardrails

Tokenlist uniqueness

  • Case-insensitive unique symbol. Fold with Unicode-aware ASCII case fold (UST1 ≡ ust1 ≡ Ust1). Empty / whitespace-only symbols are invalid.
  • Unique execute ids too (native denom lowercase; CW20 address lowercase). Two rows must not share an address even if symbols differ.
  • CI fails the MR — do not “skip duplicates at runtime.” A colliding list must not ship.
  • Do not invent a new on-chain registry. Bundled JSON only (C542-11): no fetch(tokenlist.json) in the dApp.
  • Do not treat LCD token_info.symbol as the query key (X1): a gem or unlisted CW20 spoofing symbol=UST1 must not win over the tokenlist row. Factory-listed but unpublished tokens share as bech32, never as a colliding ticker.
  • Gems stay out of tokenlist.json (#562 / U6). Uniqueness check must not be used to sneak RUBY into Swap links.

Query parse / rewrite / Share URL

  • Inbound still accepts checksummed terra1, uluna / uusd, Uniswap aliases (Q711), and tokenlist symbols (case-insensitive). UST → uusd stays an inbound-only alias (not a tokenlist symbol).
  • Resolve order: hostile / overlong / javascript: / data: / http(s): / // / 0x / ibc/ / factory/ / ETH/BNB/WETH → ignore that side, never echo. Else native denom. Else unique tokenlist symbol → execute id (env overlay address wins on LocalTerra, same as C542-5). Else checksummed terra1. Else null.
  • Factory gate + gem hide unchanged (applySwapQueryParams, Q711-2 / Q711-4). Symbol resolve is not a license to inject an unlisted CW20 into the picker.
  • Outbound canonical keys remain first-party from / to / exactAmount / exactField via URL + URLSearchParams (Q713-9). Uniswap names stay inbound-only. After apply, rewrite symbols when unique, else execute id. Published tokenlist casing (cLUNC, vFDUSD, SpaceUSD, UST1), not forced uppercase.
  • Natives outbound: prefer LUNC / USTC, not uluna / uusd. Inbound from=uluna still applies, then rewrite to from=LUNC.
  • Same-token / one-sided / ignored field still fail closed to retail defaults (Q711-7, #489). No lecture banner.
  • Production cannot encode or share a gem. Never honor ?showGems=1 (Q713-8).
  • Share URL still not window.location.href. Origin is window.location.origin. No VITE_PUBLIC_ORIGIN. Drop leftover recipient / WC / Uniswap keys.
  • Execute path unchanged: offer-in + min received; reverse quote stays Q713-4. Amount / exactField encoding unchanged.
  • U1: from=UST1 is AMM Swap, not /ust1 mint/redeem.
  • #630: combobox display stays LUNC/USTC; query encoding may use those symbols.
  • Do not persist query in localStorage. Do not map EVM 0x to Terra. Do not change nginx. Do not mount a second Swap at /swap.

Share chrome

  • Visible Swap control: Share + pay TokenLogo + → + receive TokenLogo. Updates when fromToken / toToken change (picker, flip, inbound apply).
  • Logos via existing TokenLogo + resolveAllowedTokenLogoUri only. alt="" (decorative). Failed / unknown URI → blockie from address/denom seed, not a remote fetch of attacker logoURI.
  • aria-label must be text (e.g. Share LUNC to UST1 swap link), not “Share image to image.” Visible word Share stays for #489.
  • Trader / Portfolio ShareLinkButton stays the word Share — do not put pair logos there.
  • No new shell-panel* / card-glass (C653). Coarse+narrow: Web Share; else copy Link copied. AbortError silent. 44px hit target, focus ring, sounds.playButtonPress.
  • Web Share title/text stay strings (symbols OK in swapShareText). Do not put HTML/SVG in the share sheet.
  • Hide Share when either side is a gem or ids are missing (Q713-5 / Q713-8).

Relevant files

Path Why
tokenlist/tokenlist.json Published catalog; unique symbol / address / denom
tokenlist/README.md Document uniqueness + query/share encoding
frontend-dapp/src/utils/createPairTokenCatalog.ts Already imports bundled list; reuse, do not fetch
frontend-dapp/src/utils/swapQueryParams.ts Parse, resolve, canonicalSwapSearch, swapDeepLinkPath
frontend-dapp/src/utils/tokenRegistry.ts Today’s ticker map; should derive from / stay aligned with tokenlist
frontend-dapp/src/utils/sharePageLink.ts buildCanonicalSwapShareUrl, swapShareText
frontend-dapp/src/utils/sharePageLinkCopy.ts Swap aria-label pattern
frontend-dapp/src/pages/SwapPage.tsx Rewrite + Share mount
frontend-dapp/src/components/ui/ShareLinkButton.tsx Allow logo slot / ReactNode label without breaking trader
frontend-dapp/src/components/ui/TokenLogo.tsx Allowlisted logos
frontend-dapp/src/utils/tokenLogoAllowlist.ts #378 host allowlist
frontend-dapp/src/utils/tokenDisplay.ts Display symbol / logoURI
frontend-dapp/src/utils/ust1SecondaryMarket.ts First-party Swap CTA should emit from=UST1&to=vFDUSD
frontend-dapp/src/utils/tradeQueryResolve.ts Inbound symbols via shared parser; path stays /trade/{pair}
docs/frontend.md Q711 / Q713 sections + new invariants
skills/AGENTS_FRONTEND_SWAP_QUERY_PARAMS.md Inbound symbols + tokenlist
skills/AGENTS_FRONTEND_SWAP_URL_SYNC.md Canonical outbound symbols + Share chrome
skills/AGENTS_FRONTEND_SHARE_LINK.md TS-2 Swap exception: symbol query + logo label
scripts/qa/ + Makefile New verify-issue-* + uniqueness check
  1. Static uniqueness checker (Python, no network) over tokenlist/tokenlist.json: fail on duplicate case-folded symbols, duplicate lowercase addresses, duplicate lowercase denoms, empty symbol. Wire into CI / make verify-issue-{iid} and mention it in tokenlist/README.md. Optional: Vitest that reads the same JSON so frontend tests fail in the same PR.
  2. Single bundled lookup helper used by Swap query parse and Create Pair catalog (do not copy-paste two maps). Input: tokenlist rows + existing Vite overlays (C542-5). Output: case-insensitive symbol → execute id and execute id → published symbol. Prefer this over growing CW20_MAP by hand. Keep UST → uusd as an extra inbound alias not in the JSON.
  3. canonicalSwapSearch (and therefore rewrite, Share URL, swapDeepLinkPath) encode with executeIdToQueryToken(id): tokenlist symbol if unique mapping exists, else uluna/uusd/terra1. Inbound resolveSwapQueryTokenValue remains the inverse. swapSearchEquals still compares URLSearchParams strings — after this change the canonical string for LUNC/USTC is from=LUNC&to=USTC.
  4. First-party CTAs (ust1SecondarySwapPath, any marketing/docs examples in-repo) switch to symbols. Old bech32 / uluna links must keep working.
  5. Share chrome: extend ShareLinkButton with an optional buttonContent?: ReactNode (or a thin SwapShareButton wrapper) so Swap can render Share + two TokenLogos + a literal →. Default remains the string Share. Drive logos from the same registry/tokenlist logoURI as the pickers. Update aria-label from live symbols.
  6. Docs / skills / verify scripts for #711 and #713: update expected canonical strings; do not weaken hostile/gem/factory tests.

Do not use LCD token_info.symbol for query encoding. Do not uppercase-normalize outbound (cLUNC must not become CLUNC if that is a different tokenlist row — uniqueness already forbids that pair).

Acceptance criteria

  • TL-1. CI / make target fails if tokenlist.json has two tokens whose symbol matches case-insensitively (fixture: UST1 + ust1), or duplicate address/denom. Current published list passes.
  • TL-2. README states uniqueness and that Swap from/to / Share use those symbols.
  • QS-1. /?from=UST1&to=USTR (and from=ust1, mixed case) selects those factory tokens after apply. Same for LUNC/USTC/cLUNC/vFDUSD/SpaceUSD and every other current tokenlist row that is in getAllTokens.
  • QS-2. Checksummed terra1 and uluna/uusd still apply. After apply, the bar rewrites to the tokenlist symbol when one exists (from=uluna&to=<ust1 bech32> → from=LUNC&to=UST1).
  • QS-3. Factory-listed CW20 not in the tokenlist still applies by address; canonical/share keep the bech32 (no invented ticker).
  • QS-4. Unlisted / gem / hostile / spoofed ticker still ignored per side; raw string never appears in combobox, share text, or href. ?showGems=1 still inert.
  • QS-5. /swap?from=UST1&to=cUSTC preserves search on redirect to /. /trade?from=UST1&to=cUSTC still replaces to /trade/{uniquePair} when unique.
  • QS-6. swapDeepLinkPath / ust1SecondarySwapPath / Share URL use symbols when available. Amount / exactField=output encoding unchanged. Execute still offer-in.
  • SH-1. Swap header Share visible label is Share {pay TokenLogo} → {receive TokenLogo} (data-testid="swap-share-link"). Logos update on picker change and flip. aria-label includes both display symbols.
  • SH-2. Trader / Portfolio Share still say Share with no pair logos. No extra shell-panel* / card-glass. Desktop copy / mobile Web Share behavior unchanged (Q713-5).
  • SH-3. Copied / shared URL is {origin}/?from=<symbol-or-id>&to=<symbol-or-id> (+ amount/exactField), never location.href, never a gem.
  • Docs: docs/frontend.md, swap query + URL-sync + share skills, docs/testing.md, AGENTS.md, make verify-issue-{iid} plus regressions make verify-issue-711 and make verify-issue-713.

Test plan (all paths)

Tokenlist / CI

  • Current tokenlist.json uniqueness passes.
  • Fixture JSON with UST1 + ust1 (different addresses) fails the checker.
  • Fixture with two identical lowercase CW20 addresses, different symbols, fails.
  • Fixture with two uluna native rows fails.
  • Empty symbol / whitespace symbol fails.
  • SpaceUSD vs SPACEUSD fails; current single SpaceUSD row passes.

Inbound parse (Vitest swapQueryParams / tokenRegistry / catalog)

  • Each published tokenlist symbol (all casings: published, lower, upper) → the overlay-or-published execute id.
  • from=LUNC&to=USTC, from=uluna&to=uusd, from=UST&to=CL8Y.
  • from=<UST1 bech32>&to=vFDUSD.
  • One-sided ?to=UST1 → receive UST1, pay the other retail default.
  • Same token both sides (from=UST1&to=ust1) → keep pay, default the other.
  • Uniswap inputCurrency=UST1&outputCurrency=USTR still applies, then rewrite to from=UST1&to=USTR.
  • Amount + exactField=output still parse; reverse-quote rules unchanged.
  • LocalTerra overlay: symbol UST1 resolves to VITE_UST1_TOKEN_ADDRESS when set; outbound still from=UST1.

Factory / gem / hostile (do not regress #711)

  • Unlisted checksummed terra1 ignored.
  • Gem address and gem ticker RUBY ignored in production; showGems=1 ignored.
  • javascript:, data:, URLs, 0x, ETH, ibc/, factory/, overlong, <>"', %3Cscript%3E ignored and not echoed.
  • Spoof: factory gem whose LCD symbol is UST1 does not satisfy from=UST1 (X1).

Rewrite / Share URL

  • After apply, searchParams equals canonicalSwapSearch with symbols.
  • Picker / flip updates symbols in the bar (replace, no history spam).
  • Share clipboard / navigator.share URL matches the canonical bar (plus origin), not leftover aliases.
  • Token with no tokenlist row: share/rewrite keep bech32; inbound of that bech32 still works.
  • ust1SecondarySwapPath() is /?from=UST1&to=vFDUSD (or cUSTC quote leg) on mainnet ids.

Swap page RTL

  • /?from=UST1&to=USTR selects those tokens (extend SwapPage.queryParams.test.tsx).
  • After load from bech32 query, bar shows symbols.
  • Share button present; accessible name includes both symbols; two imgs or blockies inside swap-share-link.
  • Flip updates logos and aria-label.
  • Gem pair: Share hidden; bar does not write gem symbols.

ShareLinkButton

  • Default (trader) still text Share, no logos.
  • Swap override renders logos without dropping type=button, focus, copy/share/abort paths.

Trade / Create Pair

  • /trade?from=UST1&to=cUSTC unique pair → /trade/{addr} (extend tradeQueryResolve tests). Ambiguous / missing still ignore.
  • /create catalog unchanged except it cannot ship a colliding tokenlist (CI). No Swap factory graph leak (C542-8).

Playwright (5 workers; no e2e-tx required)

  • /?from=UST1&to=USTR (and /swap?from=LUNC&to=USTC) selects labels LUNC/USTC/UST1 as applicable; URL ends canonical symbols.
  • Hostile from= still ignored.
  • Share click (desktop mock): clipboard URL has from= / to= symbols; button shows two logos that change after flip.

Docs / verify

  • make verify-issue-{iid}
  • make verify-issue-711 and make verify-issue-713 (update expected canonical strings, do not drop hostile/gem cases)
  • make verify-issue-665, make verify-issue-542, make verify-issue-630, python3 scripts/check_chrome_nesting.py

Test plan (attack, hack, and abuse)

Vector Expect
Ticker collision / homograph UST1 vs ust1 vs Uѕt1 (Cyrillic) cannot ship. Non-ASCII symbols: reject or treat as distinct and never match ASCII UST1. Prefer reject non-ASCII tickers in the uniqueness script if not already in the list.
Phish from=UST1 to a hostile CW20 Lookup is tokenlist address (overlay), then factory-gate. LCD token_info.symbol is not consulted for query ids.
Open redirect / XSS in from= Hostile ignore; Navigate / Share use URL + URLSearchParams only. Logos never use query string as src.
logoURI javascript/data/http resolveAllowedTokenLogoUri drops; blockie fallback. Share button must not set src from query or from unchecked JSON beyond the allowlist.
Overlong / repeating keys SWAP_QUERY_VALUE_MAX_LEN; last non-empty within family; no ReDoS on symbol fold.
Gem + showGems=1 Still inert. Share/rewrite never emit gem symbols or gem bech32 on production builds.
Unlisted factory token claiming CL8Y No tokenlist row → encoded as bech32. Cannot steal from=CL8Y.
Web Share / clipboard leak Still not location.href (no WC URI, recipient, Uniswap leftovers).
Create Pair catalog collision CI blocks the list before picker shows two identical tickers.
LocalTerra overlay mismatch Symbol still UST1; execute id is overlay. Do not write the overlay bech32 into the public share URL when the symbol is unique.
Trade auto-place Symbol query still must not Place / connect / skip #678.
Cognitive overload No banner “we rewrote your address to UST1.” Silent rewrite.

Verification criteria

Done when:

  1. make verify-issue-{iid} is green on a clean tree.
  2. make verify-issue-711 and make verify-issue-713 are green with updated canonical expectations (from=LUNC&to=USTC, from=UST1, …) and all hostile/gem/factory cases still fail closed.
  3. Published tokenlist uniqueness is enforced in CI; a planted ust1 duplicate fails.
  4. Manual or Playwright: open http://127.0.0.1:5173/?from=UST1&to=USTR → pickers match; bar shows those symbols; Share reads Share {UST1 logo} → {USTR logo}; copied URL uses from=UST1&to=USTR; flip updates logos and URL.
  5. Paste an old #713 bech32/uluna link → pair still applies → bar/Share rewrite to symbols.
  6. Trader profile Share still says Share with no token logos.
  7. No new chrome nesting; no runtime HTTP tokenlist; no execute-path change.

Issue type: frontend feature. Priority: P2 (share/deep-link UX; not a launch blocker). No Coolify / wasm / indexer.

## Summary Follow-up to [#711](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/711) / [#713](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/713). One product surface: **human swap links**. Bundle (do not split): 1. **Tokenlist unique symbols** — `tokenlist/tokenlist.json` must reject duplicate tickers (case-insensitive: `UST1` and `ust1` cannot both exist). 2. **Swap `from=` / `to=` accept tokenlist symbols** — `/?from=UST1&to=USTR` in addition to native denoms (`uluna` / `uusd`) and checksummed `terra1` addresses. 3. **Canonical outbound + Share payload prefer symbols** — rewrite and Share emit `from=UST1` (published tokenlist casing) when a unique symbol exists; otherwise keep denom / bech32. 4. **Swap Share chrome** — header control reads **Share {pay logo} → {receive logo}** and updates when either picker changes. Land as a **new MR on top of #713**. Do not reopen #711/#713. Factory gate, gem hide, hostile ignore, reverse-quote execute-is-offer-in, and “never `window.location.href`” stay. Related: #542 (bundled tokenlist, no runtime HTTP), #630 (display LUNC/USTC), #665 (Share primitive), #489 (silent fail-closed), #562 (gems / **X1**), #508 (**U1**), #653 (no extra chrome), #378 (logo host allowlist). ## Current codebase ### Inbound tickers are a hardcoded registry, not the tokenlist [`resolveSwapQueryTokenValue`](frontend-dapp/src/utils/swapQueryParams.ts) already maps **Q711-5** product tickers via [`lookupTokenIdByProductTicker`](frontend-dapp/src/utils/tokenRegistry.ts): `LUNC`/`uluna` → `uluna`, `USTC`/`UST`/`uusd` → `uusd`, and hub CW20s (`CL8Y`, `UST1`, `cLUNC`, `cUSTC`, `vFDUSD`, `USTR`, `ALPHA`, `USTRIX`, `SpaceUSD`) through `CW20_MAP`. Checksummed `terra1` still works. Hostile / `0x` / `ETH` / `ibc/` / `factory/` / gems-as-tickers (`RUBY`) fail closed. That map is **hand-maintained** in `tokenRegistry.ts`. Swap never reads [`tokenlist/tokenlist.json`](tokenlist/tokenlist.json). Create Pair does ([`createPairTokenCatalog.ts`](frontend-dapp/src/utils/createPairTokenCatalog.ts), **C542-11**). A token added only to the published list is selectable on `/create` but **`/?from=NEWSYM` is ignored** until someone copies it into `CW20_MAP`. Registry vs tokenlist address drift is already asserted in [`createPairTokenCatalog.test.ts`](frontend-dapp/src/utils/__tests__/createPairTokenCatalog.test.ts); there is **no** symbol-uniqueness check. Inbound still works for `from=UST1` today **only because** UST1 is in the registry. The gap is: tokenlist is not the source of truth, uniqueness is not enforced, and **outbound does not emit symbols**. ### Outbound helpers still write execute ids [`canonicalSwapSearch`](frontend-dapp/src/utils/swapQueryParams.ts) sets `from` / `to` to `payId` / `receiveId` (execute ids: `uluna` / `uusd` / checksummed bech32). Comment on `swapDeepLinkPath`: “Bech32 / native denoms only — not display tickers.” Callers that inherit this: | Helper | Today | |--------|--------| | `canonicalSwapSearch` / SwapPage `setSearchParams` rewrite (**Q713-1**) | `/?from=uluna&to=terra1f0eq…` | | `swapDeepLinkPath` | same | | [`buildCanonicalSwapShareUrl`](frontend-dapp/src/utils/sharePageLink.ts) | same (`URLSearchParams` from resolved ids) | | [`ust1SecondarySwapPath`](frontend-dapp/src/utils/ust1SecondaryMarket.ts) | `from=<ust1 bech32>&to=<quote bech32>` | [`parseSwapQueryParams`](frontend-dapp/src/utils/swapQueryParams.ts) is shared with [`tradeQueryResolve.ts`](frontend-dapp/src/utils/tradeQueryResolve.ts) (`/trade?from=&to=` → unique pair). Trade already benefits if inbound symbols resolve; it does **not** keep query in the canonical `/trade/{pair}` path (**Q713-7**). Do not change Trade identity. Vitest / Playwright still treat `from=uluna&to=uusd` as the canonical string ([`swapQueryParams.test.ts`](frontend-dapp/src/utils/__tests__/swapQueryParams.test.ts), [`sharePageLink.test.ts`](frontend-dapp/src/utils/__tests__/sharePageLink.test.ts), [`e2e/swap-url-sync-713.spec.ts`](frontend-dapp/e2e/swap-url-sync-713.spec.ts), [`e2e/swap-query-params.spec.ts`](frontend-dapp/e2e/swap-query-params.spec.ts)). ### Tokenlist has no unique-symbol gate [`tokenlist/README.md`](tokenlist/README.md) requires `symbol` and `SYMBOL.png` filenames. It does **not** say symbols must be unique case-insensitively. Current list includes mixed-case `SpaceUSD` next to uppercase `UST1` / `CL8Y`. Two rows `ust1` and `UST1` would both parse, Create Pair would show two labels that collide in a ticker lookup, and `from=UST1` would be ambiguous. [`buildCreatePairCw20Options`](frontend-dapp/src/utils/createPairTokenCatalog.ts) drops **duplicate addresses** (lowercase), not duplicate symbols. [`scripts/qa/keplr_cw20_registry_validate.py`](scripts/qa/keplr_cw20_registry_validate.py) reads the tokenlist for Keplr pack parity, not ticker uniqueness. No `make` target fails a colliding `symbol`. ### Share button is still the word “Share” [`SwapPage.tsx`](frontend-dapp/src/pages/SwapPage.tsx) mounts [`ShareLinkButton`](frontend-dapp/src/components/ui/ShareLinkButton.tsx) next to Settings (`data-testid="swap-share-link"`). Visible label is [`SHARE_LINK_BUTTON_LABEL`](frontend-dapp/src/utils/sharePageLinkCopy.ts) = **Share**. `buttonLabel` is a `string`. Logos are not passed. Web Share / clipboard `text` is [`swapShareText`](frontend-dapp/src/utils/sharePageLink.ts): `CL8Y DEX swap UST1 → USTR` (display symbols, not logos). `aria-label` is the generic **Share swap link**. Trader / Portfolio Share must stay text-only (**TS-1–TS-13**). [`TokenLogo`](frontend-dapp/src/components/ui/TokenLogo.tsx) already renders allowlisted `logoURI` (or blockie fallback). [`resolveAllowedTokenLogoUri`](frontend-dapp/src/utils/tokenLogoAllowlist.ts) (#378) must stay the only `<img src>` path. ## Why the new implementation is needed 1. **Shared links are unreadable.** After #713 the bar and Share copy a 64-character `terra1…` for every CW20. Humans and Telegram/Twitter previews want `/?from=UST1&to=USTR`. Addresses remain valid inbound for bots and for factory tokens with no tokenlist row. 2. **Inbound symbols are incomplete.** Q711-5 covers the registry, not the published list. Adding a community token to `tokenlist.json` should make `from=SYMBOL` work without a second hand-edit in `tokenRegistry.ts`. 3. **Duplicate tickers are a phishing primitive.** If the list can contain `UST1` and `ust1` pointing at different contracts, `from=UST1` is an attacker-chosen contract. Uniqueness is a **CI gate**, not a runtime “pick the first row” heuristic. 4. **Share chrome does not show which pair you are sending.** The control says Share while the pickers already show logos. **Share {logo} → {logo}** that tracks the live pair makes the copied link match what the user sees, without a second card (**C653**). This is **frontend query encoding + tokenlist CI + one Share label**. No contract migrate, no indexer change, no Coolify env, no runtime HTTP tokenlist fetch. ## Constraints / guardrails ### Tokenlist uniqueness - **Case-insensitive unique `symbol`.** Fold with Unicode-aware ASCII case fold (`UST1` ≡ `ust1` ≡ `Ust1`). Empty / whitespace-only symbols are invalid. - **Unique execute ids too** (native `denom` lowercase; CW20 `address` lowercase). Two rows must not share an address even if symbols differ. - **CI fails the MR** — do not “skip duplicates at runtime.” A colliding list must not ship. - **Do not** invent a new on-chain registry. Bundled JSON only (**C542-11**): no `fetch(tokenlist.json)` in the dApp. - **Do not** treat LCD `token_info.symbol` as the query key (**X1**): a gem or unlisted CW20 spoofing `symbol=UST1` must not win over the tokenlist row. Factory-listed but unpublished tokens share as **bech32**, never as a colliding ticker. - Gems stay out of `tokenlist.json` (**#562** / **U6**). Uniqueness check must not be used to sneak `RUBY` into Swap links. ### Query parse / rewrite / Share URL - **Inbound still accepts** checksummed `terra1`, `uluna` / `uusd`, Uniswap aliases (**Q711**), and tokenlist symbols (case-insensitive). `UST` → `uusd` stays an inbound-only alias (not a tokenlist symbol). - **Resolve order:** hostile / overlong / `javascript:` / `data:` / `http(s):` / `//` / `0x` / `ibc/` / `factory/` / `ETH`/`BNB`/`WETH` → ignore that side, never echo. Else native denom. Else unique tokenlist symbol → execute id (env overlay address wins on LocalTerra, same as **C542-5**). Else checksummed `terra1`. Else `null`. - **Factory gate + gem hide unchanged** (`applySwapQueryParams`, **Q711-2 / Q711-4**). Symbol resolve is not a license to inject an unlisted CW20 into the picker. - **Outbound canonical keys** remain first-party `from` / `to` / `exactAmount` / `exactField` via `URL` + `URLSearchParams` (**Q713-9**). Uniswap names stay inbound-only. After apply, rewrite **symbols when unique**, else execute id. Published tokenlist **casing** (`cLUNC`, `vFDUSD`, `SpaceUSD`, `UST1`), not forced uppercase. - Natives outbound: prefer **`LUNC` / `USTC`**, not `uluna` / `uusd`. Inbound `from=uluna` still applies, then rewrite to `from=LUNC`. - **Same-token / one-sided / ignored field** still fail closed to retail defaults (**Q711-7**, **#489**). No lecture banner. - Production cannot encode or share a gem. Never honor `?showGems=1` (**Q713-8**). - Share URL still **not** `window.location.href`. Origin is `window.location.origin`. No `VITE_PUBLIC_ORIGIN`. Drop leftover `recipient` / WC / Uniswap keys. - Execute path unchanged: offer-in + min received; reverse quote stays **Q713-4**. Amount / `exactField` encoding unchanged. - **U1:** `from=UST1` is AMM Swap, not `/ust1` mint/redeem. - **#630:** combobox display stays LUNC/USTC; query encoding may use those symbols. - Do **not** persist query in `localStorage`. Do not map EVM `0x` to Terra. Do not change nginx. Do not mount a second Swap at `/swap`. ### Share chrome - Visible Swap control: **Share** + pay `TokenLogo` + **→** + receive `TokenLogo`. Updates when `fromToken` / `toToken` change (picker, flip, inbound apply). - Logos via existing `TokenLogo` + `resolveAllowedTokenLogoUri` only. `alt=""` (decorative). Failed / unknown URI → blockie from address/denom seed, not a remote fetch of attacker `logoURI`. - **`aria-label` must be text** (e.g. **Share LUNC to UST1 swap link**), not “Share image to image.” Visible word **Share** stays for #489. - Trader / Portfolio `ShareLinkButton` stays the word **Share** — do not put pair logos there. - No new `shell-panel*` / `card-glass` (**C653**). Coarse+narrow: Web Share; else copy **Link copied**. `AbortError` silent. 44px hit target, focus ring, `sounds.playButtonPress`. - Web Share `title`/`text` stay strings (symbols OK in `swapShareText`). Do not put HTML/SVG in the share sheet. - Hide Share when either side is a gem or ids are missing (**Q713-5 / Q713-8**). ## Relevant files | Path | Why | |------|-----| | `tokenlist/tokenlist.json` | Published catalog; unique `symbol` / address / denom | | `tokenlist/README.md` | Document uniqueness + query/share encoding | | `frontend-dapp/src/utils/createPairTokenCatalog.ts` | Already imports bundled list; reuse, do not fetch | | `frontend-dapp/src/utils/swapQueryParams.ts` | Parse, resolve, `canonicalSwapSearch`, `swapDeepLinkPath` | | `frontend-dapp/src/utils/tokenRegistry.ts` | Today’s ticker map; should derive from / stay aligned with tokenlist | | `frontend-dapp/src/utils/sharePageLink.ts` | `buildCanonicalSwapShareUrl`, `swapShareText` | | `frontend-dapp/src/utils/sharePageLinkCopy.ts` | Swap aria-label pattern | | `frontend-dapp/src/pages/SwapPage.tsx` | Rewrite + Share mount | | `frontend-dapp/src/components/ui/ShareLinkButton.tsx` | Allow logo slot / ReactNode label without breaking trader | | `frontend-dapp/src/components/ui/TokenLogo.tsx` | Allowlisted logos | | `frontend-dapp/src/utils/tokenLogoAllowlist.ts` | #378 host allowlist | | `frontend-dapp/src/utils/tokenDisplay.ts` | Display symbol / logoURI | | `frontend-dapp/src/utils/ust1SecondaryMarket.ts` | First-party Swap CTA should emit `from=UST1&to=vFDUSD` | | `frontend-dapp/src/utils/tradeQueryResolve.ts` | Inbound symbols via shared parser; path stays `/trade/{pair}` | | `docs/frontend.md` | Q711 / Q713 sections + new invariants | | `skills/AGENTS_FRONTEND_SWAP_QUERY_PARAMS.md` | Inbound symbols + tokenlist | | `skills/AGENTS_FRONTEND_SWAP_URL_SYNC.md` | Canonical outbound symbols + Share chrome | | `skills/AGENTS_FRONTEND_SHARE_LINK.md` | TS-2 Swap exception: symbol query + logo label | | `scripts/qa/` + `Makefile` | New `verify-issue-*` + uniqueness check | ## Recommended direction 1. **Static uniqueness checker** (Python, no network) over `tokenlist/tokenlist.json`: fail on duplicate case-folded symbols, duplicate lowercase addresses, duplicate lowercase denoms, empty symbol. Wire into CI / `make verify-issue-{iid}` and mention it in `tokenlist/README.md`. Optional: Vitest that reads the same JSON so frontend tests fail in the same PR. 2. **Single bundled lookup helper** used by Swap query parse **and** Create Pair catalog (do not copy-paste two maps). Input: tokenlist rows + existing Vite overlays (**C542-5**). Output: case-insensitive `symbol → execute id` and `execute id → published symbol`. Prefer this over growing `CW20_MAP` by hand. Keep `UST` → `uusd` as an extra inbound alias **not** in the JSON. 3. **`canonicalSwapSearch`** (and therefore rewrite, Share URL, `swapDeepLinkPath`) encode with `executeIdToQueryToken(id)`: tokenlist symbol if unique mapping exists, else `uluna`/`uusd`/`terra1`. Inbound `resolveSwapQueryTokenValue` remains the inverse. `swapSearchEquals` still compares `URLSearchParams` strings — after this change the canonical string for LUNC/USTC is `from=LUNC&to=USTC`. 4. **First-party CTAs** (`ust1SecondarySwapPath`, any marketing/docs examples in-repo) switch to symbols. Old bech32 / `uluna` links must **keep working**. 5. **Share chrome:** extend `ShareLinkButton` with an optional `buttonContent?: ReactNode` (or a thin `SwapShareButton` wrapper) so Swap can render `Share` + two `TokenLogo`s + a literal `→`. Default remains the string **Share**. Drive logos from the same registry/tokenlist `logoURI` as the pickers. Update `aria-label` from live symbols. 6. **Docs / skills / verify scripts** for #711 and #713: update expected canonical strings; do not weaken hostile/gem/factory tests. Do **not** use LCD `token_info.symbol` for query encoding. Do **not** uppercase-normalize outbound (`cLUNC` must not become `CLUNC` if that is a different tokenlist row — uniqueness already forbids that pair). ## Acceptance criteria - [ ] **TL-1.** CI / `make` target fails if `tokenlist.json` has two tokens whose `symbol` matches case-insensitively (fixture: `UST1` + `ust1`), or duplicate address/denom. Current published list passes. - [ ] **TL-2.** README states uniqueness and that Swap `from`/`to` / Share use those symbols. - [ ] **QS-1.** `/?from=UST1&to=USTR` (and `from=ust1`, mixed case) selects those factory tokens after apply. Same for `LUNC`/`USTC`/`cLUNC`/`vFDUSD`/`SpaceUSD` and every other current tokenlist row that is in `getAllTokens`. - [ ] **QS-2.** Checksummed `terra1` and `uluna`/`uusd` still apply. After apply, the bar **rewrites** to the tokenlist symbol when one exists (`from=uluna&to=<ust1 bech32>` → `from=LUNC&to=UST1`). - [ ] **QS-3.** Factory-listed CW20 **not** in the tokenlist still applies by address; canonical/share keep the bech32 (no invented ticker). - [ ] **QS-4.** Unlisted / gem / hostile / spoofed ticker still ignored per side; raw string never appears in combobox, share `text`, or `href`. `?showGems=1` still inert. - [ ] **QS-5.** `/swap?from=UST1&to=cUSTC` preserves search on redirect to `/`. `/trade?from=UST1&to=cUSTC` still `replace`s to `/trade/{uniquePair}` when unique. - [ ] **QS-6.** `swapDeepLinkPath` / `ust1SecondarySwapPath` / Share URL use symbols when available. Amount / `exactField=output` encoding unchanged. Execute still offer-in. - [ ] **SH-1.** Swap header Share visible label is **Share {pay TokenLogo} → {receive TokenLogo}** (`data-testid="swap-share-link"`). Logos update on picker change and flip. `aria-label` includes both display symbols. - [ ] **SH-2.** Trader / Portfolio Share still say **Share** with no pair logos. No extra `shell-panel*` / `card-glass`. Desktop copy / mobile Web Share behavior unchanged (**Q713-5**). - [ ] **SH-3.** Copied / shared URL is `{origin}/?from=<symbol-or-id>&to=<symbol-or-id>` (+ amount/exactField), never `location.href`, never a gem. - [ ] Docs: `docs/frontend.md`, swap query + URL-sync + share skills, `docs/testing.md`, `AGENTS.md`, `make verify-issue-{iid}` plus regressions `make verify-issue-711` and `make verify-issue-713`. ## Test plan (all paths) ### Tokenlist / CI - Current `tokenlist.json` uniqueness passes. - Fixture JSON with `UST1` + `ust1` (different addresses) **fails** the checker. - Fixture with two identical lowercase CW20 addresses, different symbols, **fails**. - Fixture with two `uluna` native rows **fails**. - Empty symbol / whitespace symbol **fails**. - `SpaceUSD` vs `SPACEUSD` **fails**; current single `SpaceUSD` row **passes**. ### Inbound parse (Vitest `swapQueryParams` / `tokenRegistry` / catalog) - Each published tokenlist symbol (all casings: published, lower, upper) → the overlay-or-published execute id. - `from=LUNC&to=USTC`, `from=uluna&to=uusd`, `from=UST&to=CL8Y`. - `from=<UST1 bech32>&to=vFDUSD`. - One-sided `?to=UST1` → receive UST1, pay the other retail default. - Same token both sides (`from=UST1&to=ust1`) → keep pay, default the other. - Uniswap `inputCurrency=UST1&outputCurrency=USTR` still applies, then rewrite to `from=UST1&to=USTR`. - Amount + `exactField=output` still parse; reverse-quote rules unchanged. - LocalTerra overlay: symbol `UST1` resolves to `VITE_UST1_TOKEN_ADDRESS` when set; outbound still `from=UST1`. ### Factory / gem / hostile (do not regress #711) - Unlisted checksummed `terra1` ignored. - Gem address and gem ticker `RUBY` ignored in production; `showGems=1` ignored. - `javascript:`, `data:`, URLs, `0x`, `ETH`, `ibc/`, `factory/`, overlong, `<>"'`, `%3Cscript%3E` ignored and not echoed. - Spoof: factory gem whose LCD symbol is `UST1` does **not** satisfy `from=UST1` (**X1**). ### Rewrite / Share URL - After apply, `searchParams` equals `canonicalSwapSearch` with **symbols**. - Picker / flip updates symbols in the bar (`replace`, no history spam). - Share clipboard / `navigator.share` URL matches the canonical bar (plus origin), not leftover aliases. - Token with no tokenlist row: share/rewrite keep bech32; inbound of that bech32 still works. - `ust1SecondarySwapPath()` is `/?from=UST1&to=vFDUSD` (or `cUSTC` quote leg) on mainnet ids. ### Swap page RTL - `/?from=UST1&to=USTR` selects those tokens (extend `SwapPage.queryParams.test.tsx`). - After load from bech32 query, bar shows symbols. - Share button present; accessible name includes both symbols; two imgs or blockies inside `swap-share-link`. - Flip updates logos and `aria-label`. - Gem pair: Share hidden; bar does not write gem symbols. ### ShareLinkButton - Default (trader) still text **Share**, no logos. - Swap override renders logos without dropping `type=button`, focus, copy/share/abort paths. ### Trade / Create Pair - `/trade?from=UST1&to=cUSTC` unique pair → `/trade/{addr}` (extend `tradeQueryResolve` tests). Ambiguous / missing still ignore. - `/create` catalog unchanged except it cannot ship a colliding tokenlist (CI). No Swap factory graph leak (**C542-8**). ### Playwright (5 workers; no `e2e-tx` required) - `/?from=UST1&to=USTR` (and `/swap?from=LUNC&to=USTC`) selects labels LUNC/USTC/UST1 as applicable; URL ends canonical symbols. - Hostile `from=` still ignored. - Share click (desktop mock): clipboard URL has `from=` / `to=` symbols; button shows two logos that change after flip. ### Docs / verify - `make verify-issue-{iid}` - `make verify-issue-711` and `make verify-issue-713` (update expected canonical strings, do not drop hostile/gem cases) - `make verify-issue-665`, `make verify-issue-542`, `make verify-issue-630`, `python3 scripts/check_chrome_nesting.py` ## Test plan (attack, hack, and abuse) | Vector | Expect | |--------|--------| | **Ticker collision / homograph** | `UST1` vs `ust1` vs `Uѕt1` (Cyrillic) cannot ship. Non-ASCII symbols: reject or treat as distinct **and** never match ASCII `UST1`. Prefer reject non-ASCII tickers in the uniqueness script if not already in the list. | | **Phish `from=UST1` to a hostile CW20** | Lookup is tokenlist address (overlay), then factory-gate. LCD `token_info.symbol` is not consulted for query ids. | | **Open redirect / XSS in `from=`** | Hostile ignore; `Navigate` / Share use `URL` + `URLSearchParams` only. Logos never use query string as `src`. | | **`logoURI` javascript/data/http** | `resolveAllowedTokenLogoUri` drops; blockie fallback. Share button must not set `src` from query or from unchecked JSON beyond the allowlist. | | **Overlong / repeating keys** | `SWAP_QUERY_VALUE_MAX_LEN`; last non-empty within family; no ReDoS on symbol fold. | | **Gem + `showGems=1`** | Still inert. Share/rewrite never emit gem symbols or gem bech32 on production builds. | | **Unlisted factory token claiming `CL8Y`** | No tokenlist row → encoded as bech32. Cannot steal `from=CL8Y`. | | **Web Share / clipboard leak** | Still not `location.href` (no WC URI, `recipient`, Uniswap leftovers). | | **Create Pair catalog collision** | CI blocks the list before picker shows two identical tickers. | | **LocalTerra overlay mismatch** | Symbol still `UST1`; execute id is overlay. Do not write the overlay bech32 into the public share URL when the symbol is unique. | | **Trade auto-place** | Symbol query still must not Place / connect / skip #678. | | **Cognitive overload** | No banner “we rewrote your address to UST1.” Silent rewrite. | ## Verification criteria Done when: 1. `make verify-issue-{iid}` is green on a clean tree. 2. `make verify-issue-711` and `make verify-issue-713` are green with **updated** canonical expectations (`from=LUNC&to=USTC`, `from=UST1`, …) and all hostile/gem/factory cases still fail closed. 3. Published tokenlist uniqueness is enforced in CI; a planted `ust1` duplicate fails. 4. Manual or Playwright: open `http://127.0.0.1:5173/?from=UST1&to=USTR` → pickers match; bar shows those symbols; Share reads **Share {UST1 logo} → {USTR logo}**; copied URL uses `from=UST1&to=USTR`; flip updates logos and URL. 5. Paste an old `#713` bech32/`uluna` link → pair still applies → bar/Share rewrite to symbols. 6. Trader profile Share still says **Share** with no token logos. 7. No new chrome nesting; no runtime HTTP tokenlist; no execute-path change. Issue type: frontend feature. Priority: P2 (share/deep-link UX; not a launch blocker). No Coolify / wasm / indexer.
PlasticDigits commented 2026-08-31 16:08:49 +00:00 (Migrated from gitlab.com)

marked as related to #711

marked as related to #711
PlasticDigits commented 2026-08-31 16:08:51 +00:00 (Migrated from gitlab.com)

marked as related to #713

marked as related to #713
PlasticDigits commented 2026-08-31 16:08:52 +00:00 (Migrated from gitlab.com)

marked as related to #665

marked as related to #665
PlasticDigits commented 2026-08-31 16:08:53 +00:00 (Migrated from gitlab.com)

marked as related to #542

marked as related to #542
PlasticDigits commented 2026-08-31 16:08:54 +00:00 (Migrated from gitlab.com)

marked as related to #630

marked as related to #630
PlasticDigits commented 2026-09-01 07:32:29 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1201

mentioned in merge request !1201
PlasticDigits commented 2026-09-01 07:46:46 +00:00 (Migrated from gitlab.com)

mentioned in commit 53237a5034

mentioned in commit 53237a5034cffbc9b09494b2e850b2a466572e15
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-09-01 07:46:47 +00:00
PlasticDigits commented 2026-09-01 07:48:09 +00:00 (Migrated from gitlab.com)

mentioned in issue #716

mentioned in issue #716
PlasticDigits commented 2026-09-01 07:48:09 +00:00 (Migrated from gitlab.com)

marked as related to #716

marked as related to #716
PlasticDigits commented 2026-09-01 07:48:22 +00:00 (Migrated from gitlab.com)

Merge of !1201 (no CI wait)

Merged immediately (cancelled glab MWPS so we did not wait on the running pipeline). Merge commit 53237a50 on main. Source branch removed.

Conflicts: none. feat/715-swap-tokenlist-symbols was already based on main (cbd37cd8); merge-tree had no conflict hunks.

Sanity / AC (local, this machine):

  • make verify-issue-715 → PASS=8 FAIL=0 (Playwright SKIP — no LocalTerra / .env.local)
  • make verify-issue-711 → PASS
  • make verify-issue-713 → PASS (Playwright SKIP same reason)
  • Chrome nesting guard passed. Unique-symbol CI + fail-closed fixtures passed.

Fix included before merge: #713 e2e still asserted Share aria-label /share swap/i. After SH-1 the label is Share LUNC to USTC swap link. Updated in 86744db1 on the MR branch.

Leftovers (do not reopen this issue): #716 — Coolify frontend rebuild, LocalTerra Playwright 5 workers (e2e/swap-tokenlist-symbols-715.spec.ts + #713/#711 smokes), Trader Share still text-only, unused SHARE_LINK_ARIA_SWAP. Sibling #714 Playwright must now expect symbols + the new aria-label.

No wasm / indexer / Coolify env.

## Merge of !1201 (no CI wait) Merged immediately (cancelled `glab` MWPS so we did not wait on the running pipeline). Merge commit `53237a50` on `main`. Source branch removed. **Conflicts:** none. `feat/715-swap-tokenlist-symbols` was already based on `main` (`cbd37cd8`); merge-tree had no conflict hunks. **Sanity / AC (local, this machine):** - `make verify-issue-715` → PASS=8 FAIL=0 (Playwright SKIP — no LocalTerra / `.env.local`) - `make verify-issue-711` → PASS - `make verify-issue-713` → PASS (Playwright SKIP same reason) - Chrome nesting guard passed. Unique-symbol CI + fail-closed fixtures passed. **Fix included before merge:** `#713` e2e still asserted Share `aria-label` `/share swap/i`. After SH-1 the label is `Share LUNC to USTC swap link`. Updated in `86744db1` on the MR branch. **Leftovers (do not reopen this issue):** [#716](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/716) — Coolify frontend rebuild, LocalTerra Playwright 5 workers (`e2e/swap-tokenlist-symbols-715.spec.ts` + #713/#711 smokes), Trader Share still text-only, unused `SHARE_LINK_ARIA_SWAP`. Sibling [#714](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/714) Playwright must now expect symbols + the new aria-label. No wasm / indexer / Coolify env.
PlasticDigits commented 2026-09-01 07:48:24 +00:00 (Migrated from gitlab.com)

mentioned in issue #714

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