feat(frontend): Swap URL sync, reverse quotes, share control, Create/Trade query prefill #713

Closed
opened 2026-08-31 05:36:02 +00:00 by PlasticDigits · 12 comments
PlasticDigits commented 2026-08-31 05:36:02 +00:00 (Migrated from gitlab.com)

Summary

Follow-up to #711 / !1199. One product surface: shareable DEX URLs.

Bundle (do not split):

  1. Reverse quotes — Uniswap exactField=output (You Receive is the independent amount).
  2. URL rewrite — keep /?from=&to= in sync with picker, flip, and amount (replace, not history spam).
  3. Swap Share — unintrusive control: Web Share on mobile, copy link on desktop.
  4. Create Pair + Trade query prefill — /create?a=&b= (explicit exception to C542-11’s no-prefill sentence) and /trade?from=&to= resolving to canonical /trade/:pairAddr.

Land as a new MR on top of !1199. Do not expand !1199 after review has started. #711 inbound parser, factory gate, gem hide, and hostile ignore stay.

Parent inbound work: #711. Related: #542, #665, #678, #489, #653, #182, #481, #562, #630, #693, #190 (hybrid_reverse_simulation). Marketing-site DEX hrefs stay gated on !1199 + Coolify (CL8Y-web #6); this issue makes those links stay true after the user changes tokens.

Current codebase

Swap reads query once and never writes it back

SwapPage.tsx uses useSearchParams + appliedSwapQueryKeyRef: apply once per search string after getAllTokens(pairs) (Q711-7). Picker onChange, flip, and inputAmount do not call setSearchParams. swapDeepLinkPath already mints /?from=&to= (+ optional exactAmount) with URLSearchParams, but nothing on the page uses it after landing.

Copying the address bar after changing tokens still shares the inbound pair (or a bare /). Aggregator aliases (inputCurrency, …) stay in the bar forever.

/ is Swap. /swap redirects with search preserved (SwapAliasRedirect.tsx, Q711-1). Layout Outlet is key={pathname} — search-only replace does not remount Swap. Good for rewrite; the apply/write loop is the risk.

Amount is pay-sided only; You Receive is not an input

Parser amount keys: exactAmount / amount / value / amountIn (Q711-6). There is no exactField. You Pay is an <input>; You Receive is quote chrome. Quotes go through offer-in quoteCw20ViaRouteSolve (GET /route/solve + simRaw). Direct-pair reverse simulation already exists: reverseSimulateSwap → hybrid_reverse_simulation (#190). It is unused on Swap. Router / indexer solve has no exact-out amount.

Execute is always offer-in + min received / hop floors. That must stay.

No Share on Swap

ShareLinkButton + sharePageLink.ts (TS-1–TS-13, #665) cover /trader/:address (and optional trade/charts path URLs with search stripped). TS-2 forbids sharing window.location.href and strips query — correct for trader profiles, wrong for Swap (the query is the payload). SHARE_PAGE_KINDS is trader | trade | charts only. Swap header is H2 Swap + Settings inside one shell-panel-strong — no share control.

Create Pair voids query on purpose

CreatePairPage.tsx initializes tokenA / tokenB to ''. No useSearchParams. RTL P402-5 / C542-11 asserts /create?a=&b= does not prefill (CreatePairPage.test.tsx). Catalog is bundled tokenlist.json + env overlays — not Swap’s factory universe (C542-8). Natives are forbidden (C542-2). Submit still requires checksum (C542-3), distinct legs, and code-id whitelist (C542-6). Create Token → /create was copy-address only so a query string could not phish an admin into creating a hostile pair without looking.

Trade identity is the pair path

App.tsx: /trade and /trade/:pairAddr. tradePairRoute.ts validates the segment and auto-picks a default factory pair when there is no deep link. Ticket is Market-default (#693). No from/to query. Sharing a pair is optional in #665 as /trade/{pairAddr} with no search.

Why the new implementation is needed

  1. Share is a lie today. After #711 a link can open the right pair, but the next picker change desyncs the URL. Mobile users have no control to share the live Swap. Desktop users copy a stale bar or a bare origin.
  2. Reverse quote is how Uniswap/Pancake links work. exactField=output + exactAmount means “I want this many of the receive token.” Ignoring it looks like the dApp dropped half the link. Direct pairs already have hybrid_reverse_simulation.
  3. Create Pair / Trade are the other share surfaces. Operators paste two 64-char addresses; a checksummed /create?a=&b= from Create Token or docs is the same job Swap just grew. Trade already has a canonical path — inbound ?from=&to= should resolve to that path so token-page CTAs do not need to know the pair contract.
  4. One parser family. Splitting rewrite vs reverse vs share vs Create/Trade produces apply/write loops and duplicate hostile-query code. Bundle them.

This is frontend routing + quote field + one Share control. No contract migrate, no indexer exact-out solver, no Coolify env.

Constraints / guardrails

  • Factory / catalog gates stay. Swap rewrite and share emit only ids in getAllTokens(pairs) after gem hide. Create Pair still uses create-pair catalog + checksum paste, never Swap’s factory graph (C542-8). Trade resolve only matches factory-listed pairs.
  • Checksum (#382). terra1… must pass isValidTerraBech32Address. Hostile / overlong / javascript: / data: / http(s): / // / 0x / ibc/ / factory/ / ETH → ignore that side. Never echo raw query into combobox, share text, or href.
  • Gems (#562). Production does not apply or write gem ids. Never honor ?showGems=1. Do not share a gem pair on mainnet builds.
  • Canonical outbound keys remain first-party from / to / exactAmount / exactField via URLSearchParams (extend swapDeepLinkPath). Uniswap names stay inbound-only. After rewrite, drop leftover inbound aliases from the bar.
  • replace, never push, for picker/amount/flip. Debounce amount (reuse swap quote debounce, ~300ms). No-op when the canonical query string is unchanged (breaks apply ↔ write loops). Do not remount (pathname key).
  • Reverse quote execute path. UI may be exact-out; broadcast is still offer-in + min received. Do not skip #678 quote-only / insufficient / 5% confirm-again / 30% Expert / 99% / blacklist / pause / freeze. Do not honor slippage / expertMode / recipient / pool_only / hybrid_optimize from the URL (#596).
  • Reverse quote routing. Use reverseSimulateSwap only when the live route is a single factory pair (direct hybrid / wrap). GET /route/solve is offer-in only — no binary-search exact-out, no indexer API change in this issue. If the best route is ≥2 hops, ignore exactField=output (silent, #489) and treat amount as pay-sided.
  • Share payload. Never share window.location.href (WC / leftover hostile keys). Build {origin}/?from=&to= (+ amount / exactField) from resolved ids. Origin is window.location.origin (TS-9). No VITE_PUBLIC_ORIGIN. No P&L. Static title CL8Y DEX swap + optional resolved symbols (not raw query).
  • Share UX. Header row next to Settings — no new shell-panel* / card-glass (C653). Label Share (or icon + Share). Coarse pointer / narrow viewport + navigator.share: Web Share. Desktop / share missing / canShare === false / non-abort error: copyToClipboard (Link copied). AbortError = cancel, no toast. 44px hit target, aria-label, focus ring, sounds.playButtonPress.
  • Create Pair exception. This issue replaces the “No /create?a=&b=” sentence of C542-11 only. Keep: no HTTP catalog fetch, no natives, listed ≠ skip whitelist, U1 notice, no auto-submit. One-sided query OK. Same token → ignore second side. Custom terra1 that checksums may prefill Custom contract (still #382 + code-id check). Do not auto-click Create.
  • P402-5. Create Token may start emitting /create?a=<newCw20> after this ships; until then copy-address remains valid. Prefill is not a substitute for the admin reading the picker.
  • Trade canonical path. /trade/:pairAddr stays the share/identity URL. /trade?from=&to= (aliases OK inbound) → unique factory pair whose legs match (order independent) → replace to /trade/{pair}. Zero or multiple matches → ignore query, existing auto-pick. Optional amount + side=buy|sell prefills the ticket only; never Place. Do not invent a query-only Trade page.
  • #489. No lecture banner “we ignored exactField / create query.” Silent fail-closed.
  • XSS / open redirect. No string concat into Navigate to. Share url is URL + URLSearchParams. No dangerouslySetInnerHTML.
  • Do not persist query in localStorage. Do not map EVM 0x to Terra. Do not change nginx. U1: Swap/Trade UST1 links remain AMM, not mint/redeem.

Relevant files

Path Why
frontend-dapp/src/utils/swapQueryParams.ts Parse exactField; canonicalize write; swapDeepLinkPath
frontend-dapp/src/pages/SwapPage.tsx Apply + replace search; reverse-quote field; Share in header
frontend-dapp/src/services/terraclassic/pair.ts reverseSimulateSwap (direct only)
frontend-dapp/src/utils/cw20RouteSolveQuote.ts Offer-in only — do not fake exact-out
frontend-dapp/src/utils/sharePageLink.ts + ShareLinkButton.tsx Extend kinds / swap URL builder; reuse button
frontend-dapp/src/utils/sharePageLinkCopy.ts Swap title / aria / copied string
frontend-dapp/src/pages/CreatePairPage.tsx Apply a/b; optional rewrite
frontend-dapp/src/utils/createPairTokenCatalog.ts Gate prefill to catalog (or checksum paste)
frontend-dapp/src/pages/CreatePairPage.test.tsx Replace C542-11 “never prefill” with hostile-ignore + happy path
frontend-dapp/src/pages/TradePage.tsx + tradePairRoute.ts Resolve from/to → pair path; optional ticket prefill
frontend-dapp/src/App.tsx No second Swap mount; /trade already exists
frontend-dapp/src/components/common/SwapAliasRedirect.tsx Unchanged preserve search
docs/frontend.md New invariants; amend Q711-6/7, C542-11 sentence
skills/AGENTS_FRONTEND_SWAP_QUERY_PARAMS.md Rewrite, reverse, share
skills/AGENTS_FRONTEND_SHARE_LINK.md Swap exception to TS-2
skills/AGENTS_FRONTEND_CREATE_PAIR_PICKER.md Query prefill + remaining C542
scripts/qa/verify-issue-711.sh Keep green; add verify-issue-* for this ticket
  1. Canonical search helper (pure, Vitest-first): canonicalSwapSearch({ payId, receiveId, amountHuman, exactField }) → URLSearchParams with from/to, exactAmount only if legal, exactField=output only when output. Empty amount omitted. Call from Swap replace and from share URL builder (origin + '/' + search, hash empty).
  2. Write-back on Swap. After apply-from-URL, treat subsequent picker/flip/amount as source of truth: setSearchParams(canonical, { replace: true }) when canonical.toString() !== currentCanonical. Amount debounce. Incoming apply must set the ref to the canonical key so the first rewrite does not re-apply as a “new” search.
  3. Reverse quote UI. exactField=output (and inbound aliases independentField=output if cheap) makes You Receive the driven <input> and You Pay the quoted offer from reverseSimulateSwap when the route is 1-hop. Flip tokens keeps exactField. Execute: take reverse offer_amount, run existing forward quote + min-receive path. Multihop: ignore output field (pay-sided).
  4. Share control on the Swap header row (flex next to Settings). Reuse ShareLinkButton. Mobile/coarse: navigator.share. Desktop: copy canonical URL. data-testid="swap-share-link".
  5. Create Pair. parseCreatePairQuery — keys a/tokenA/token_a and b/tokenB/token_b. Checksum + (catalog hit or valid terra1 for custom). Apply once; optional rewrite to ?a=&b= with bech32 only. Update C542-11 docs/tests.
  6. Trade. On /trade (no :pairAddr) or /trade/ , parse Swap-style from/to; find unique factory pair with both legs; navigate(/trade/{pair}, { replace: true }). Ticket: optional amount / side after pair is known; Market default unchanged. Pair share stays path-only (#665) unless you also append amount (not required).

Acceptance criteria

  • AC1. Changing Swap pay/receive/flip/amount updates the URL to canonical /?from=&to= (exactAmount / exactField when set) via history.replaceState / setSearchParams({ replace: true }). Back button is not spammed per keystroke.
  • AC2. Inbound Uniswap aliases still apply (#711), then rewrite to canonical keys (aliases disappear from the bar).
  • AC3. User picker after a deep link is not snapped back to the original query (write follows state; apply does not fight it).
  • AC4. exactField=output + legal amount on a direct pair: You Receive shows that amount; You Pay shows reverse-sim offer; quote/execute still offer-in + min received; #678 / 5–30–99 unchanged. Multihop: output field ignored (pay-sided).
  • AC5. Swap header Share: mobile/coarse uses Web Share with canonical URL; desktop copies Link copied. Abort is silent. Payload has no wallet, WC, slippage, or raw hostile query.
  • AC6. /create?a=<listedCw20>&b=<listedCw20> prefills Token A/B. Hostile / native / bad checksum ignored per side. Submit still disabled until whitelist + distinct legs. No auto-submit.
  • AC7. /trade?from=uluna&to=<listed> (or aliases) replaces to /trade/{uniquePair}. Ambiguous/missing pair: ignore. /trade/:pairAddr unchanged as canonical. Ticket not auto-placed.
  • AC8. Production cannot share or rewrite a gem. ?showGems=1 still ignored.
  • AC9. Docs + skills: Q711-6/7 updated; C542-11 prefill sentence replaced; Share TS exception for Swap search; make verify-issue-711 still passes.
  • AC10. No new chrome layer; no lecture banners; Create Pair catalog still has no runtime HTTP.

Test plan (functional paths)

# Path Expect
T1 / no query, pick tokens URL becomes /?from=&to= (ids, not tickers)
T2 /?inputCurrency=uluna&outputCurrency=<listed> Pair applies, bar rewrites to from/to
T3 Change receive after T2 URL follows new receive; no snap-back
T4 Type amount 1.5 (debounced) exactAmount=1.5 appears; rapid typing = one replace
T5 Clear amount exactAmount omitted
T6 Flip from/to swap; exactField preserved if set
T7 Direct pair exactField=output&exactAmount=1 Receive 1; pay from reverse sim
T8 Multihop + exactField=output Amount treated pay-sided; no crash
T9 Share on desktop Clipboard = canonical https?://{origin}/?from=&to=
T10 Share on coarse + canShare navigator.share with same URL; abort → no copy error
T11 /create?a=&b= listed CW20s Both pickers set; Create still gated
T12 /create?a=javascript:alert(1) Ignored; empty A
T13 /create?a=uluna Ignored (native)
T14 /trade?from=uluna&to=<listed> unique pair Lands on /trade/{pair}
T15 /trade?from=uluna&to=uluna Ignore; default pair pick
T16 Existing /swap E2E + #711 RTL Still green
T17 Wrap pair reverse quote Still valid wrap UI; execute unchanged
T18 Hash #/ Ignored; search only

Vitest: table-driven canonical search + exactField parse; RTL Swap rewrite + share; CreatePair happy + hostile (replace old C542-11 never-prefill); Trade resolve helper. Playwright: share button present; URL updates on token change (PLAYWRIGHT_SKIP_CHAIN=1 OK). 5 workers.

Test plan (attack, hack, and abuse)

# Vector Expect
A1 Rewrite concatenating from + user string Never; only URLSearchParams.set
A2 Share window.location.href with ?recipient= leftover Share builder uses canonical keys only
A3 exactField=output huge amount Cap 24 chars / isPositiveDecimalAmount; else ignore field
A4 Reverse quote as execute bypass Still offer-in; slippage / Expert / blacklist still block
A5 /create?a=<attackerCw20> unlisted Prefill custom only if checksum valid; whitelist warning; no auto-submit
A6 /create?a=<gem> production Ignore (retail hide / not in catalog)
A7 /trade?from= open redirect https://phish Parser returns no URL; navigate only to /trade/{bech32}
A8 Multiple factory pairs same legs Do not pick at random; ignore
A9 History flood from amount input Debounce + replace; not push
A10 Apply/write infinite loop Canonical compare; RTL / no act-spam
A11 Share title with raw to=<script> Title static; symbols from registry only
A12 exactField=output on paused/frozen listed token Existing banners; no execute
A13 Clickwrap / WC URI in search Strip; share/rewrite never copies WC
A14 Desktop accidentally opening OS share sheet Copy fallback when not coarse / canShare false
A15 C542-8 leak: Create prefill from Swap factory id not in catalog Ignore unless checksum paste path

Verification criteria

  • New make verify-issue-<id> (parser + RTL + CreatePair + Trade helper + docs/skills greps). make verify-issue-711, make verify-issue-542, make verify-issue-665, make verify-issue-678, python3 scripts/check_chrome_nesting.py stay green.
  • Playwright: token change rewrites URL; Share copies canonical search; /create?a=&b= prefills listed CW20s; /trade?from=&to= resolves unique pair.
  • Manual: make dev → open /swap?from=uluna&to=<listed> → change token → Share/copy → paste in new tab → same pair. Reverse quote on a direct pair. Hostile create query does nothing. Trade query lands on pair workspace, not Market submit.
  • Coolify after merge: share from dex.cl8y.com produces a link that survives a reload; Create/Trade prefills do not auto-tx.

Out of scope

  • Indexer exact-out /route/solve.
  • Persisting Swap state in localStorage.
  • Per-route Open Graph (#578).
  • QR library / third-party share SDK.
  • Mapping BSC 0x CL8Y into Terra Swap.
  • cl8y-web hrefs (already CL8Y-web #6, gated on !1199).
  • Expanding !1199 itself.
## Summary Follow-up to [#711](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/711) / [!1199](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/merge_requests/481). One product surface: **shareable DEX URLs**. Bundle (do not split): 1. **Reverse quotes** — Uniswap `exactField=output` (You Receive is the independent amount). 2. **URL rewrite** — keep `/?from=&to=` in sync with picker, flip, and amount (replace, not history spam). 3. **Swap Share** — unintrusive control: **Web Share on mobile**, **copy link on desktop**. 4. **Create Pair + Trade query prefill** — `/create?a=&b=` (explicit exception to **C542-11**’s no-prefill sentence) and `/trade?from=&to=` resolving to canonical `/trade/:pairAddr`. Land as a **new MR on top of !1199**. Do not expand !1199 after review has started. #711 inbound parser, factory gate, gem hide, and hostile ignore stay. Parent inbound work: #711. Related: #542, #665, #678, #489, #653, #182, #481, #562, #630, #693, #190 (`hybrid_reverse_simulation`). Marketing-site DEX hrefs stay gated on !1199 + Coolify ([CL8Y-web #6](https://gitlab.com/PlasticDigits/CL8Y-web/-/work_items/6)); this issue makes those links stay true after the user changes tokens. ## Current codebase ### Swap reads query once and never writes it back [`SwapPage.tsx`](frontend-dapp/src/pages/SwapPage.tsx) uses `useSearchParams` + `appliedSwapQueryKeyRef`: apply **once per search string** after `getAllTokens(pairs)` (**Q711-7**). Picker `onChange`, flip, and `inputAmount` do **not** call `setSearchParams`. [`swapDeepLinkPath`](frontend-dapp/src/utils/swapQueryParams.ts) already mints `/?from=&to=` (+ optional `exactAmount`) with `URLSearchParams`, but nothing on the page uses it after landing. Copying the address bar after changing tokens still shares the **inbound** pair (or a bare `/`). Aggregator aliases (`inputCurrency`, …) stay in the bar forever. `/` is Swap. `/swap` redirects with search preserved ([`SwapAliasRedirect.tsx`](frontend-dapp/src/components/common/SwapAliasRedirect.tsx), **Q711-1**). Layout Outlet is `key={pathname}` — search-only `replace` does **not** remount Swap. Good for rewrite; the apply/write loop is the risk. ### Amount is pay-sided only; You Receive is not an input Parser amount keys: `exactAmount` / `amount` / `value` / `amountIn` (**Q711-6**). There is **no** `exactField`. You Pay is an `<input>`; You Receive is quote chrome. Quotes go through offer-in [`quoteCw20ViaRouteSolve`](frontend-dapp/src/utils/cw20RouteSolveQuote.ts) (`GET /route/solve` + `simRaw`). Direct-pair **reverse** simulation already exists: [`reverseSimulateSwap`](frontend-dapp/src/services/terraclassic/pair.ts) → `hybrid_reverse_simulation` (#190). It is unused on Swap. Router / indexer solve has **no** exact-out amount. Execute is always offer-in + min received / hop floors. That must stay. ### No Share on Swap [`ShareLinkButton`](frontend-dapp/src/components/ui/ShareLinkButton.tsx) + [`sharePageLink.ts`](frontend-dapp/src/utils/sharePageLink.ts) (**TS-1–TS-13**, #665) cover `/trader/:address` (and optional trade/charts **path** URLs with search stripped). **TS-2** forbids sharing `window.location.href` and strips query — correct for trader profiles, wrong for Swap (the query **is** the payload). `SHARE_PAGE_KINDS` is `trader | trade | charts` only. Swap header is H2 **Swap** + **Settings** inside one `shell-panel-strong` — no share control. ### Create Pair voids query on purpose [`CreatePairPage.tsx`](frontend-dapp/src/pages/CreatePairPage.tsx) initializes `tokenA` / `tokenB` to `''`. No `useSearchParams`. RTL **P402-5 / C542-11** asserts `/create?a=&b=` does **not** prefill ([`CreatePairPage.test.tsx`](frontend-dapp/src/pages/CreatePairPage.test.tsx)). Catalog is bundled `tokenlist.json` + env overlays — **not** Swap’s factory universe (**C542-8**). Natives are forbidden (**C542-2**). Submit still requires checksum (**C542-3**), distinct legs, and code-id whitelist (**C542-6**). Create Token → `/create` was copy-address only so a query string could not phish an admin into creating a hostile pair without looking. ### Trade identity is the pair path [`App.tsx`](frontend-dapp/src/App.tsx): `/trade` and `/trade/:pairAddr`. [`tradePairRoute.ts`](frontend-dapp/src/utils/tradePairRoute.ts) validates the segment and auto-picks a default factory pair when there is no deep link. Ticket is Market-default (#693). No `from`/`to` query. Sharing a pair is optional in #665 as `/trade/{pairAddr}` with **no** search. ## Why the new implementation is needed 1. **Share is a lie today.** After #711 a link can open the right pair, but the next picker change desyncs the URL. Mobile users have no control to share the live Swap. Desktop users copy a stale bar or a bare origin. 2. **Reverse quote is how Uniswap/Pancake links work.** `exactField=output` + `exactAmount` means “I want this many of the receive token.” Ignoring it looks like the dApp dropped half the link. Direct pairs already have `hybrid_reverse_simulation`. 3. **Create Pair / Trade are the other share surfaces.** Operators paste two 64-char addresses; a checksummed `/create?a=&b=` from Create Token or docs is the same job Swap just grew. Trade already has a canonical path — inbound `?from=&to=` should **resolve** to that path so token-page CTAs do not need to know the pair contract. 4. **One parser family.** Splitting rewrite vs reverse vs share vs Create/Trade produces apply/write loops and duplicate hostile-query code. Bundle them. This is **frontend routing + quote field + one Share control**. No contract migrate, no indexer exact-out solver, no Coolify env. ## Constraints / guardrails - **Factory / catalog gates stay.** Swap rewrite and share emit only ids in `getAllTokens(pairs)` after gem hide. Create Pair still uses **create-pair catalog + checksum paste**, never Swap’s factory graph (**C542-8**). Trade resolve only matches **factory-listed pairs**. - **Checksum (#382).** `terra1…` must pass `isValidTerraBech32Address`. Hostile / overlong / `javascript:` / `data:` / `http(s):` / `//` / `0x` / `ibc/` / `factory/` / `ETH` → ignore that side. Never echo raw query into combobox, share `text`, or `href`. - **Gems (#562).** Production does not apply or **write** gem ids. Never honor `?showGems=1`. Do not share a gem pair on mainnet builds. - **Canonical outbound keys** remain first-party `from` / `to` / `exactAmount` / `exactField` via `URLSearchParams` (extend `swapDeepLinkPath`). Uniswap names stay **inbound-only**. After rewrite, drop leftover inbound aliases from the bar. - **`replace`, never `push`,** for picker/amount/flip. Debounce amount (reuse swap quote debounce, ~300ms). No-op when the canonical query string is unchanged (breaks apply ↔ write loops). Do not remount (pathname key). - **Reverse quote execute path.** UI may be exact-out; **broadcast is still offer-in + min received**. Do not skip #678 quote-only / insufficient / 5% confirm-again / 30% Expert / 99% / blacklist / pause / freeze. Do not honor `slippage` / `expertMode` / `recipient` / `pool_only` / `hybrid_optimize` from the URL (#596). - **Reverse quote routing.** Use `reverseSimulateSwap` only when the live route is a **single factory pair** (direct hybrid / wrap). `GET /route/solve` is offer-in only — **no** binary-search exact-out, **no** indexer API change in this issue. If the best route is ≥2 hops, ignore `exactField=output` (silent, #489) and treat amount as pay-sided. - **Share payload.** Never share `window.location.href` (WC / leftover hostile keys). Build `{origin}/?from=&to=` (+ amount / exactField) from **resolved** ids. Origin is `window.location.origin` (**TS-9**). No `VITE_PUBLIC_ORIGIN`. No P&L. Static title **CL8Y DEX swap** + optional resolved symbols (not raw query). - **Share UX.** Header row next to Settings — **no** new `shell-panel*` / `card-glass` (**C653**). Label **Share** (or icon + Share). Coarse pointer / narrow viewport + `navigator.share`: Web Share. Desktop / share missing / `canShare === false` / non-abort error: `copyToClipboard` (**Link copied**). `AbortError` = cancel, no toast. 44px hit target, `aria-label`, focus ring, `sounds.playButtonPress`. - **Create Pair exception.** This issue **replaces** the “No `/create?a=&b=`” sentence of **C542-11** only. Keep: no HTTP catalog fetch, no natives, listed ≠ skip whitelist, U1 notice, no auto-submit. One-sided query OK. Same token → ignore second side. Custom `terra1` that checksums may prefill **Custom contract** (still #382 + code-id check). Do not auto-click Create. - **P402-5.** Create Token may start emitting `/create?a=<newCw20>` **after** this ships; until then copy-address remains valid. Prefill is not a substitute for the admin reading the picker. - **Trade canonical path.** `/trade/:pairAddr` stays the share/identity URL. `/trade?from=&to=` (aliases OK inbound) → unique factory pair whose legs match (order independent) → `replace` to `/trade/{pair}`. Zero or multiple matches → ignore query, existing auto-pick. Optional `amount` + `side=buy|sell` prefills the ticket only; **never** Place. Do not invent a query-only Trade page. - **#489.** No lecture banner “we ignored exactField / create query.” Silent fail-closed. - **XSS / open redirect.** No string concat into `Navigate to`. Share `url` is `URL` + `URLSearchParams`. No `dangerouslySetInnerHTML`. - **Do not** persist query in `localStorage`. Do not map EVM `0x` to Terra. Do not change nginx. U1: Swap/Trade UST1 links remain AMM, not mint/redeem. ## Relevant files | Path | Why | |------|-----| | `frontend-dapp/src/utils/swapQueryParams.ts` | Parse `exactField`; canonicalize write; `swapDeepLinkPath` | | `frontend-dapp/src/pages/SwapPage.tsx` | Apply + replace search; reverse-quote field; Share in header | | `frontend-dapp/src/services/terraclassic/pair.ts` | `reverseSimulateSwap` (direct only) | | `frontend-dapp/src/utils/cw20RouteSolveQuote.ts` | Offer-in only — do not fake exact-out | | `frontend-dapp/src/utils/sharePageLink.ts` + `ShareLinkButton.tsx` | Extend kinds / swap URL builder; reuse button | | `frontend-dapp/src/utils/sharePageLinkCopy.ts` | Swap title / aria / copied string | | `frontend-dapp/src/pages/CreatePairPage.tsx` | Apply `a`/`b`; optional rewrite | | `frontend-dapp/src/utils/createPairTokenCatalog.ts` | Gate prefill to catalog (or checksum paste) | | `frontend-dapp/src/pages/CreatePairPage.test.tsx` | Replace C542-11 “never prefill” with hostile-ignore + happy path | | `frontend-dapp/src/pages/TradePage.tsx` + `tradePairRoute.ts` | Resolve `from`/`to` → pair path; optional ticket prefill | | `frontend-dapp/src/App.tsx` | No second Swap mount; `/trade` already exists | | `frontend-dapp/src/components/common/SwapAliasRedirect.tsx` | Unchanged preserve search | | `docs/frontend.md` | New invariants; amend Q711-6/7, C542-11 sentence | | `skills/AGENTS_FRONTEND_SWAP_QUERY_PARAMS.md` | Rewrite, reverse, share | | `skills/AGENTS_FRONTEND_SHARE_LINK.md` | Swap exception to TS-2 | | `skills/AGENTS_FRONTEND_CREATE_PAIR_PICKER.md` | Query prefill + remaining C542 | | `scripts/qa/verify-issue-711.sh` | Keep green; add `verify-issue-*` for this ticket | ## Recommended direction 1. **Canonical search helper** (pure, Vitest-first): `canonicalSwapSearch({ payId, receiveId, amountHuman, exactField })` → `URLSearchParams` with `from`/`to`, `exactAmount` only if legal, `exactField=output` only when output. Empty amount omitted. Call from Swap `replace` and from share URL builder (`origin` + `'/'` + search, hash empty). 2. **Write-back on Swap.** After apply-from-URL, treat subsequent picker/flip/amount as source of truth: `setSearchParams(canonical, { replace: true })` when `canonical.toString() !== currentCanonical`. Amount debounce. Incoming apply must set the ref to the **canonical** key so the first rewrite does not re-apply as a “new” search. 3. **Reverse quote UI.** `exactField=output` (and inbound aliases `independentField=output` if cheap) makes You Receive the driven `<input>` and You Pay the quoted offer from `reverseSimulateSwap` when the route is 1-hop. Flip tokens keeps `exactField`. Execute: take reverse `offer_amount`, run existing forward quote + min-receive path. Multihop: ignore output field (pay-sided). 4. **Share control** on the Swap header row (flex next to Settings). Reuse `ShareLinkButton`. Mobile/coarse: `navigator.share`. Desktop: copy canonical URL. `data-testid="swap-share-link"`. 5. **Create Pair.** `parseCreatePairQuery` — keys `a`/`tokenA`/`token_a` and `b`/`tokenB`/`token_b`. Checksum + (catalog hit **or** valid terra1 for custom). Apply once; optional rewrite to `?a=&b=` with bech32 only. Update C542-11 docs/tests. 6. **Trade.** On `/trade` (no `:pairAddr`) or `/trade/` , parse Swap-style `from`/`to`; find unique factory pair with both legs; `navigate(/trade/{pair}, { replace: true })`. Ticket: optional `amount` / `side` after pair is known; Market default unchanged. Pair share stays path-only (#665) unless you also append `amount` (not required). ## Acceptance criteria - [ ] **AC1.** Changing Swap pay/receive/flip/amount updates the URL to canonical `/?from=&to=` (`exactAmount` / `exactField` when set) via `history.replaceState` / `setSearchParams({ replace: true })`. Back button is not spammed per keystroke. - [ ] **AC2.** Inbound Uniswap aliases still apply (#711), then rewrite to canonical keys (aliases disappear from the bar). - [ ] **AC3.** User picker after a deep link is **not** snapped back to the original query (write follows state; apply does not fight it). - [ ] **AC4.** `exactField=output` + legal amount on a **direct** pair: You Receive shows that amount; You Pay shows reverse-sim offer; quote/execute still offer-in + min received; #678 / 5–30–99 unchanged. Multihop: output field ignored (pay-sided). - [ ] **AC5.** Swap header **Share**: mobile/coarse uses Web Share with canonical URL; desktop copies **Link copied**. Abort is silent. Payload has no wallet, WC, slippage, or raw hostile query. - [ ] **AC6.** `/create?a=<listedCw20>&b=<listedCw20>` prefills Token A/B. Hostile / native / bad checksum ignored per side. Submit still disabled until whitelist + distinct legs. **No** auto-submit. - [ ] **AC7.** `/trade?from=uluna&to=<listed>` (or aliases) `replace`s to `/trade/{uniquePair}`. Ambiguous/missing pair: ignore. `/trade/:pairAddr` unchanged as canonical. Ticket not auto-placed. - [ ] **AC8.** Production cannot share or rewrite a gem. `?showGems=1` still ignored. - [ ] **AC9.** Docs + skills: Q711-6/7 updated; C542-11 prefill sentence replaced; Share TS exception for Swap search; `make verify-issue-711` still passes. - [ ] **AC10.** No new chrome layer; no lecture banners; Create Pair catalog still has no runtime HTTP. ## Test plan (functional paths) | # | Path | Expect | |---|------|--------| | T1 | `/` no query, pick tokens | URL becomes `/?from=&to=` (ids, not tickers) | | T2 | `/?inputCurrency=uluna&outputCurrency=<listed>` | Pair applies, bar rewrites to `from`/`to` | | T3 | Change receive after T2 | URL follows new receive; no snap-back | | T4 | Type amount `1.5` (debounced) | `exactAmount=1.5` appears; rapid typing = one replace | | T5 | Clear amount | `exactAmount` omitted | | T6 | Flip | `from`/`to` swap; `exactField` preserved if set | | T7 | Direct pair `exactField=output&exactAmount=1` | Receive `1`; pay from reverse sim | | T8 | Multihop + `exactField=output` | Amount treated pay-sided; no crash | | T9 | Share on desktop | Clipboard = canonical `https?://{origin}/?from=&to=` | | T10 | Share on coarse + `canShare` | `navigator.share` with same URL; abort → no copy error | | T11 | `/create?a=&b=` listed CW20s | Both pickers set; Create still gated | | T12 | `/create?a=javascript:alert(1)` | Ignored; empty A | | T13 | `/create?a=uluna` | Ignored (native) | | T14 | `/trade?from=uluna&to=<listed>` unique pair | Lands on `/trade/{pair}` | | T15 | `/trade?from=uluna&to=uluna` | Ignore; default pair pick | | T16 | Existing `/swap` E2E + #711 RTL | Still green | | T17 | Wrap pair reverse quote | Still valid wrap UI; execute unchanged | | T18 | Hash `#/` | Ignored; search only | Vitest: table-driven canonical search + exactField parse; RTL Swap rewrite + share; CreatePair happy + hostile (replace old C542-11 never-prefill); Trade resolve helper. Playwright: share button present; URL updates on token change (`PLAYWRIGHT_SKIP_CHAIN=1` OK). 5 workers. ## Test plan (attack, hack, and abuse) | # | Vector | Expect | |---|--------|--------| | A1 | Rewrite concatenating `from` + user string | Never; only `URLSearchParams.set` | | A2 | Share `window.location.href` with `?recipient=` leftover | Share builder uses canonical keys only | | A3 | `exactField=output` huge amount | Cap 24 chars / `isPositiveDecimalAmount`; else ignore field | | A4 | Reverse quote as execute bypass | Still offer-in; slippage / Expert / blacklist still block | | A5 | `/create?a=<attackerCw20>` unlisted | Prefill custom only if checksum valid; whitelist warning; no auto-submit | | A6 | `/create?a=<gem>` production | Ignore (retail hide / not in catalog) | | A7 | `/trade?from=` open redirect `https://phish` | Parser returns no URL; navigate only to `/trade/{bech32}` | | A8 | Multiple factory pairs same legs | Do not pick at random; ignore | | A9 | History flood from amount input | Debounce + replace; not push | | A10 | Apply/write infinite loop | Canonical compare; RTL / no act-spam | | A11 | Share title with raw `to=<script>` | Title static; symbols from registry only | | A12 | `exactField=output` on paused/frozen listed token | Existing banners; no execute | | A13 | Clickwrap / WC URI in search | Strip; share/rewrite never copies WC | | A14 | Desktop accidentally opening OS share sheet | Copy fallback when not coarse / `canShare` false | | A15 | C542-8 leak: Create prefill from Swap factory id not in catalog | Ignore unless checksum paste path | ## Verification criteria - New `make verify-issue-<id>` (parser + RTL + CreatePair + Trade helper + docs/skills greps). `make verify-issue-711`, `make verify-issue-542`, `make verify-issue-665`, `make verify-issue-678`, `python3 scripts/check_chrome_nesting.py` stay green. - Playwright: token change rewrites URL; Share copies canonical search; `/create?a=&b=` prefills listed CW20s; `/trade?from=&to=` resolves unique pair. - Manual: `make dev` → open `/swap?from=uluna&to=<listed>` → change token → Share/copy → paste in new tab → same pair. Reverse quote on a direct pair. Hostile create query does nothing. Trade query lands on pair workspace, not Market submit. - Coolify after merge: share from dex.cl8y.com produces a link that survives a reload; Create/Trade prefills do not auto-tx. ## Out of scope - Indexer exact-out `/route/solve`. - Persisting Swap state in `localStorage`. - Per-route Open Graph (#578). - QR library / third-party share SDK. - Mapping BSC `0x` CL8Y into Terra Swap. - cl8y-web hrefs (already [CL8Y-web #6](https://gitlab.com/PlasticDigits/CL8Y-web/-/work_items/6), gated on !1199). - Expanding !1199 itself.
PlasticDigits commented 2026-08-31 05:36:04 +00:00 (Migrated from gitlab.com)

marked as related to #711

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

mentioned in issue #711

mentioned in issue #711
PlasticDigits commented 2026-08-31 05:36:20 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1199

mentioned in merge request !1199
PlasticDigits commented 2026-08-31 09:54:26 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1200

mentioned in merge request !1200
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-31 11:17:13 +00:00
PlasticDigits commented 2026-08-31 11:18:41 +00:00 (Migrated from gitlab.com)

mentioned in issue #714

mentioned in issue #714
PlasticDigits commented 2026-08-31 11:18:42 +00:00 (Migrated from gitlab.com)

marked as related to #714

marked as related to #714
PlasticDigits commented 2026-08-31 11:18:57 +00:00 (Migrated from gitlab.com)

Merged as !1200 into main (stacked on !1199 / #711).

Sanity check before merge:

  • Makefile conflict vs main 708–710 help text — resolved (kept 708–710 + verify-issue-711 / verify-issue-713).
  • #678 Reduce in reverse mode wrote pay-human into You Receive; Max/fraction already switched exactField to input. Fixed on the MR.
  • make verify-issue-711 and make verify-issue-713 passed. Playwright Swap URL/Share e2e skipped — LocalTerra VITE_FACTORY_ADDRESS pin was stale (Create Pair query smoke still passed).
  • RTL still does not assert picker→URL rewrite or Share clipboard (T1/T3/T9). Trade /trade?from=&to= e2e not run.

Post-merge leftover: #714. Do not reopen this issue unless a merged invariant is wrong. Marketing-site hrefs stay CL8Y-web #6.

Merged as !1200 into main (stacked on !1199 / #711). Sanity check before merge: - Makefile conflict vs main 708–710 help text — resolved (kept 708–710 + `verify-issue-711` / `verify-issue-713`). - `#678` Reduce in reverse mode wrote pay-human into You Receive; Max/fraction already switched `exactField` to input. Fixed on the MR. - `make verify-issue-711` and `make verify-issue-713` passed. Playwright Swap URL/Share e2e skipped — LocalTerra `VITE_FACTORY_ADDRESS` pin was stale (Create Pair query smoke still passed). - RTL still does not assert picker→URL rewrite or Share clipboard (T1/T3/T9). Trade `/trade?from=&to=` e2e not run. Post-merge leftover: #714. Do not reopen this issue unless a merged invariant is wrong. Marketing-site hrefs stay [CL8Y-web #6](https://gitlab.com/PlasticDigits/CL8Y-web/-/work_items/6).
PlasticDigits commented 2026-08-31 16:08:49 +00:00 (Migrated from gitlab.com)

mentioned in issue #715

mentioned in issue #715
PlasticDigits commented 2026-08-31 16:08:51 +00:00 (Migrated from gitlab.com)

marked as related to #715

marked as related to #715
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:11 +00:00 (Migrated from gitlab.com)

mentioned in commit 86744db1ad

mentioned in commit 86744db1add36673b81950a440622aca73ecac31
PlasticDigits commented 2026-09-01 07:48:09 +00:00 (Migrated from gitlab.com)

mentioned in issue #716

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