feat(frontend): Swap from=/to= tokenlist symbols, unique-symbol CI, Share logos #715
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#715
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
Follow-up to #711 / #713. One product surface: human swap links.
Bundle (do not split):
tokenlist/tokenlist.jsonmust reject duplicate tickers (case-insensitive:UST1andust1cannot both exist).from=/to=accept tokenlist symbols —/?from=UST1&to=USTRin addition to native denoms (uluna/uusd) and checksummedterra1addresses.from=UST1(published tokenlist casing) when a unique symbol exists; otherwise keep denom / bech32.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
resolveSwapQueryTokenValuealready maps Q711-5 product tickers vialookupTokenIdByProductTicker:LUNC/uluna→uluna,USTC/UST/uusd→uusd, and hub CW20s (CL8Y,UST1,cLUNC,cUSTC,vFDUSD,USTR,ALPHA,USTRIX,SpaceUSD) throughCW20_MAP. Checksummedterra1still works. Hostile /0x/ETH/ibc//factory// gems-as-tickers (RUBY) fail closed.That map is hand-maintained in
tokenRegistry.ts. Swap never readstokenlist/tokenlist.json. Create Pair does (createPairTokenCatalog.ts, C542-11). A token added only to the published list is selectable on/createbut/?from=NEWSYMis ignored until someone copies it intoCW20_MAP. Registry vs tokenlist address drift is already asserted increatePairTokenCatalog.test.ts; there is no symbol-uniqueness check.Inbound still works for
from=UST1today 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
canonicalSwapSearchsetsfrom/totopayId/receiveId(execute ids:uluna/uusd/ checksummed bech32). Comment onswapDeepLinkPath: “Bech32 / native denoms only — not display tickers.”Callers that inherit this:
canonicalSwapSearch/ SwapPagesetSearchParamsrewrite (Q713-1)/?from=uluna&to=terra1f0eq…swapDeepLinkPathbuildCanonicalSwapShareUrlURLSearchParamsfrom resolved ids)ust1SecondarySwapPathfrom=<ust1 bech32>&to=<quote bech32>parseSwapQueryParamsis shared withtradeQueryResolve.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=uusdas 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.mdrequiressymbolandSYMBOL.pngfilenames. It does not say symbols must be unique case-insensitively. Current list includes mixed-caseSpaceUSDnext to uppercaseUST1/CL8Y. Two rowsust1andUST1would both parse, Create Pair would show two labels that collide in a ticker lookup, andfrom=UST1would be ambiguous.buildCreatePairCw20Optionsdrops duplicate addresses (lowercase), not duplicate symbols.scripts/qa/keplr_cw20_registry_validate.pyreads the tokenlist for Keplr pack parity, not ticker uniqueness. Nomaketarget fails a collidingsymbol.Share button is still the word “Share”
SwapPage.tsxmountsShareLinkButtonnext to Settings (data-testid="swap-share-link"). Visible label isSHARE_LINK_BUTTON_LABEL= Share.buttonLabelis astring. Logos are not passed.Web Share / clipboard
textisswapShareText:CL8Y DEX swap UST1 → USTR(display symbols, not logos).aria-labelis the generic Share swap link. Trader / Portfolio Share must stay text-only (TS-1–TS-13).TokenLogoalready renders allowlistedlogoURI(or blockie fallback).resolveAllowedTokenLogoUri(#378) must stay the only<img src>path.Why the new implementation is needed
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.tokenlist.jsonshould makefrom=SYMBOLwork without a second hand-edit intokenRegistry.ts.UST1andust1pointing at different contracts,from=UST1is an attacker-chosen contract. Uniqueness is a CI gate, not a runtime “pick the first row” heuristic.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
symbol. Fold with Unicode-aware ASCII case fold (UST1≡ust1≡Ust1). Empty / whitespace-only symbols are invalid.denomlowercase; CW20addresslowercase). Two rows must not share an address even if symbols differ.fetch(tokenlist.json)in the dApp.token_info.symbolas the query key (X1): a gem or unlisted CW20 spoofingsymbol=UST1must not win over the tokenlist row. Factory-listed but unpublished tokens share as bech32, never as a colliding ticker.tokenlist.json(#562 / U6). Uniqueness check must not be used to sneakRUBYinto Swap links.Query parse / rewrite / Share URL
terra1,uluna/uusd, Uniswap aliases (Q711), and tokenlist symbols (case-insensitive).UST→uusdstays an inbound-only alias (not a tokenlist symbol).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 checksummedterra1. Elsenull.applySwapQueryParams, Q711-2 / Q711-4). Symbol resolve is not a license to inject an unlisted CW20 into the picker.from/to/exactAmount/exactFieldviaURL+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.LUNC/USTC, notuluna/uusd. Inboundfrom=ulunastill applies, then rewrite tofrom=LUNC.?showGems=1(Q713-8).window.location.href. Origin iswindow.location.origin. NoVITE_PUBLIC_ORIGIN. Drop leftoverrecipient/ WC / Uniswap keys.exactFieldencoding unchanged.from=UST1is AMM Swap, not/ust1mint/redeem.localStorage. Do not map EVM0xto Terra. Do not change nginx. Do not mount a second Swap at/swap.Share chrome
TokenLogo+ → + receiveTokenLogo. Updates whenfromToken/toTokenchange (picker, flip, inbound apply).TokenLogo+resolveAllowedTokenLogoUrionly.alt=""(decorative). Failed / unknown URI → blockie from address/denom seed, not a remote fetch of attackerlogoURI.aria-labelmust be text (e.g. Share LUNC to UST1 swap link), not “Share image to image.” Visible word Share stays for #489.ShareLinkButtonstays the word Share — do not put pair logos there.shell-panel*/card-glass(C653). Coarse+narrow: Web Share; else copy Link copied.AbortErrorsilent. 44px hit target, focus ring,sounds.playButtonPress.title/textstay strings (symbols OK inswapShareText). Do not put HTML/SVG in the share sheet.Relevant files
tokenlist/tokenlist.jsonsymbol/ address / denomtokenlist/README.mdfrontend-dapp/src/utils/createPairTokenCatalog.tsfrontend-dapp/src/utils/swapQueryParams.tscanonicalSwapSearch,swapDeepLinkPathfrontend-dapp/src/utils/tokenRegistry.tsfrontend-dapp/src/utils/sharePageLink.tsbuildCanonicalSwapShareUrl,swapShareTextfrontend-dapp/src/utils/sharePageLinkCopy.tsfrontend-dapp/src/pages/SwapPage.tsxfrontend-dapp/src/components/ui/ShareLinkButton.tsxfrontend-dapp/src/components/ui/TokenLogo.tsxfrontend-dapp/src/utils/tokenLogoAllowlist.tsfrontend-dapp/src/utils/tokenDisplay.tsfrontend-dapp/src/utils/ust1SecondaryMarket.tsfrom=UST1&to=vFDUSDfrontend-dapp/src/utils/tradeQueryResolve.ts/trade/{pair}docs/frontend.mdskills/AGENTS_FRONTEND_SWAP_QUERY_PARAMS.mdskills/AGENTS_FRONTEND_SWAP_URL_SYNC.mdskills/AGENTS_FRONTEND_SHARE_LINK.mdscripts/qa/+Makefileverify-issue-*+ uniqueness checkRecommended direction
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 intokenlist/README.md. Optional: Vitest that reads the same JSON so frontend tests fail in the same PR.symbol → execute idandexecute id → published symbol. Prefer this over growingCW20_MAPby hand. KeepUST→uusdas an extra inbound alias not in the JSON.canonicalSwapSearch(and therefore rewrite, Share URL,swapDeepLinkPath) encode withexecuteIdToQueryToken(id): tokenlist symbol if unique mapping exists, elseuluna/uusd/terra1. InboundresolveSwapQueryTokenValueremains the inverse.swapSearchEqualsstill comparesURLSearchParamsstrings — after this change the canonical string for LUNC/USTC isfrom=LUNC&to=USTC.ust1SecondarySwapPath, any marketing/docs examples in-repo) switch to symbols. Old bech32 /ulunalinks must keep working.ShareLinkButtonwith an optionalbuttonContent?: ReactNode(or a thinSwapShareButtonwrapper) so Swap can renderShare+ twoTokenLogos + a literal→. Default remains the string Share. Drive logos from the same registry/tokenlistlogoURIas the pickers. Updatearia-labelfrom live symbols.Do not use LCD
token_info.symbolfor query encoding. Do not uppercase-normalize outbound (cLUNCmust not becomeCLUNCif that is a different tokenlist row — uniqueness already forbids that pair).Acceptance criteria
maketarget fails iftokenlist.jsonhas two tokens whosesymbolmatches case-insensitively (fixture:UST1+ust1), or duplicate address/denom. Current published list passes.from/to/ Share use those symbols./?from=UST1&to=USTR(andfrom=ust1, mixed case) selects those factory tokens after apply. Same forLUNC/USTC/cLUNC/vFDUSD/SpaceUSDand every other current tokenlist row that is ingetAllTokens.terra1anduluna/uusdstill apply. After apply, the bar rewrites to the tokenlist symbol when one exists (from=uluna&to=<ust1 bech32>→from=LUNC&to=UST1).text, orhref.?showGems=1still inert./swap?from=UST1&to=cUSTCpreserves search on redirect to/./trade?from=UST1&to=cUSTCstillreplaces to/trade/{uniquePair}when unique.swapDeepLinkPath/ust1SecondarySwapPath/ Share URL use symbols when available. Amount /exactField=outputencoding unchanged. Execute still offer-in.data-testid="swap-share-link"). Logos update on picker change and flip.aria-labelincludes both display symbols.shell-panel*/card-glass. Desktop copy / mobile Web Share behavior unchanged (Q713-5).{origin}/?from=<symbol-or-id>&to=<symbol-or-id>(+ amount/exactField), neverlocation.href, never a gem.docs/frontend.md, swap query + URL-sync + share skills,docs/testing.md,AGENTS.md,make verify-issue-{iid}plus regressionsmake verify-issue-711andmake verify-issue-713.Test plan (all paths)
Tokenlist / CI
tokenlist.jsonuniqueness passes.UST1+ust1(different addresses) fails the checker.ulunanative rows fails.SpaceUSDvsSPACEUSDfails; current singleSpaceUSDrow passes.Inbound parse (Vitest
swapQueryParams/tokenRegistry/ catalog)from=LUNC&to=USTC,from=uluna&to=uusd,from=UST&to=CL8Y.from=<UST1 bech32>&to=vFDUSD.?to=UST1→ receive UST1, pay the other retail default.from=UST1&to=ust1) → keep pay, default the other.inputCurrency=UST1&outputCurrency=USTRstill applies, then rewrite tofrom=UST1&to=USTR.exactField=outputstill parse; reverse-quote rules unchanged.UST1resolves toVITE_UST1_TOKEN_ADDRESSwhen set; outbound stillfrom=UST1.Factory / gem / hostile (do not regress #711)
terra1ignored.RUBYignored in production;showGems=1ignored.javascript:,data:, URLs,0x,ETH,ibc/,factory/, overlong,<>"',%3Cscript%3Eignored and not echoed.UST1does not satisfyfrom=UST1(X1).Rewrite / Share URL
searchParamsequalscanonicalSwapSearchwith symbols.replace, no history spam).navigator.shareURL matches the canonical bar (plus origin), not leftover aliases.ust1SecondarySwapPath()is/?from=UST1&to=vFDUSD(orcUSTCquote leg) on mainnet ids.Swap page RTL
/?from=UST1&to=USTRselects those tokens (extendSwapPage.queryParams.test.tsx).swap-share-link.aria-label.ShareLinkButton
type=button, focus, copy/share/abort paths.Trade / Create Pair
/trade?from=UST1&to=cUSTCunique pair →/trade/{addr}(extendtradeQueryResolvetests). Ambiguous / missing still ignore./createcatalog unchanged except it cannot ship a colliding tokenlist (CI). No Swap factory graph leak (C542-8).Playwright (5 workers; no
e2e-txrequired)/?from=UST1&to=USTR(and/swap?from=LUNC&to=USTC) selects labels LUNC/USTC/UST1 as applicable; URL ends canonical symbols.from=still ignored.from=/to=symbols; button shows two logos that change after flip.Docs / verify
make verify-issue-{iid}make verify-issue-711andmake 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.pyTest plan (attack, hack, and abuse)
UST1vsust1vsUѕt1(Cyrillic) cannot ship. Non-ASCII symbols: reject or treat as distinct and never match ASCIIUST1. Prefer reject non-ASCII tickers in the uniqueness script if not already in the list.from=UST1to a hostile CW20token_info.symbolis not consulted for query ids.from=Navigate/ Share useURL+URLSearchParamsonly. Logos never use query string assrc.logoURIjavascript/data/httpresolveAllowedTokenLogoUridrops; blockie fallback. Share button must not setsrcfrom query or from unchecked JSON beyond the allowlist.SWAP_QUERY_VALUE_MAX_LEN; last non-empty within family; no ReDoS on symbol fold.showGems=1CL8Yfrom=CL8Y.location.href(no WC URI,recipient, Uniswap leftovers).UST1; execute id is overlay. Do not write the overlay bech32 into the public share URL when the symbol is unique.Verification criteria
Done when:
make verify-issue-{iid}is green on a clean tree.make verify-issue-711andmake verify-issue-713are green with updated canonical expectations (from=LUNC&to=USTC,from=UST1, …) and all hostile/gem/factory cases still fail closed.ust1duplicate fails.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 usesfrom=UST1&to=USTR; flip updates logos and URL.#713bech32/ulunalink → pair still applies → bar/Share rewrite to symbols.Issue type: frontend feature. Priority: P2 (share/deep-link UX; not a launch blocker). No Coolify / wasm / indexer.
marked as related to #711
marked as related to #713
marked as related to #665
marked as related to #542
marked as related to #630
mentioned in merge request !1201
mentioned in commit
53237a5034mentioned in issue #716
marked as related to #716
Merge of !1201 (no CI wait)
Merged immediately (cancelled
glabMWPS so we did not wait on the running pipeline). Merge commit53237a50onmain. Source branch removed.Conflicts: none.
feat/715-swap-tokenlist-symbolswas already based onmain(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→ PASSmake verify-issue-713→ PASS (Playwright SKIP same reason)Fix included before merge:
#713e2e still asserted Sharearia-label/share swap/i. After SH-1 the label isShare LUNC to USTC swap link. Updated in86744db1on 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, unusedSHARE_LINK_ARIA_SWAP. Sibling #714 Playwright must now expect symbols + the new aria-label.No wasm / indexer / Coolify env.
mentioned in issue #714