[ux] Off-chain error humanization — wallet modal / Tiers / Trade / Swap indexer paths surface raw errors #145

Closed
opened 2026-05-07 06:38:58 +00:00 by Brouie · 8 comments
Brouie commented 2026-05-07 06:38:58 +00:00 (Migrated from gitlab.com)

@totdking — visual / UX finding from source-level audit, assigning to you. raw error strings from non-contract layers (wallet extensions, fetch errors, indexer queries) bubble through to UI without humanization. companion to #134 which scopes contract-side errors only.

Scope

#134 covers contract-side error humanization via humanizeTerraTxError.ts. but the off-chain layer (wallet extension errors, indexer fetch errors, mutation errors) does NOT route through the same classifier — these surface raw to users.

HIGH — raw error pass-through, off-chain layer

frontend-dapp/src/components/wallet/WalletModal.tsx:65

{error && <div className="alert-error mb-4">{error}</div>}

error is read from the wallet store (useWallet.ts:45) which sets it to err.message or 'Connection failed'. raw surfaces:

  • Failed to enable Keplr (User rejected the request)
  • Failed to connect Leap: Leap extension is not installed (this is the same surface visible in #139's screenshot)
  • WalletConnect succeeded but ...
  • generic extension errors that include stack traces or internal SDK references

frontend-dapp/src/pages/TiersPage.tsx:255-258

{(registerMutation.isError || deregisterMutation.isError) && (
  <div className="mt-3 alert-error !text-xs">
    {registerMutation.error?.message || deregisterMutation.error?.message}
  </div>
)}

mutation errors rendered raw. these mutation hooks may NOT route through transactions.ts for all branches — verify before fixing. likely surfaces include signing rejected, fetch failed, network unreachable.

frontend-dapp/src/pages/TiersPage.tsx:276-280

{tiersQuery.isError && (
  <RetryError
    message={`Failed to load tiers: ${tiersQuery.error?.message ?? 'Unknown error'}`}
    onRetry={() => void tiersQuery.refetch()}
  />
)}

concatenates raw fetch error message into the displayed string. surfaces include NetworkError when attempting to fetch resource, AbortError, Failed to fetch — none humanized.

frontend-dapp/src/pages/TradePage.tsx:125 and :162

<RetryError message={(indexerPairQuery.error as Error).message} onRetry={() => indexerPairQuery.refetch()} />

passed straight to RetryError without humanization. same fetch-error class as Tiers above. duplicated at two render sites in the same file.

frontend-dapp/src/pages/SwapPage.tsx:167-168

setIndexerRouteError(e instanceof Error ? e.message : String(e))

stored raw, then rendered at lines 745-747 in the route comparison panel.

POLISH

frontend-dapp/src/App.tsx:95, 114 — ErrorBoundary

{this.state.error?.message || 'An unexpected error occurred'}

good fallback string but a render-time error stack message is surfaced directly to the user. consider hiding the raw message behind a "details" disclosure (<details><summary> pattern) or a "report this" affordance.

frontend-dapp/src/components/ui/TxResultAlert.tsx:14-16 — architectural risk

<div ...>{message}</div>

renders {message} directly with no further processing. architectural concern: humanization must happen at every CALL SITE, not in this component — there is currently no enforcement that callers pass humanized strings. any new caller will silently regress on copy. consider:

  • adding a humanize step inside TxResultAlert itself, OR
  • a lint rule that flags raw error.message passed to TxResultAlert's message prop

How to verify on local stack

For each site listed above:

  1. Trigger the failure path (wallet rejection, indexer down, etc.). Easy ways:
    • WalletModal: try connecting an uninstalled wallet (already covered in #139)
    • TiersPage: kill the indexer (make indexer-stop or just stop the container) then refresh /tiers
    • TradePage: same — kill indexer, navigate to /trade/{pair}
    • SwapPage: thin-liquidity pair to trigger indexer route mismatch (also exercised in #134's repro)
  2. Observe the rendered error string
  3. Confirm it is the raw underlying error, not a humanized version

Suggestion

extend humanizeTerraTxError.ts (or factor a sibling humanizeFetchError.ts / humanizeWalletError.ts) and route ALL the sites above through the same classifier shape that #134 just landed for contract errors.

priority order:

  1. WalletModal (highest user impact — first interaction)
  2. TradePage indexer error (most visible — Trade is a primary surface)
  3. TiersPage register/deregister + load
  4. SwapPage indexer route error
  5. ErrorBoundary polish
  6. TxResultAlert architectural fix (lint or component-level humanizer)

Severity

P2 — UX quality. doesn't block functionality but exposes retail users to internal SDK/network jargon. compounds with the trust-signal gaps in #139/#140 where the connect modal is the user's first impression.

Cross-reference

  • DEX #134 — humanize contract-side errors (sibling, just landed for Max spread)
  • DEX #133 — visual QA umbrella
  • DEX #139 — connect modal raw error rendered (line 65 of WalletModal — same surface this ticket scopes)

cc @PlasticDigits

@totdking — visual / UX finding from source-level audit, assigning to you. raw error strings from non-contract layers (wallet extensions, fetch errors, indexer queries) bubble through to UI without humanization. companion to #134 which scopes contract-side errors only. ## Scope #134 covers contract-side error humanization via `humanizeTerraTxError.ts`. but the off-chain layer (wallet extension errors, indexer fetch errors, mutation errors) does NOT route through the same classifier — these surface raw to users. ## HIGH — raw error pass-through, off-chain layer ### `frontend-dapp/src/components/wallet/WalletModal.tsx:65` ```tsx {error && <div className="alert-error mb-4">{error}</div>} ``` `error` is read from the wallet store (`useWallet.ts:45`) which sets it to `err.message` or `'Connection failed'`. raw surfaces: - `Failed to enable Keplr (User rejected the request)` - `Failed to connect Leap: Leap extension is not installed` (this is the same surface visible in #139's screenshot) - `WalletConnect succeeded but ...` - generic extension errors that include stack traces or internal SDK references ### `frontend-dapp/src/pages/TiersPage.tsx:255-258` ```tsx {(registerMutation.isError || deregisterMutation.isError) && ( <div className="mt-3 alert-error !text-xs"> {registerMutation.error?.message || deregisterMutation.error?.message} </div> )} ``` mutation errors rendered raw. these mutation hooks may NOT route through `transactions.ts` for all branches — verify before fixing. likely surfaces include `signing rejected`, `fetch failed`, `network unreachable`. ### `frontend-dapp/src/pages/TiersPage.tsx:276-280` ```tsx {tiersQuery.isError && ( <RetryError message={`Failed to load tiers: ${tiersQuery.error?.message ?? 'Unknown error'}`} onRetry={() => void tiersQuery.refetch()} /> )} ``` concatenates raw fetch error message into the displayed string. surfaces include `NetworkError when attempting to fetch resource`, `AbortError`, `Failed to fetch` — none humanized. ### `frontend-dapp/src/pages/TradePage.tsx:125` and `:162` ```tsx <RetryError message={(indexerPairQuery.error as Error).message} onRetry={() => indexerPairQuery.refetch()} /> ``` passed straight to RetryError without humanization. same fetch-error class as Tiers above. duplicated at two render sites in the same file. ### `frontend-dapp/src/pages/SwapPage.tsx:167-168` ```tsx setIndexerRouteError(e instanceof Error ? e.message : String(e)) ``` stored raw, then rendered at lines 745-747 in the route comparison panel. ## POLISH ### `frontend-dapp/src/App.tsx:95, 114` — ErrorBoundary ```tsx {this.state.error?.message || 'An unexpected error occurred'} ``` good fallback string but a render-time error stack message is surfaced directly to the user. consider hiding the raw message behind a "details" disclosure (`<details><summary>` pattern) or a "report this" affordance. ### `frontend-dapp/src/components/ui/TxResultAlert.tsx:14-16` — architectural risk ```tsx <div ...>{message}</div> ``` renders `{message}` directly with no further processing. **architectural concern**: humanization must happen at every CALL SITE, not in this component — there is currently no enforcement that callers pass humanized strings. any new caller will silently regress on copy. consider: - adding a humanize step inside `TxResultAlert` itself, OR - a lint rule that flags raw `error.message` passed to `TxResultAlert`'s `message` prop ## How to verify on local stack For each site listed above: 1. Trigger the failure path (wallet rejection, indexer down, etc.). Easy ways: - **WalletModal**: try connecting an uninstalled wallet (already covered in #139) - **TiersPage**: kill the indexer (`make indexer-stop` or just stop the container) then refresh `/tiers` - **TradePage**: same — kill indexer, navigate to `/trade/{pair}` - **SwapPage**: thin-liquidity pair to trigger indexer route mismatch (also exercised in #134's repro) 2. Observe the rendered error string 3. Confirm it is the raw underlying error, not a humanized version ## Suggestion extend `humanizeTerraTxError.ts` (or factor a sibling `humanizeFetchError.ts` / `humanizeWalletError.ts`) and route ALL the sites above through the same classifier shape that #134 just landed for contract errors. priority order: 1. WalletModal (highest user impact — first interaction) 2. TradePage indexer error (most visible — Trade is a primary surface) 3. TiersPage register/deregister + load 4. SwapPage indexer route error 5. ErrorBoundary polish 6. TxResultAlert architectural fix (lint or component-level humanizer) ## Severity P2 — UX quality. doesn't block functionality but exposes retail users to internal SDK/network jargon. compounds with the trust-signal gaps in #139/#140 where the connect modal is the user's first impression. ## Cross-reference - DEX #134 — humanize contract-side errors (sibling, just landed for Max spread) - DEX #133 — visual QA umbrella - DEX #139 — connect modal raw error rendered (line 65 of WalletModal — same surface this ticket scopes) cc @PlasticDigits
Brouie commented 2026-05-07 06:38:58 +00:00 (Migrated from gitlab.com)

assigned to @totdking

assigned to @totdking
PlasticDigits commented 2026-05-09 05:02:20 +00:00 (Migrated from gitlab.com)

unassigned @totdking

unassigned @totdking
PlasticDigits commented 2026-05-09 05:02:57 +00:00 (Migrated from gitlab.com)

See comment on #144 - bugfix not a verification issue so should not be assigned to totdking

See comment on #144 - bugfix not a verification issue so should not be assigned to totdking
PlasticDigits commented 2026-05-09 05:22:49 +00:00 (Migrated from gitlab.com)

mentioned in commit 435b1cc0be

mentioned in commit 435b1cc0be88edcaf4142571e3efa4c3e965c20c
PlasticDigits commented 2026-05-09 05:23:17 +00:00 (Migrated from gitlab.com)

Implementation landed on `main` (435b1cc)

Summary: Central off-chain error humanization (`humanizeUserFacingError.ts`) chains existing tx/LCD rules from `humanizeTerraTxError.ts` with new wallet/fetch/indexer patterns in `humanizeOffChainError.ts`. `RetryError` and `TxResultAlert` (error) apply humanization automatically; wallet connect stores humanized copy; Swap/Tiers/Trade call sites updated where needed; ErrorBoundary shows friendly copy plus collapsible Technical details.

Docs: `docs/frontend.md` § User-facing errors, `skills/AGENTS_FRONTEND_USER_ERRORS.md`, `docs/README.md` agent crosslink.

Verification checklist

  • Wallet modal: reject connection or use missing extension → message is short retail copy (not raw SDK string).
  • Trade: indexer error that is not “unavailable” banner → Retry panel shows humanized network/indexer copy (not raw `Failed to fetch` alone).
  • Tiers: stop indexer, load `/tiers` → retry panel humanized; register/deregister failure → inline alert humanized.
  • Swap: indexer route solver failure path → route comparison error humanized.
  • Tx failures: swap/pool/limit alerts still readable; contract messages still hit #134 patterns where applicable.
  • ErrorBoundary: trigger a route error → friendly line + Technical details disclosure shows raw message.
  • Unit: `npm run test:unit -- src/utils/tests/humanizeUserFacingError.test.ts` passes.

/cc @brouie — please verify UX copy on the checklist above when you have a moment. Leaving this issue open until QA signs off.

## Implementation landed on \`main\` ([435b1cc](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/commit/435b1cc)) **Summary:** Central **off-chain** error humanization ([\`humanizeUserFacingError.ts\`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/utils/humanizeUserFacingError.ts)) chains existing **tx/LCD** rules from [\`humanizeTerraTxError.ts\`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/utils/humanizeTerraTxError.ts) with new wallet/fetch/indexer patterns in [\`humanizeOffChainError.ts\`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/utils/humanizeOffChainError.ts). **\`RetryError\`** and **\`TxResultAlert\` (error)** apply humanization automatically; **wallet connect** stores humanized copy; **Swap/Tiers/Trade** call sites updated where needed; **ErrorBoundary** shows friendly copy plus collapsible **Technical details**. **Docs:** [\`docs/frontend.md\` § User-facing errors](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/frontend.md#user-facing-errors-humanization), [\`skills/AGENTS_FRONTEND_USER_ERRORS.md\`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/skills/AGENTS_FRONTEND_USER_ERRORS.md), [\`docs/README.md\`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/README.md) agent crosslink. ### Verification checklist - [ ] **Wallet modal:** reject connection or use missing extension → message is short retail copy (not raw SDK string). - [ ] **Trade:** indexer error that is *not* “unavailable” banner → **Retry** panel shows humanized network/indexer copy (not raw \`Failed to fetch\` alone). - [ ] **Tiers:** stop indexer, load \`/tiers\` → retry panel humanized; register/deregister failure → inline alert humanized. - [ ] **Swap:** indexer route solver failure path → route comparison error humanized. - [ ] **Tx failures:** swap/pool/limit alerts still readable; contract messages still hit **#134** patterns where applicable. - [ ] **ErrorBoundary:** trigger a route error → friendly line + **Technical details** disclosure shows raw message. - [ ] **Unit:** \`npm run test:unit -- src/utils/__tests__/humanizeUserFacingError.test.ts\` passes. /cc @brouie — please verify UX copy on the checklist above when you have a moment. Leaving this issue **open** until QA signs off.
PlasticDigits commented 2026-05-09 05:28:20 +00:00 (Migrated from gitlab.com)

mentioned in commit cad1efa258

mentioned in commit cad1efa258101c755618b55043dbc583d229e9a5
totdking commented 2026-05-28 11:09:14 +00:00 (Migrated from gitlab.com)

Verification checklist

  • Wallet modal: reject connection or use missing extension → message is short retail copy (not raw SDK string).
  • Trade: indexer error that is not “unavailable” banner → Retry panel shows humanized network/indexer copy (not raw `Failed to fetch` alone).
  • Tiers: stop indexer, load `/tiers` → retry panel humanized; register/deregister failure → inline alert humanized.
  • Swap: indexer route solver failure path → route comparison error humanized.
  • Tx failures: swap/pool/limit alerts still readable; contract messages still hit #134 patterns where applicable.
  • ErrorBoundary: trigger a route error → friendly line + Technical details disclosure shows raw message.
  • Unit: `npm run test:unit -- src/utils/tests/humanizeUserFacingError.test.ts` passes.

Issues noticed:

  1. The Unit test did not run becuase the actual file is located in src/utils/__tests__/ and not src/utils/tests so the test was run with the correct path and all unit tests passed with no errors or regression

Other than this, the issue is good to be closed @PlasticDigits @Brouie

### Verification checklist * [x] **Wallet modal:** reject connection or use missing extension → message is short retail copy (not raw SDK string). * [x] **Trade:** indexer error that is _not_ “unavailable” banner → **Retry** panel shows humanized network/indexer copy (not raw \`Failed to fetch\` alone). * [x] **Tiers:** stop indexer, load \`/tiers\` → retry panel humanized; register/deregister failure → inline alert humanized. * [x] **Swap:** indexer route solver failure path → route comparison error humanized. * [x] **Tx failures:** swap/pool/limit alerts still readable; contract messages still hit **#134** patterns where applicable. * [x] **ErrorBoundary:** trigger a route error → friendly line + **Technical details** disclosure shows raw message. * [x] **Unit:** \`npm run test:unit -- src/utils/**tests**/humanizeUserFacingError.test.ts\` passes. ### Issues noticed: 1. The Unit test did not run becuase the actual file is located in `src/utils/__`**`tests__`**`/` and not `src/utils/`**`tests`** so the test was run with the correct path and all unit tests passed with no errors or regression Other than this, the issue is good to be closed @PlasticDigits @Brouie
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-05-28 12:14:57 +00:00
PlasticDigits commented 2026-08-30 02:48:10 +00:00 (Migrated from gitlab.com)

mentioned in issue #706

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