Swap stays on "Calculating..." and never returns a quote #484

Closed
opened 2026-07-12 17:47:08 +00:00 by amm.s.198231 · 15 comments
amm.s.198231 commented 2026-07-12 17:47:08 +00:00 (Migrated from gitlab.com)

Swap stays on "Calculating..." and never returns a quote

What broke: The Swap page never calculates the output amount. It remains stuck on "Calculating..." indefinitely.

Steps to reproduce:

  1. Open https://dex.cl8y.com using the Keplr mobile browser on Android.
  2. Connect a Terra Classic wallet.
  3. Select JADE as the input token and RUBY as the output token.
  4. Enter an amount (e.g. 22.5 JADE).
  5. Wait for the quote.

Expected behavior: The DEX should calculate and display the swap quote within a few seconds.

Actual behavior: The interface stays on "Calculating..." forever and no quote is returned. The swap button also remains in the "CALCULATING..." state.

Environment:

Device: Android

Wallet: Keplr Mobile

Browser: Keplr in-app browser

Screenshot:

Screenshot_٢٠٢٦٠٧١٢_٢٠٤١١٧.jpg{width=346 height=600}

Swap stays on "Calculating..." and never returns a quote What broke: The Swap page never calculates the output amount. It remains stuck on "Calculating..." indefinitely. Steps to reproduce: 1. Open https://dex.cl8y.com using the Keplr mobile browser on Android. 2. Connect a Terra Classic wallet. 3. Select JADE as the input token and RUBY as the output token. 4. Enter an amount (e.g. 22.5 JADE). 5. Wait for the quote. Expected behavior: The DEX should calculate and display the swap quote within a few seconds. Actual behavior: The interface stays on "Calculating..." forever and no quote is returned. The swap button also remains in the "CALCULATING..." state. Environment: Device: Android Wallet: Keplr Mobile Browser: Keplr in-app browser Screenshot: ![Screenshot_٢٠٢٦٠٧١٢_٢٠٤١١٧.jpg](/uploads/24c1b439e3cfaa5ccdf81f4eb070d9da/Screenshot_%D9%A2%D9%A0%D9%A2%D9%A6%D9%A0%D9%A7%D9%A1%D9%A2_%D9%A2%D9%A0%D9%A4%D9%A1%D9%A1%D9%A7.jpg){width=346 height=600}
amm.s.198231 commented 2026-07-12 17:54:58 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
PlasticDigits commented 2026-07-13 01:03:56 +00:00 (Migrated from gitlab.com)

Requires investigation before implmenetation - to investigator agent, please provide relevant files, code, possible reasons, verification criteria, and an implementation plan

Requires investigation before implmenetation - to investigator agent, please provide relevant files, code, possible reasons, verification criteria, and an implementation plan
PlasticDigits commented 2026-07-13 09:16:07 +00:00 (Migrated from gitlab.com)

Investigation findings (#484)

User report (Keplr Android in-app browser): JADE → RUBY with amount 22.5 stays on Calculating... (receive field + CTA) indefinitely.

What “Calculating...” actually means

Both surfaces are driven by the React Query sim query on Swap:

UI Condition Code
Swap button simQuery.isLoading || simQuoteStale SwapPage.tsx (~919–921)
You Receive simQuery.isFetching SwapPage.tsx (~1324–1327)

simQuoteStale (isSubmitQuoteStale in quoteDebounce.ts) is true whenever isFetching is true (also debounce / placeholder / hybrid snapshot mismatch). So any long or overlapping fetch keeps both the button and the output on Calculating.

There is no wallet gate on the quote itself (enabled: hasRoute && positive amount), but a connected wallet does pass trader= into indexer + LCD sims (#245).

JADE → RUBY is not a direct pair

Mainnet soft-launch has no JADE/RUBY pair. Tokens are CW20s:

  • JADE: terra1ejq3mjjgnklpa3pg4jterlfwsny055gpmcjf3fz0ev3ueajnzeysz6xxgr
  • RUBY: terra1fga508hzx8dd7x8q4uhm6mdhkqv6fxrtsea3r27smdqmv5k2jgxq5zk9fc

Quote path is the CW20 multi-hop branch in SwapPage.tsx (~479–527):

  1. GET /api/v1/route/solve (getRouteSolve, 15s client timeout + 1 retry)
  2. enrichSwapOperationsWithHopMinReturns — ~2 LCD calls per hop
  3. simulateMultiHopSwap — router LCD
  4. preflightSwapRouteSpread — another ~2 LCD calls per hop

directHybridQuote.ts is not on this path (direct-pair only).

Production probe (this investigation)

curl -sS -w 'time=%{time_total}\n' --max-time 45 \
  'https://indexer.dex.cl8y.com/api/v1/route/solve?token_in=<JADE>&token_out=<RUBY>&amount_in=22500000'

Result just now:

  • HTTP 200 in ~24.3s
  • 4 hops / 4 router_operations
  • estimated_amount_out: 8339 (~0.008339 RUBY for 22.5 JADE)
  • lcd_hybrid_queries: 357
  • Path: JADE → … → … → … → RUBY (intermediate_tokens length 5)

Frontend indexer client timeout is FETCH_TIMEOUT_MS = 15_000 (indexer/client.ts). A ~24s solve exceeds the client abort, so the browser often times out, falls through to client BFS + more LCD work, while React Query still has refetchInterval: 10_000 on simQuery (SwapPage.tsx:581) and the queryFn does not take an AbortSignal for cancellation of superseded runs.

Likely root cause (ranked)

  1. Highest — refetch pile-up on a slow multi-hop quote
    JADE→RUBY quote work routinely exceeds 10–15s (indexer alone measured ~24s). With refetchInterval: 10_000 and no query cancellation, overlapping fetches keep isFetching === true almost continuously → perpetual Calculating on button + receive. Keplr mobile WebView makes sequential LCD fan-out worse.

  2. High — receive UI hides prior quote while refetching
    Output uses simQuery.isFetching even when keepPreviousData still has a prior simData, so every background refetch replaces the amount with Calculating.

  3. Medium — 15s indexer timeout vs slow global_v1 for this pair
    Timeout → catch → client BFS fallback doubles work; React Query retries (App.tsx) extend “Calculating” further before “Quote unavailable”.

  4. Medium — Keplr in-app browser amplification
    Same code path on desktop may eventually settle; mobile WebView + public LCD connection limits amplify hangs. Not a Keplr signing bug (quote is off-chain HTTP).

  5. Lower — debounce / hybrid settings flicker
    Unlikely for a stable 22.5 input unless IME/settings keep changing the sim key.

Not suspected as primary: amount parsing (22.5 → 22500000 at 6 decimals), missing CW20 eligibility, or CSP/CORS (those fail fast).

Relevant files

  • frontend-dapp/src/pages/SwapPage.tsx — sim query, Calculating gates, CW20 multi-hop orchestration
  • frontend-dapp/src/hooks/useSubmitAlignedSimQuote.ts / frontend-dapp/src/utils/quoteDebounce.ts — simQuoteStale / isFetching
  • frontend-dapp/src/services/indexer/client.ts — getRouteSolve, 15s timeout
  • frontend-dapp/src/services/terraclassic/swapRoutePreflight.ts — per-hop LCD enrich/preflight
  • frontend-dapp/src/services/terraclassic/router.ts / queries.ts — multi-hop sim + 10s LCD timeout
  • frontend-dapp/src/components/trade/TradeMarketOrderPanel.tsx — same refetchInterval: 10_000 pattern
  • indexer route solver (route_solver) — hybrid global_v1 cost for distant pairs
  • deployments/mainnet-soft-launch/deploy-trace.md — pair graph (no direct JADE/RUBY)

Verification checklist

  • Reproduce on Keplr Android in-app browser: JADE → RUBY, 22.5, wallet connected; note whether Calculating exceeds ~2 minutes vs eventual Quote unavailable
  • Remote-debug WebView (chrome://inspect): confirm overlapping GET …/api/v1/route/solve every ~10s while prior request still pending; count LCD /smart/ bursts
  • Same pair/amount on desktop Chrome — if desktop settles and mobile does not, confirms WebView/latency amplification
  • Curl indexer latency for this pair (expect often >15s today); note hop count and lcd_hybrid_queries
  • Disconnect wallet / omit trader and retest — isolates fee-tier/indexer cache path (#245)
  • Confirm whether UI ever flips to Quote unavailable + retry (retry/backoff) vs true infinite Calculating (refetch overlap)
  • After any fix: quote settles or shows error within a hard SLA; never infinite Calculating; Trade market panel updated if same pattern applies

Suggested fix plan (implementation order)

  1. Instrument timing in simQuery.queryFn (indexer / enrich / router / preflight) and confirm refetch overlap on mobile.
  2. Stop refetch pile-up: skip/lengthen refetchInterval while fetchStatus === 'fetching' (or raise multi-hop interval).
  3. Show stale quote during background refetch for the receive field; keep submit blocked via alignment helpers without forever-pulse UI.
  4. Pass React Query signal through indexer + LCD fetches so superseded quotes abort.
  5. Reduce LCD fan-out (cache getPair per quote; avoid duplicate enrich+preflight sims where safe).
  6. Hard-cap total quote time → clear Quote unavailable + retry.
  7. Indexer follow-up if JADE↔RUBY hybrid solve routinely >15s / 4 hops / hundreds of LCD queries — cache hot paths or tighten search budget for distant pairs.
  8. Apply the same refetch/stale-display fixes to Trade market if still sharing the pattern.

Follow-ups (only if relevant after quote works)

  • Submit gas headroom for long JADE/RUBY-family multi-hop paths (ROUTER_SWAP_OPS_MIN_GAS_PER_HOP / known EMBER→JADE→RUBY OOG notes in constants.ts) — separate from the hang, same token family.
  • Very small estimated_amount_out for 22.5 JADE may warrant a product check on liquidity/route quality once quoting settles.

Bottom line: Most consistent with a UI/refetch state machine issue layered on a legitimately expensive JADE→RUBY multi-hop quote (prod indexer ~24s, 4 hops, 357 LCD hybrid queries), amplified on Keplr mobile — not a missing route or amount bug.

## Investigation findings (#484) User report (Keplr Android in-app browser): JADE → RUBY with amount `22.5` stays on **Calculating...** (receive field + CTA) indefinitely. ### What “Calculating...” actually means Both surfaces are driven by the React Query sim query on Swap: | UI | Condition | Code | |----|-----------|------| | Swap button | `simQuery.isLoading \|\| simQuoteStale` | `SwapPage.tsx` (~919–921) | | You Receive | `simQuery.isFetching` | `SwapPage.tsx` (~1324–1327) | `simQuoteStale` (`isSubmitQuoteStale` in `quoteDebounce.ts`) is **true whenever `isFetching` is true** (also debounce / placeholder / hybrid snapshot mismatch). So any long or overlapping fetch keeps both the button and the output on Calculating. There is **no wallet gate** on the quote itself (`enabled: hasRoute && positive amount`), but a connected wallet does pass `trader=` into indexer + LCD sims (#245). ### JADE → RUBY is not a direct pair Mainnet soft-launch has **no JADE/RUBY pair**. Tokens are CW20s: - JADE: `terra1ejq3mjjgnklpa3pg4jterlfwsny055gpmcjf3fz0ev3ueajnzeysz6xxgr` - RUBY: `terra1fga508hzx8dd7x8q4uhm6mdhkqv6fxrtsea3r27smdqmv5k2jgxq5zk9fc` Quote path is the CW20 multi-hop branch in `SwapPage.tsx` (~479–527): 1. `GET /api/v1/route/solve` (`getRouteSolve`, 15s client timeout + 1 retry) 2. `enrichSwapOperationsWithHopMinReturns` — ~2 LCD calls per hop 3. `simulateMultiHopSwap` — router LCD 4. `preflightSwapRouteSpread` — another ~2 LCD calls per hop `directHybridQuote.ts` is **not** on this path (direct-pair only). ### Production probe (this investigation) ```bash curl -sS -w 'time=%{time_total}\n' --max-time 45 \ 'https://indexer.dex.cl8y.com/api/v1/route/solve?token_in=<JADE>&token_out=<RUBY>&amount_in=22500000' ``` Result just now: - **HTTP 200 in ~24.3s** - **4 hops** / 4 `router_operations` - `estimated_amount_out`: `8339` (~0.008339 RUBY for 22.5 JADE) - `lcd_hybrid_queries`: **357** - Path: JADE → … → … → … → RUBY (`intermediate_tokens` length 5) Frontend indexer client timeout is **`FETCH_TIMEOUT_MS = 15_000`** (`indexer/client.ts`). A ~24s solve **exceeds the client abort**, so the browser often times out, falls through to client BFS + more LCD work, while React Query still has `refetchInterval: 10_000` on `simQuery` (`SwapPage.tsx:581`) and the `queryFn` does **not** take an AbortSignal for cancellation of superseded runs. ### Likely root cause (ranked) 1. **Highest — refetch pile-up on a slow multi-hop quote** JADE→RUBY quote work routinely exceeds 10–15s (indexer alone measured ~24s). With `refetchInterval: 10_000` and no query cancellation, overlapping fetches keep `isFetching === true` almost continuously → perpetual Calculating on button + receive. Keplr mobile WebView makes sequential LCD fan-out worse. 2. **High — receive UI hides prior quote while refetching** Output uses `simQuery.isFetching` even when `keepPreviousData` still has a prior `simData`, so every background refetch replaces the amount with Calculating. 3. **Medium — 15s indexer timeout vs slow `global_v1` for this pair** Timeout → catch → client BFS fallback doubles work; React Query retries (App.tsx) extend “Calculating” further before “Quote unavailable”. 4. **Medium — Keplr in-app browser amplification** Same code path on desktop may eventually settle; mobile WebView + public LCD connection limits amplify hangs. Not a Keplr signing bug (quote is off-chain HTTP). 5. **Lower — debounce / hybrid settings flicker** Unlikely for a stable `22.5` input unless IME/settings keep changing the sim key. Not suspected as primary: amount parsing (`22.5` → `22500000` at 6 decimals), missing CW20 eligibility, or CSP/CORS (those fail fast). ### Relevant files - `frontend-dapp/src/pages/SwapPage.tsx` — sim query, Calculating gates, CW20 multi-hop orchestration - `frontend-dapp/src/hooks/useSubmitAlignedSimQuote.ts` / `frontend-dapp/src/utils/quoteDebounce.ts` — `simQuoteStale` / `isFetching` - `frontend-dapp/src/services/indexer/client.ts` — `getRouteSolve`, 15s timeout - `frontend-dapp/src/services/terraclassic/swapRoutePreflight.ts` — per-hop LCD enrich/preflight - `frontend-dapp/src/services/terraclassic/router.ts` / `queries.ts` — multi-hop sim + 10s LCD timeout - `frontend-dapp/src/components/trade/TradeMarketOrderPanel.tsx` — same `refetchInterval: 10_000` pattern - `indexer` route solver (`route_solver`) — hybrid `global_v1` cost for distant pairs - `deployments/mainnet-soft-launch/deploy-trace.md` — pair graph (no direct JADE/RUBY) ### Verification checklist - [ ] Reproduce on Keplr Android in-app browser: JADE → RUBY, `22.5`, wallet connected; note whether Calculating exceeds ~2 minutes vs eventual Quote unavailable - [ ] Remote-debug WebView (`chrome://inspect`): confirm overlapping `GET …/api/v1/route/solve` every ~10s while prior request still pending; count LCD `/smart/` bursts - [ ] Same pair/amount on desktop Chrome — if desktop settles and mobile does not, confirms WebView/latency amplification - [ ] Curl indexer latency for this pair (expect often **>15s** today); note hop count and `lcd_hybrid_queries` - [ ] Disconnect wallet / omit `trader` and retest — isolates fee-tier/indexer cache path (#245) - [ ] Confirm whether UI ever flips to **Quote unavailable** + retry (retry/backoff) vs true infinite Calculating (refetch overlap) - [ ] After any fix: quote settles or shows error within a hard SLA; never infinite Calculating; Trade market panel updated if same pattern applies ### Suggested fix plan (implementation order) 1. Instrument timing in `simQuery.queryFn` (indexer / enrich / router / preflight) and confirm refetch overlap on mobile. 2. **Stop refetch pile-up:** skip/lengthen `refetchInterval` while `fetchStatus === 'fetching'` (or raise multi-hop interval). 3. **Show stale quote during background refetch** for the receive field; keep submit blocked via alignment helpers without forever-pulse UI. 4. Pass React Query `signal` through indexer + LCD fetches so superseded quotes abort. 5. Reduce LCD fan-out (cache `getPair` per quote; avoid duplicate enrich+preflight sims where safe). 6. Hard-cap total quote time → clear **Quote unavailable** + retry. 7. Indexer follow-up if JADE↔RUBY hybrid solve routinely >15s / 4 hops / hundreds of LCD queries — cache hot paths or tighten search budget for distant pairs. 8. Apply the same refetch/stale-display fixes to Trade market if still sharing the pattern. ### Follow-ups (only if relevant after quote works) - Submit gas headroom for long JADE/RUBY-family multi-hop paths (`ROUTER_SWAP_OPS_MIN_GAS_PER_HOP` / known EMBER→JADE→RUBY OOG notes in `constants.ts`) — separate from the hang, same token family. - Very small `estimated_amount_out` for 22.5 JADE may warrant a product check on liquidity/route quality once quoting settles. **Bottom line:** Most consistent with a **UI/refetch state machine issue** layered on a **legitimately expensive JADE→RUBY multi-hop quote** (prod indexer ~24s, 4 hops, 357 LCD hybrid queries), amplified on Keplr mobile — not a missing route or amount bug.
PlasticDigits commented 2026-07-13 09:33:04 +00:00 (Migrated from gitlab.com)

mentioned in commit 6b214dd161

mentioned in commit 6b214dd16178bfddec1dd09c79aa86c888eceeec
PlasticDigits commented 2026-07-13 09:33:33 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1020

mentioned in merge request !1020
PlasticDigits commented 2026-07-13 10:29:21 +00:00 (Migrated from gitlab.com)

mentioned in commit 6873d06cd5

mentioned in commit 6873d06cd5c3d7ced1c34c1053757cf9ea1b83c2
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-07-13 10:29:22 +00:00
PlasticDigits commented 2026-07-13 10:33:12 +00:00 (Migrated from gitlab.com)

mentioned in issue #485

mentioned in issue #485
PlasticDigits commented 2026-07-13 10:33:12 +00:00 (Migrated from gitlab.com)

marked as related to #485

marked as related to #485
PlasticDigits commented 2026-07-13 10:57:31 +00:00 (Migrated from gitlab.com)

mentioned in commit 24594c5f4c

mentioned in commit 24594c5f4c2fb45268959371e7f646c3be54914b
PlasticDigits commented 2026-07-13 10:57:33 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1021

mentioned in merge request !1021
PlasticDigits commented 2026-07-15 04:06:40 +00:00 (Migrated from gitlab.com)

mentioned in issue #496

mentioned in issue #496
PlasticDigits commented 2026-07-25 04:32:52 +00:00 (Migrated from gitlab.com)

mentioned in commit 70937b9c1f

mentioned in commit 70937b9c1ffc124db473dc594cc18406b57173dd
PlasticDigits commented 2026-07-25 04:32:58 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1031

mentioned in merge request !1031
PlasticDigits commented 2026-08-22 11:02:34 +00:00 (Migrated from gitlab.com)

mentioned in issue #595

mentioned in issue #595
PlasticDigits commented 2026-08-28 09:24:17 +00:00 (Migrated from gitlab.com)

mentioned in issue #694

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