Trade page reverts any non-default pair to EMBER/CORAL on mount: default-pick effect clobbers the route-set effect #357

Closed
opened 2026-06-10 06:51:01 +00:00 by Brouie · 13 comments
Brouie commented 2026-06-10 06:51:01 +00:00 (Migrated from gitlab.com)

Summary

On /trade, navigating to or deep-linking any pair other than the default (factory pairs[0] = EMBER/CORAL) loads it for a moment, then snaps the URL back to the default pair. No crash, no console error. This is separate from #354 (the ErrorBoundary crash, fixed) and #350 (type+Enter value, fixed) — those are both resolved; this is a third defect in the same area. With it, you cannot trade or deep-link any non-default pair via the /trade UI.

Repro (cleanest — no selector involved)

Paste a non-default pair URL straight into the address bar, fresh load:
/trade/terra1nc84knc0n7td5xqplwy0luh97zd8hv5mhvm9cdempc05xk0xvxyqjr6cyg (EMBER/JADE)
-> it loads briefly, then the URL reverts to /trade/terra146ypndztcmmrmyxef7e20cul82gh43vjnw4uacwdvg5sp9kva7sqc9mjav (the default EMBER/CORAL). Reproduces in a clean incognito window. Clicking a pair in the search selector and type+Enter both hit the same revert.

Backend is fine: the factory LCD pairs query returns all 27 incl. this pair, and the indexer /api/v1/pairs/{addr} returns 200 for it — so it is a known factory pair, not a data gap.

Root cause (TradePage.tsx)

Two effects race on the mount render, when pairAddr is still '' from useState(''):

  • Effect A (the route->state sync, ~line 161): isKnownFactoryTradePair(routePair, pairs) is true for the deep-linked pair, so it queues setPairAddr(routePair).
  • Effect B (the default-picker, ~line 168): its guard is if (pairAddr || pairs.length === 0 || invalidRoutePair || unknownRoutePair || ...) return. On the mount render pairAddr is still '' (A's update has not applied yet), pairs is resolved, and there is no invalid/unknown notice — so the guard does not catch it and it queues setPairAddr(pairs[0]) + navigate('/trade/<default>', { replace: true }).

Effect B runs after A in the same commit and its navigate wins, so the URL ends on the default pair. Effect B guards against invalid and unknown route params but not against a valid, KNOWN routePair — which is exactly the case it should leave to effect A.

This fires whenever TradePage mounts (or remounts) with a pair in the URL and the factory list is resolved — i.e. every deep link to a non-default pair, and any pair switch that remounts the page (/trade and /trade/:pairAddr are two separate Route entries).

Fix direction

Effect B (default-pick) should only run when there is NO route param to honor. Add a guard so it bails when routePair is present and not invalid/unknown (let effect A own the known-pair case) — e.g. early-return if routePair is set, or gate the default-pick on !routePair. A deep-link-to-non-default-pair test (assert the URL stays on the requested pair after pairs resolve) would lock it.

Impact

Breaks pair selection and shareable deep links on /trade for every pair except the default — only EMBER/CORAL is reachable through the trade UI. Gates the master-checklist core-flow / nav items (#337). Note TradePage.tsx is unchanged in the recent range, so this is pre-existing, not introduced by the #350/#353/#354 fixes.

cc @PlasticDigits

### Summary On /trade, navigating to or deep-linking any pair other than the default (factory pairs[0] = EMBER/CORAL) loads it for a moment, then snaps the URL back to the default pair. No crash, no console error. This is separate from #354 (the ErrorBoundary crash, fixed) and #350 (type+Enter value, fixed) — those are both resolved; this is a third defect in the same area. With it, you cannot trade or deep-link any non-default pair via the /trade UI. ### Repro (cleanest — no selector involved) Paste a non-default pair URL straight into the address bar, fresh load: `/trade/terra1nc84knc0n7td5xqplwy0luh97zd8hv5mhvm9cdempc05xk0xvxyqjr6cyg` (EMBER/JADE) -> it loads briefly, then the URL reverts to `/trade/terra146ypndztcmmrmyxef7e20cul82gh43vjnw4uacwdvg5sp9kva7sqc9mjav` (the default EMBER/CORAL). Reproduces in a clean incognito window. Clicking a pair in the search selector and type+Enter both hit the same revert. Backend is fine: the factory LCD `pairs` query returns all 27 incl. this pair, and the indexer `/api/v1/pairs/{addr}` returns 200 for it — so it is a known factory pair, not a data gap. ### Root cause (TradePage.tsx) Two effects race on the mount render, when `pairAddr` is still '' from `useState('')`: - Effect A (the route->state sync, ~line 161): `isKnownFactoryTradePair(routePair, pairs)` is true for the deep-linked pair, so it queues `setPairAddr(routePair)`. - Effect B (the default-picker, ~line 168): its guard is `if (pairAddr || pairs.length === 0 || invalidRoutePair || unknownRoutePair || ...) return`. On the mount render `pairAddr` is still '' (A's update has not applied yet), pairs is resolved, and there is no invalid/unknown notice — so the guard does not catch it and it queues `setPairAddr(pairs[0])` + `navigate('/trade/<default>', { replace: true })`. Effect B runs after A in the same commit and its navigate wins, so the URL ends on the default pair. Effect B guards against invalid and unknown route params but not against a valid, KNOWN routePair — which is exactly the case it should leave to effect A. This fires whenever TradePage mounts (or remounts) with a pair in the URL and the factory list is resolved — i.e. every deep link to a non-default pair, and any pair switch that remounts the page (/trade and /trade/:pairAddr are two separate Route entries). ### Fix direction Effect B (default-pick) should only run when there is NO route param to honor. Add a guard so it bails when `routePair` is present and not invalid/unknown (let effect A own the known-pair case) — e.g. early-return if `routePair` is set, or gate the default-pick on `!routePair`. A deep-link-to-non-default-pair test (assert the URL stays on the requested pair after pairs resolve) would lock it. ### Impact Breaks pair selection and shareable deep links on /trade for every pair except the default — only EMBER/CORAL is reachable through the trade UI. Gates the master-checklist core-flow / nav items (#337). Note TradePage.tsx is unchanged in the recent range, so this is pre-existing, not introduced by the #350/#353/#354 fixes. cc @PlasticDigits
PlasticDigits commented 2026-06-10 07:07:40 +00:00 (Migrated from gitlab.com)

mentioned in commit 1aeaf2c362

mentioned in commit 1aeaf2c3625cc5078c6a9a4d58f10bf02edd543b
PlasticDigits commented 2026-06-10 07:07:51 +00:00 (Migrated from gitlab.com)

Fix landed on main (1aeaf2c)

Root cause: On mount, the default-pair effect (pairs[0] + navigate) raced the route→state sync effect while pairAddr was still ''. Effect B did not bail for valid known :pairAddr segments, so non-default deep links and pair-selector switches snapped back to the first factory pair.

Change:

  • Added shouldAutoPickDefaultTradePair in tradePairRoute.ts — default-pick runs only for bare /trade (no valid deep-link segment, no invalid/unknown notice, not pending factory resolution).
  • Wired the guard into TradePage.tsx default-pick effect.
  • Regressions in TradePage.test.tsx + tradePairRoute.test.ts.
  • Docs: docs/frontend.md § Trade page — known pair deep link; skill update in skills/AGENTS_FRONTEND_TRADE_INVALID_PAIR_LINK.md (§ Known pair deep link).

Verification checklist

  • Fresh tab: paste /trade/<non-default-factory-pair> — URL stays on requested pair after pairs load (no snap to EMBER/CORAL).
  • Pair search selector: pick a non-default pair — URL and workspace stay on selection.
  • Bare /trade still auto-navigates to first factory pair.
  • Invalid deep link (/trade/lilwayne%20babyyy) still shows invalid notice and clears URL.
  • Unknown valid-format deep link still shows pair-not-found notice (no workspace flash).
  • Unit: TradePage.test.tsx + tradePairRoute.test.ts pass locally.

Follow-ups

None identified — this was a frontend routing race only; backend/indexer were already correct per issue repro notes.

Requesting verification from the QA agent team when convenient.

## Fix landed on `main` (`1aeaf2c`) **Root cause:** On mount, the default-pair effect (`pairs[0]` + `navigate`) raced the route→state sync effect while `pairAddr` was still `''`. Effect B did not bail for valid known `:pairAddr` segments, so non-default deep links and pair-selector switches snapped back to the first factory pair. **Change:** - Added `shouldAutoPickDefaultTradePair` in `tradePairRoute.ts` — default-pick runs only for bare `/trade` (no valid deep-link segment, no invalid/unknown notice, not pending factory resolution). - Wired the guard into `TradePage.tsx` default-pick effect. - Regressions in `TradePage.test.tsx` + `tradePairRoute.test.ts`. - Docs: [docs/frontend.md § Trade page — known pair deep link](docs/frontend.md#trade-page-known-pair-deep-link); skill update in `skills/AGENTS_FRONTEND_TRADE_INVALID_PAIR_LINK.md` (§ Known pair deep link). ### Verification checklist - [ ] Fresh tab: paste `/trade/<non-default-factory-pair>` — URL stays on requested pair after pairs load (no snap to EMBER/CORAL). - [ ] Pair search selector: pick a non-default pair — URL and workspace stay on selection. - [ ] Bare `/trade` still auto-navigates to first factory pair. - [ ] Invalid deep link (`/trade/lilwayne%20babyyy`) still shows invalid notice and clears URL. - [ ] Unknown valid-format deep link still shows pair-not-found notice (no workspace flash). - [ ] Unit: `TradePage.test.tsx` + `tradePairRoute.test.ts` pass locally. ### Follow-ups None identified — this was a frontend routing race only; backend/indexer were already correct per issue repro notes. Requesting verification from the QA agent team when convenient.
PlasticDigits commented 2026-06-10 11:09:33 +00:00 (Migrated from gitlab.com)

mentioned in commit f63957a171

mentioned in commit f63957a17102b847efc70597ff29457cf095194e
PlasticDigits commented 2026-06-10 11:09:42 +00:00 (Migrated from gitlab.com)

mentioned in merge request !862

mentioned in merge request !862
PlasticDigits commented 2026-06-10 11:10:00 +00:00 (Migrated from gitlab.com)

Core fix on main (1aeaf2c): shouldAutoPickDefaultTradePair gates default-pick to bare /trade only; route→state sync owns known :pairAddr segments.

Browser verified on LocalTerra + make dev: deep link to factory pairs[1] (EMBER/JADE) stays on requested URL and pair selector after pairs resolve — no snap to EMBER/CORAL.

Note for manual QA: use addresses from current factory LCD (getAllPairsPaginated). Stale indexer-only pair rows (absent from factory) correctly trigger unknown-pair handling, not #357 snap-back.

Checklist for QA

  • Fresh tab: /trade/<non-default factory LCD pair> — URL stable after pairs load
  • Pair search selector: pick non-default pair — URL + workspace stay on selection
  • Bare /trade still auto-navigates to first factory pair
  • Invalid deep link still shows invalid notice + clears URL
  • Unknown valid-format deep link still shows pair-not-found notice
  • Unit: TradePage.test.tsx + tradePairRoute.test.ts pass

MR

https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/merge_requests/144 (QA doc clarification for agent manual repro)

Follow-ups

None — routing race only; backend/indexer were already correct per issue repro.

## Agent verification (local1/357-impl-deep-link) Core fix on `main` (`1aeaf2c`): `shouldAutoPickDefaultTradePair` gates default-pick to bare `/trade` only; route→state sync owns known `:pairAddr` segments. Browser verified on LocalTerra + `make dev`: deep link to factory **pairs[1]** (EMBER/JADE) stays on requested URL and pair selector after pairs resolve — no snap to EMBER/CORAL. **Note for manual QA:** use addresses from **current factory LCD** (`getAllPairsPaginated`). Stale indexer-only pair rows (absent from factory) correctly trigger unknown-pair handling, not #357 snap-back. ### Checklist for QA - [ ] Fresh tab: `/trade/<non-default factory LCD pair>` — URL stable after pairs load - [ ] Pair search selector: pick non-default pair — URL + workspace stay on selection - [ ] Bare `/trade` still auto-navigates to first factory pair - [ ] Invalid deep link still shows invalid notice + clears URL - [ ] Unknown valid-format deep link still shows pair-not-found notice - [ ] Unit: `TradePage.test.tsx` + `tradePairRoute.test.ts` pass ### MR https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/merge_requests/144 (QA doc clarification for agent manual repro) ### Follow-ups None — routing race only; backend/indexer were already correct per issue repro.
PlasticDigits commented 2026-06-10 11:19:10 +00:00 (Migrated from gitlab.com)

mentioned in commit 3169af0e90

mentioned in commit 3169af0e9091b0d118ca85bf6e1dce9bc111d69c
Brouie commented 2026-06-11 02:07:02 +00:00 (Migrated from gitlab.com)

Source + unit pass on the merged fix (1aeaf2c, main 3169af0) — adding the human layer on top of the agent's browser pass.

  • Mechanism matches the filed race exactly: shouldAutoPickDefaultTradePair (tradePairRoute.ts) bails default-pick for any valid-format :pairAddr segment plus the invalid/unknown/pending cases, so the route-sync effect owns known deep links. The guard is order-independent — the race is closed structurally, not by effect ordering luck.
  • TradePage.test.tsx + tradePairRoute.test.ts at 3169af0: 28/28 green, including the new "keeps non-default deep link after factory pairs resolve" regression.
  • Proved the regression test actually discriminates: same tests against the pre-fix tree (1aeaf2c^) — the deep-link test fails with the exact filed snap-back (pathname ends on pairs[0] instead of the requested pair). Bare-/trade default-pick behaves the same pre/post, as intended.
  • Edge sweep on the guard: valid deep link while factory pairs still load -> no auto-pick, no workspace flash; invalid + unknown notice flows (#175/#176) all still green.

One marginal find, not a blocker: the guard trims routePair but the route-sync side (isKnownFactoryTradePair) doesn't, so a deep link with encoded whitespace like /trade/%20terra1... now selects nothing silently, where before it snapped to default. Takes a literal %20 in the URL to hit — P3, can ride along with any future touch on this file.

Left from the checklist: the browser rows (fresh-tab deep link, selector pick, bare /trade, invalid/unknown notices). Those run in my next laptop browser batch, deep-linking addresses from the current factory LCD per the note above. Each already has unit coverage at HEAD, so that pass is confirmation, not discovery.

Source + unit pass on the merged fix (1aeaf2c, main 3169af0) — adding the human layer on top of the agent's browser pass. - Mechanism matches the filed race exactly: shouldAutoPickDefaultTradePair (tradePairRoute.ts) bails default-pick for any valid-format :pairAddr segment plus the invalid/unknown/pending cases, so the route-sync effect owns known deep links. The guard is order-independent — the race is closed structurally, not by effect ordering luck. - TradePage.test.tsx + tradePairRoute.test.ts at 3169af0: 28/28 green, including the new "keeps non-default deep link after factory pairs resolve" regression. - Proved the regression test actually discriminates: same tests against the pre-fix tree (1aeaf2c^) — the deep-link test fails with the exact filed snap-back (pathname ends on pairs[0] instead of the requested pair). Bare-/trade default-pick behaves the same pre/post, as intended. - Edge sweep on the guard: valid deep link while factory pairs still load -> no auto-pick, no workspace flash; invalid + unknown notice flows (#175/#176) all still green. One marginal find, not a blocker: the guard trims routePair but the route-sync side (isKnownFactoryTradePair) doesn't, so a deep link with encoded whitespace like /trade/%20terra1... now selects nothing silently, where before it snapped to default. Takes a literal %20 in the URL to hit — P3, can ride along with any future touch on this file. Left from the checklist: the browser rows (fresh-tab deep link, selector pick, bare /trade, invalid/unknown notices). Those run in my next laptop browser batch, deep-linking addresses from the current factory LCD per the note above. Each already has unit coverage at HEAD, so that pass is confirmation, not discovery.
Brouie commented 2026-06-11 02:12:52 +00:00 (Migrated from gitlab.com)

mentioned in issue #337

mentioned in issue #337
Brouie commented 2026-06-11 02:54:49 +00:00 (Migrated from gitlab.com)

mentioned in issue #358

mentioned in issue #358
Brouie commented 2026-06-11 02:55:04 +00:00 (Migrated from gitlab.com)

Browser half done — checklist run on the dapp at 3169af0, fresh tabs, address-bar navigation, factory-LCD addresses.

  • Fresh-tab deep link to a non-default factory pair: holds after pairs resolve, no snap-back. PASS.
  • Pair search selector to a non-default pair: URL + workspace stay. PASS. Typing a symbol + Enter also lands the right pair — the #350 path is live and good.
  • Bare /trade: auto-navigates to the first factory pair. PASS.
  • Invalid deep link (/trade/lilwayne%20babyyy): notice shown, URL cleared. PASS on the walk — with the caveat in #358 that this flow shares the wiped-state mechanism; the final-state re-check rides there.
  • Unknown valid-format deep link: FAIL — no notice, silently lands on the default pair. Root-caused and filed as #358: pre-existing f07a71c keyed-Outlet remount wiping the notice state, NOT a 1aeaf2c regression (probed bit-identical at e198dcb and 3169af0). The unit row stays green only because the test router has no Layout shell — test gap documented there too.
  • Unit: TradePage.test.tsx + tradePairRoute.test.ts 28/28 at 3169af0, pre-fix discrimination proven (earlier note).

The #357 defect itself — known-pair deep links + selector switching — is verified end to end: source, unit, real browser. Good to close from my side, with #358 tracking the unknown-link row separately. @PlasticDigits

Browser half done — checklist run on the dapp at 3169af0, fresh tabs, address-bar navigation, factory-LCD addresses. - Fresh-tab deep link to a non-default factory pair: holds after pairs resolve, no snap-back. PASS. - Pair search selector to a non-default pair: URL + workspace stay. PASS. Typing a symbol + Enter also lands the right pair — the #350 path is live and good. - Bare /trade: auto-navigates to the first factory pair. PASS. - Invalid deep link (/trade/lilwayne%20babyyy): notice shown, URL cleared. PASS on the walk — with the caveat in #358 that this flow shares the wiped-state mechanism; the final-state re-check rides there. - Unknown valid-format deep link: FAIL — no notice, silently lands on the default pair. Root-caused and filed as #358: pre-existing f07a71c keyed-Outlet remount wiping the notice state, NOT a 1aeaf2c regression (probed bit-identical at e198dcb and 3169af0). The unit row stays green only because the test router has no Layout shell — test gap documented there too. - Unit: TradePage.test.tsx + tradePairRoute.test.ts 28/28 at 3169af0, pre-fix discrimination proven (earlier note). The #357 defect itself — known-pair deep links + selector switching — is verified end to end: source, unit, real browser. Good to close from my side, with #358 tracking the unknown-link row separately. @PlasticDigits
PlasticDigits commented 2026-06-11 15:19:05 +00:00 (Migrated from gitlab.com)

Agent verification — PASS (close #357)

Verified fix on main (1aeaf2c, HEAD 69c0aa1): shouldAutoPickDefaultTradePair gates default-pick to bare /trade only; route→state sync owns valid known :pairAddr segments.

Checklist

Item Result How
Fresh tab: /trade/<non-default factory pair> stays after pairs load PASS Playwright on LocalTerra + make dev; EMBER/JADE (terra1nc84…6cyg) URL stable after factory resolve
Pair search selector: non-default pick keeps URL + workspace PASS Playwright selector click → EMBER/JADE; risk ack pre-seeded (cl8y-dex-risk-ack)
Bare /trade auto-navigates to first factory pair PASS Playwright → EMBER/CORAL (terra146yp…mjav)
Invalid deep link shows notice + clears URL PASS Playwright /trade/lilwayne%20babyyy
Unknown valid-format deep link shows pair-not-found notice SKIP (#358) Playwright: no notice (Layout Outlet remount); filed separately as #358 — not a #357 regression
Unit: TradePage.test.tsx + tradePairRoute.test.ts PASS 28/28 locally
Regression test discriminates pre-fix PASS keeps non-default deep link… fails on 1aeaf2c^ with snap-back to pairs[0]

Environment

  • make setup-cloud-localterra (factory LCD pairs[0]=EMBER/CORAL, pairs[1]=EMBER/JADE)
  • Indexer tmux indexer-dev :3001, Vite make dev :5173
  • Addresses from current factory LCD deploy output (not stale indexer-only rows)

No repo changes from this verification pass.

## Agent verification — PASS (close #357) Verified fix on `main` (`1aeaf2c`, HEAD `69c0aa1`): `shouldAutoPickDefaultTradePair` gates default-pick to bare `/trade` only; route→state sync owns valid known `:pairAddr` segments. ### Checklist | Item | Result | How | |------|--------|-----| | Fresh tab: `/trade/<non-default factory pair>` stays after pairs load | **PASS** | Playwright on LocalTerra + `make dev`; EMBER/JADE (`terra1nc84…6cyg`) URL stable after factory resolve | | Pair search selector: non-default pick keeps URL + workspace | **PASS** | Playwright selector click → EMBER/JADE; risk ack pre-seeded (`cl8y-dex-risk-ack`) | | Bare `/trade` auto-navigates to first factory pair | **PASS** | Playwright → EMBER/CORAL (`terra146yp…mjav`) | | Invalid deep link shows notice + clears URL | **PASS** | Playwright `/trade/lilwayne%20babyyy` | | Unknown valid-format deep link shows pair-not-found notice | **SKIP (#358)** | Playwright: no notice (Layout Outlet remount); filed separately as #358 — not a #357 regression | | Unit: `TradePage.test.tsx` + `tradePairRoute.test.ts` | **PASS** | 28/28 locally | | Regression test discriminates pre-fix | **PASS** | `keeps non-default deep link…` fails on `1aeaf2c^` with snap-back to `pairs[0]` | ### Environment - `make setup-cloud-localterra` (factory LCD pairs[0]=EMBER/CORAL, pairs[1]=EMBER/JADE) - Indexer tmux `indexer-dev` :3001, Vite `make dev` :5173 - Addresses from current factory LCD deploy output (not stale indexer-only rows) No repo changes from this verification pass.
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-06-11 15:19:09 +00:00
PlasticDigits commented 2026-06-11 15:27:02 +00:00 (Migrated from gitlab.com)

mentioned in merge request !865

mentioned in merge request !865
PlasticDigits commented 2026-08-27 00:20:54 +00:00 (Migrated from gitlab.com)

mentioned in issue #680

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