feat(frontend): Swap URL sync, reverse quotes, share control, Create/Trade query prefill #713
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#713
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 / !1199. One product surface: shareable DEX URLs.
Bundle (do not split):
exactField=output(You Receive is the independent amount)./?from=&to=in sync with picker, flip, and amount (replace, not history spam)./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.tsxusesuseSearchParams+appliedSwapQueryKeyRef: apply once per search string aftergetAllTokens(pairs)(Q711-7). PickeronChange, flip, andinputAmountdo not callsetSearchParams.swapDeepLinkPathalready mints/?from=&to=(+ optionalexactAmount) withURLSearchParams, 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./swapredirects with search preserved (SwapAliasRedirect.tsx, Q711-1). Layout Outlet iskey={pathname}— search-onlyreplacedoes 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 noexactField. You Pay is an<input>; You Receive is quote chrome. Quotes go through offer-inquoteCw20ViaRouteSolve(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 sharingwindow.location.hrefand strips query — correct for trader profiles, wrong for Swap (the query is the payload).SHARE_PAGE_KINDSistrader | trade | chartsonly. Swap header is H2 Swap + Settings inside oneshell-panel-strong— no share control.Create Pair voids query on purpose
CreatePairPage.tsxinitializestokenA/tokenBto''. NouseSearchParams. RTL P402-5 / C542-11 asserts/create?a=&b=does not prefill (CreatePairPage.test.tsx). Catalog is bundledtokenlist.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 →/createwas 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:/tradeand/trade/:pairAddr.tradePairRoute.tsvalidates the segment and auto-picks a default factory pair when there is no deep link. Ticket is Market-default (#693). Nofrom/toquery. Sharing a pair is optional in #665 as/trade/{pairAddr}with no search.Why the new implementation is needed
exactField=output+exactAmountmeans “I want this many of the receive token.” Ignoring it looks like the dApp dropped half the link. Direct pairs already havehybrid_reverse_simulation./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.This is frontend routing + quote field + one Share control. No contract migrate, no indexer exact-out solver, no Coolify env.
Constraints / guardrails
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.terra1…must passisValidTerraBech32Address. Hostile / overlong /javascript:/data:/http(s):////0x/ibc//factory//ETH→ ignore that side. Never echo raw query into combobox, sharetext, orhref.?showGems=1. Do not share a gem pair on mainnet builds.from/to/exactAmount/exactFieldviaURLSearchParams(extendswapDeepLinkPath). Uniswap names stay inbound-only. After rewrite, drop leftover inbound aliases from the bar.replace, neverpush, 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).slippage/expertMode/recipient/pool_only/hybrid_optimizefrom the URL (#596).reverseSimulateSwaponly when the live route is a single factory pair (direct hybrid / wrap).GET /route/solveis offer-in only — no binary-search exact-out, no indexer API change in this issue. If the best route is ≥2 hops, ignoreexactField=output(silent, #489) and treat amount as pay-sided.window.location.href(WC / leftover hostile keys). Build{origin}/?from=&to=(+ amount / exactField) from resolved ids. Origin iswindow.location.origin(TS-9). NoVITE_PUBLIC_ORIGIN. No P&L. Static title CL8Y DEX swap + optional resolved symbols (not raw query).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?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. Customterra1that checksums may prefill Custom contract (still #382 + code-id check). Do not auto-click Create./create?a=<newCw20>after this ships; until then copy-address remains valid. Prefill is not a substitute for the admin reading the picker./trade/:pairAddrstays the share/identity URL./trade?from=&to=(aliases OK inbound) → unique factory pair whose legs match (order independent) →replaceto/trade/{pair}. Zero or multiple matches → ignore query, existing auto-pick. Optionalamount+side=buy|sellprefills the ticket only; never Place. Do not invent a query-only Trade page.Navigate to. ShareurlisURL+URLSearchParams. NodangerouslySetInnerHTML.localStorage. Do not map EVM0xto Terra. Do not change nginx. U1: Swap/Trade UST1 links remain AMM, not mint/redeem.Relevant files
frontend-dapp/src/utils/swapQueryParams.tsexactField; canonicalize write;swapDeepLinkPathfrontend-dapp/src/pages/SwapPage.tsxfrontend-dapp/src/services/terraclassic/pair.tsreverseSimulateSwap(direct only)frontend-dapp/src/utils/cw20RouteSolveQuote.tsfrontend-dapp/src/utils/sharePageLink.ts+ShareLinkButton.tsxfrontend-dapp/src/utils/sharePageLinkCopy.tsfrontend-dapp/src/pages/CreatePairPage.tsxa/b; optional rewritefrontend-dapp/src/utils/createPairTokenCatalog.tsfrontend-dapp/src/pages/CreatePairPage.test.tsxfrontend-dapp/src/pages/TradePage.tsx+tradePairRoute.tsfrom/to→ pair path; optional ticket prefillfrontend-dapp/src/App.tsx/tradealready existsfrontend-dapp/src/components/common/SwapAliasRedirect.tsxdocs/frontend.mdskills/AGENTS_FRONTEND_SWAP_QUERY_PARAMS.mdskills/AGENTS_FRONTEND_SHARE_LINK.mdskills/AGENTS_FRONTEND_CREATE_PAIR_PICKER.mdscripts/qa/verify-issue-711.shverify-issue-*for this ticketRecommended direction
canonicalSwapSearch({ payId, receiveId, amountHuman, exactField })→URLSearchParamswithfrom/to,exactAmountonly if legal,exactField=outputonly when output. Empty amount omitted. Call from Swapreplaceand from share URL builder (origin+'/'+ search, hash empty).setSearchParams(canonical, { replace: true })whencanonical.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.exactField=output(and inbound aliasesindependentField=outputif cheap) makes You Receive the driven<input>and You Pay the quoted offer fromreverseSimulateSwapwhen the route is 1-hop. Flip tokens keepsexactField. Execute: take reverseoffer_amount, run existing forward quote + min-receive path. Multihop: ignore output field (pay-sided).ShareLinkButton. Mobile/coarse:navigator.share. Desktop: copy canonical URL.data-testid="swap-share-link".parseCreatePairQuery— keysa/tokenA/token_aandb/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./trade(no:pairAddr) or/trade/, parse Swap-stylefrom/to; find unique factory pair with both legs;navigate(/trade/{pair}, { replace: true }). Ticket: optionalamount/sideafter pair is known; Market default unchanged. Pair share stays path-only (#665) unless you also appendamount(not required).Acceptance criteria
/?from=&to=(exactAmount/exactFieldwhen set) viahistory.replaceState/setSearchParams({ replace: true }). Back button is not spammed per keystroke.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)./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./trade?from=uluna&to=<listed>(or aliases)replaces to/trade/{uniquePair}. Ambiguous/missing pair: ignore./trade/:pairAddrunchanged as canonical. Ticket not auto-placed.?showGems=1still ignored.make verify-issue-711still passes.Test plan (functional paths)
/no query, pick tokens/?from=&to=(ids, not tickers)/?inputCurrency=uluna&outputCurrency=<listed>from/to1.5(debounced)exactAmount=1.5appears; rapid typing = one replaceexactAmountomittedfrom/toswap;exactFieldpreserved if setexactField=output&exactAmount=11; pay from reverse simexactField=outputhttps?://{origin}/?from=&to=canSharenavigator.sharewith same URL; abort → no copy error/create?a=&b=listed CW20s/create?a=javascript:alert(1)/create?a=uluna/trade?from=uluna&to=<listed>unique pair/trade/{pair}/trade?from=uluna&to=uluna/swapE2E + #711 RTL#/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=1OK). 5 workers.Test plan (attack, hack, and abuse)
from+ user stringURLSearchParams.setwindow.location.hrefwith?recipient=leftoverexactField=outputhuge amountisPositiveDecimalAmount; else ignore field/create?a=<attackerCw20>unlisted/create?a=<gem>production/trade?from=open redirecthttps://phish/trade/{bech32}to=<script>exactField=outputon paused/frozen listed tokencanSharefalseVerification criteria
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.pystay green./create?a=&b=prefills listed CW20s;/trade?from=&to=resolves unique pair.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.Out of scope
/route/solve.localStorage.0xCL8Y into Terra Swap.marked as related to #711
mentioned in issue #711
mentioned in merge request !1199
mentioned in merge request !1200
mentioned in issue #714
marked as related to #714
Merged as !1200 into main (stacked on !1199 / #711).
Sanity check before merge:
verify-issue-711/verify-issue-713).#678Reduce in reverse mode wrote pay-human into You Receive; Max/fraction already switchedexactFieldto input. Fixed on the MR.make verify-issue-711andmake verify-issue-713passed. Playwright Swap URL/Share e2e skipped — LocalTerraVITE_FACTORY_ADDRESSpin was stale (Create Pair query smoke still passed)./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.
mentioned in issue #715
marked as related to #715
mentioned in merge request !1201
mentioned in commit
86744db1admentioned in issue #716