fix(frontend): Your History (this pair) missing amounts + CSV download fails silently #479

Closed
opened 2026-07-12 08:09:59 +00:00 by PlasticDigits · 4 comments
PlasticDigits commented 2026-07-12 08:09:59 +00:00 (Migrated from gitlab.com)

Summary

User report (mobile QA, Keplr connected): Your history (this pair) shows Time / Side / Price (Fee / Tx when scrolled) but no amount columns, and Download CSV appears to do nothing. Investigation shows both are real gaps in the same panel — not Keplr signing related.

Related: #163 (history feature), #352 (mobile horizontal scroll only), #432 (CSV formula injection — keep intact), skill skills/AGENTS_FRONTEND_ORDER_HISTORY.md.


Current codebase

Panel & mounts

Piece Behavior today
WalletIndexerHistoryPanel.tsx Renders Your history (this pair) with Swaps / Limit fills / Limit cancellations
/limits All three sections when wallet + pair selected (LimitOrdersPage.tsx)
/trade Swaps-only section inside disclosure (TradePage.tsx)
Indexer client fetchTraderHistoryCsv, downloadTextAsFile
Indexer CSV text_csv.rs — swaps CSV already includes offer_amount,return_amount; fills include token0_amount,token1_amount
Public trades UI TradesTable.tsx already shows Amount in / Amount out — wallet pair history does not reuse it

Swaps table columns (UI)

Time | Side | Price | Fee | Tx — Side is {offer_asset} → {ask_asset} (symbols only). offer_amount / return_amount are never rendered even though IndexerTrade and GET /api/v1/traders/{addr}/trades return them.

Limit fills table columns (UI)

Time | Order | Side | Price | Commission | Tx — token0_amount / token1_amount not rendered though present on IndexerLimitFill.

CSV download path

  1. Button → HistoryBlock.onCsv → downloadCsv → fetchTraderHistoryCsv(...?format=csv&pair=...) → downloadTextAsFile (blob + <a download>).
  2. No catch in onCsv — failures become unhandled rejections; no toast / inline error.
  3. Button only disabled={pending} — not gated on Keplr beyond panel visibility (walletAddress + terra1 pair).
  4. CSV does not use the wallet for signing; Keplr only supplies the address used in the URL path. If the table loads, wallet connection is sufficient for CSV.
  5. E2E wallet-history-163.spec.ts asserts the button is visible, never clicks it or asserts a download.
  6. fetchTraderHistoryCsv has no retry (unlike fetchJson); client requests limit: 500 while server caps at 200.

Mobile note

Tables use overflow-x-auto + min-w-[28rem] (#352). Narrow viewports clip left columns; users may only see Price / Fee / Tx unless they scroll — which can make the missing-amount gap feel worse, but amounts were never present.


Why a new implementation is needed

  1. Amounts — Traders cannot tell how large each swap/fill was from the history view. Price alone is insufficient for reconciling trades, taxes, or support. Data is already indexed and exported; the UI omission is the gap.
  2. CSV — Export is advertised in the panel copy (“CSV export uses the same filters as this view”) but failures are silent, so mobile users reasonably assume the feature is broken (or blame Keplr). Soft-launch support needs a reliable export or clear error feedback.
  3. Parity — Public/portfolio TradesTable shows amount columns; wallet pair history should not be a weaker surface for the user’s own trades.

Constraints / guardrails

  1. Do not change indexer schema for amounts — reuse existing offer_amount / return_amount (swaps) and token0_amount / token1_amount (fills). Prefer frontend-only for the amount columns.
  2. CSV formula injection — keep csv_escape_cell / SEC-F12 (#432) behavior; do not weaken escaping when touching export.
  3. Pair scope — history and CSV remain filtered to the selected pair (pair= query). Do not expand to global wallet history in this issue.
  4. No wallet signing for CSV — continue pure HTTP fetch + browser download; do not introduce Keplr prompts for export.
  5. Decimal display — amounts are raw chain integers in the API. Prefer human-readable formatting consistent with the rest of the dApp (formatTokenAmount / pair decimals when available). If aligning with TradesTable’s formatNum(raw) for short-term parity, document the choice; do not invent a third format.
  6. Preserve mobile scroll — adding columns must keep #352 horizontal scroll / data-testid="wallet-history-table-scroll" behavior; do not hide Amount behind a breakpoint without an accessible alternative.
  7. Cancellations — no amount fields in API; leave that table as Time / Order / Tx.
  8. CORS / VITE_INDEXER_URL — if table JSON loads, CSV uses the same origin; do not “fix” CSV by changing CORS unless a real cross-origin bug is reproduced. Prefer UX error surfacing + download path hardening first.
  9. Client limit: 500 vs server max 200 — either align the client to 200 or document clamp; do not silently imply 500 rows were exported.

Relevant files

  • frontend-dapp/src/components/trade/WalletIndexerHistoryPanel.tsx
  • frontend-dapp/src/components/trade/__tests__/WalletIndexerHistoryPanel.test.tsx
  • frontend-dapp/src/services/indexer/client.ts (fetchTraderHistoryCsv, downloadTextAsFile)
  • frontend-dapp/src/types/index.ts (IndexerTrade, IndexerLimitFill)
  • frontend-dapp/src/components/ui/TradesTable.tsx (amount column precedent)
  • frontend-dapp/src/utils/formatAmount.ts
  • frontend-dapp/e2e/wallet-history-163.spec.ts
  • indexer/src/api/text_csv.rs, indexer/src/api/traders.rs (verify only unless export bug is server-side)
  • skills/AGENTS_FRONTEND_ORDER_HISTORY.md, docs/frontend.md (wallet history section)

A. Amount columns (UI)

  1. Add Amount in / Amount out (or equivalent labels) to the swaps table from offer_amount / return_amount.
  2. Add fill size columns for limit fills from token0_amount / token1_amount (label clearly vs base/quote).
  3. Prefer reusing patterns from TradesTable (columns + formatting) or shared cell helpers to avoid drift.
  4. Optional: pass pair metadata into the panel for decimal-aware formatTokenAmount when symbols/decimals are known.
  5. Update unit tests for rendered headers/cells; update skill/docs so amounts are part of product intent.

B. CSV download reliability (same panel)

  1. Catch errors in HistoryBlock.onCsv and surface inline error or toast (same patterns as RetryError / existing alerts).
  2. Align client CSV limit with server max (200) or show that export is capped.
  3. Consider one retry on CSV fetch (parity with fetchJson).
  4. Hardening for mobile browsers if blob+<a download> fails in QA (e.g. fallback open/showSaveFilePicker only where supported — keep simple).
  5. Extend unit tests to mock fetch failure → visible error; extend E2E to click Download CSV and assert network format=csv success (and/or download event) when indexer is up — not visibility-only.

Acceptance criteria

  • Swaps (AMM) table shows human-readable offer and return amounts for each row (in addition to existing columns).
  • Limit fills (maker) table shows fill amounts from indexed token0_amount / token1_amount.
  • Limit cancellations unchanged (no fake amount column).
  • Amounts match indexer values for the same tx_hash / row id (spot-check vs JSON or CSV).
  • Mobile: amount columns reachable via existing horizontal scroll (or better responsive layout without regressing #352).
  • Download CSV: on success, browser receives a .csv file (or equivalent download) for swaps/fills/cancels as applicable.
  • Download CSV: on indexer/network failure, user sees a clear error; no silent no-op.
  • CSV still contains amount columns; formula-injection escaping (#432) still passes indexer tests.
  • No Keplr / wallet signature prompt required for CSV.
  • Docs/skill mention amount columns in wallet pair history.

Test plan (all paths)

Unit / component

  • WalletIndexerHistoryPanel renders Amount in/out (or chosen labels) when swap rows include offer_amount / return_amount.
  • Limit fills render amount cells when token0_amount / token1_amount present.
  • Empty / loading / error query states unchanged.
  • CSV success path still calls downloadTextAsFile with expected filename prefix (swaps- / limit-fills- / limit-cancellations-).
  • CSV failure path shows UI error and clears pending so the button is re-enabled.
  • Panel still returns null when wallet or pair address invalid.

Integration / indexer (regression)

  • GET /api/v1/traders/{addr}/trades?format=csv&pair=… returns header including offer_amount,return_amount.
  • Fills CSV still includes token0_amount,token1_amount.
  • Formula-injection unit tests in text_csv still pass.

E2E / manual

  • /limits + connected wallet + selected pair with known swaps: amounts visible; CSV download works (desktop Chrome).
  • /trade swaps-only history: amounts + CSV after expanding disclosure if collapsed.
  • Mobile viewport (~375px): scroll to see amount columns; CSV — verify success or visible error (not silent).
  • Keplr connected vs Simulated Wallet: both can download CSV once address is present (proves not Keplr-signing).
  • Indexer stopped / wrong VITE_INDEXER_URL: table and/or CSV show failure messaging.
  • Pair with no history: empty copy; CSV still downloads header-only or empty body without crashing.

Cross-browser smoke

  • Desktop Chrome, Firefox; mobile Safari / Chrome if available for blob download behavior.

Test plan (attack / hack / abuse vectors)

  • CSV injection — offer/ask symbols or amounts crafted with leading =, +, -, @ remain neutralized by server escaping (#432); do not strip escaping on the client.
  • Path / address abuse — malformed wallet or pair in URL must not enable arbitrary file write; download filename stays sanitized slug (existing replace(/[^a-zA-Z0-9]/g, '')).
  • Large export DoS — respect server limit max (200); client must not request unbounded rows; rapid repeated CSV clicks should not wedge UI (pending / disable).
  • Open redirect / XSS via tx or amount cells — amounts rendered as text; tx links still use getExplorerTxUrl + rel="noopener noreferrer".
  • CORS / cross-origin — CSV fetch remains same-origin as configured indexer; no credentials: 'include' unless already required elsewhere.
  • Content-Type sniffing — keep text/csv;charset=utf-8 blob type; do not execute CSV as script in-page.

Verification criteria

  1. On a LocalTerra or staging pair with ≥1 wallet swap, history shows non-empty amount cells matching indexer JSON for that row.
  2. Clicking Download CSV produces a file whose offer_amount/return_amount match the UI (modulo formatting) for the same txs.
  3. With indexer unreachable, CSV click shows an error within one interaction; DevTools shows the failed format=csv request; no unhandled rejection left unexplained.
  4. make test-frontend (or targeted Vitest) and existing trader CSV indexer tests pass; E2E history spec covers CSV click when chain/indexer available.
  5. Manual mobile check confirms amounts are discoverable and CSV is either successful or clearly failed — not “button does nothing.”
## Summary User report (mobile QA, Keplr connected): **Your history (this pair)** shows Time / Side / Price (Fee / Tx when scrolled) but **no amount columns**, and **Download CSV** appears to do nothing. Investigation shows both are real gaps in the same panel — not Keplr signing related. Related: [#163](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/163) (history feature), [#352](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/352) (mobile horizontal scroll only), [#432](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/432) (CSV formula injection — keep intact), skill [`skills/AGENTS_FRONTEND_ORDER_HISTORY.md`](skills/AGENTS_FRONTEND_ORDER_HISTORY.md). --- ## Current codebase ### Panel & mounts | Piece | Behavior today | |-------|----------------| | [`WalletIndexerHistoryPanel.tsx`](frontend-dapp/src/components/trade/WalletIndexerHistoryPanel.tsx) | Renders **Your history (this pair)** with Swaps / Limit fills / Limit cancellations | | `/limits` | All three sections when wallet + pair selected ([`LimitOrdersPage.tsx`](frontend-dapp/src/pages/LimitOrdersPage.tsx)) | | `/trade` | Swaps-only section inside disclosure ([`TradePage.tsx`](frontend-dapp/src/pages/TradePage.tsx)) | | Indexer client | [`fetchTraderHistoryCsv`](frontend-dapp/src/services/indexer/client.ts), [`downloadTextAsFile`](frontend-dapp/src/services/indexer/client.ts) | | Indexer CSV | [`text_csv.rs`](indexer/src/api/text_csv.rs) — swaps CSV **already includes** `offer_amount`,`return_amount`; fills include `token0_amount`,`token1_amount` | | Public trades UI | [`TradesTable.tsx`](frontend-dapp/src/components/ui/TradesTable.tsx) already shows **Amount in / Amount out** — wallet pair history does not reuse it | ### Swaps table columns (UI) `Time | Side | Price | Fee | Tx` — Side is `{offer_asset} → {ask_asset}` (symbols only). **`offer_amount` / `return_amount` are never rendered** even though `IndexerTrade` and `GET /api/v1/traders/{addr}/trades` return them. ### Limit fills table columns (UI) `Time | Order | Side | Price | Commission | Tx` — **`token0_amount` / `token1_amount` not rendered** though present on `IndexerLimitFill`. ### CSV download path 1. Button → `HistoryBlock.onCsv` → `downloadCsv` → `fetchTraderHistoryCsv(...?format=csv&pair=...)` → `downloadTextAsFile` (blob + `<a download>`). 2. **No `catch`** in `onCsv` — failures become unhandled rejections; **no toast / inline error**. 3. Button only `disabled={pending}` — not gated on Keplr beyond panel visibility (`walletAddress` + `terra1` pair). 4. **CSV does not use the wallet** for signing; Keplr only supplies the address used in the URL path. If the table loads, wallet connection is sufficient for CSV. 5. E2E [`wallet-history-163.spec.ts`](frontend-dapp/e2e/wallet-history-163.spec.ts) asserts the button is **visible**, never clicks it or asserts a download. 6. `fetchTraderHistoryCsv` has **no retry** (unlike `fetchJson`); client requests `limit: 500` while server caps at **200**. ### Mobile note Tables use `overflow-x-auto` + `min-w-[28rem]` (#352). Narrow viewports clip left columns; users may only see Price / Fee / Tx unless they scroll — which can make the missing-amount gap feel worse, but amounts were never present. --- ## Why a new implementation is needed 1. **Amounts** — Traders cannot tell how large each swap/fill was from the history view. Price alone is insufficient for reconciling trades, taxes, or support. Data is already indexed and exported; the UI omission is the gap. 2. **CSV** — Export is advertised in the panel copy (“CSV export uses the same filters as this view”) but failures are silent, so mobile users reasonably assume the feature is broken (or blame Keplr). Soft-launch support needs a reliable export or clear error feedback. 3. **Parity** — Public/portfolio `TradesTable` shows amount columns; wallet pair history should not be a weaker surface for the user’s own trades. --- ## Constraints / guardrails 1. **Do not change indexer schema** for amounts — reuse existing `offer_amount` / `return_amount` (swaps) and `token0_amount` / `token1_amount` (fills). Prefer frontend-only for the amount columns. 2. **CSV formula injection** — keep [`csv_escape_cell`](indexer/src/api/text_csv.rs) / SEC-F12 (#432) behavior; do not weaken escaping when touching export. 3. **Pair scope** — history and CSV remain filtered to the selected pair (`pair=` query). Do not expand to global wallet history in this issue. 4. **No wallet signing for CSV** — continue pure HTTP fetch + browser download; do not introduce Keplr prompts for export. 5. **Decimal display** — amounts are raw chain integers in the API. Prefer human-readable formatting consistent with the rest of the dApp (`formatTokenAmount` / pair decimals when available). If aligning with `TradesTable`’s `formatNum(raw)` for short-term parity, document the choice; do not invent a third format. 6. **Preserve mobile scroll** — adding columns must keep `#352` horizontal scroll / `data-testid="wallet-history-table-scroll"` behavior; do not hide Amount behind a breakpoint without an accessible alternative. 7. **Cancellations** — no amount fields in API; leave that table as Time / Order / Tx. 8. **CORS / `VITE_INDEXER_URL`** — if table JSON loads, CSV uses the same origin; do not “fix” CSV by changing CORS unless a real cross-origin bug is reproduced. Prefer UX error surfacing + download path hardening first. 9. **Client `limit: 500` vs server max 200** — either align the client to 200 or document clamp; do not silently imply 500 rows were exported. --- ## Relevant files - `frontend-dapp/src/components/trade/WalletIndexerHistoryPanel.tsx` - `frontend-dapp/src/components/trade/__tests__/WalletIndexerHistoryPanel.test.tsx` - `frontend-dapp/src/services/indexer/client.ts` (`fetchTraderHistoryCsv`, `downloadTextAsFile`) - `frontend-dapp/src/types/index.ts` (`IndexerTrade`, `IndexerLimitFill`) - `frontend-dapp/src/components/ui/TradesTable.tsx` (amount column precedent) - `frontend-dapp/src/utils/formatAmount.ts` - `frontend-dapp/e2e/wallet-history-163.spec.ts` - `indexer/src/api/text_csv.rs`, `indexer/src/api/traders.rs` (verify only unless export bug is server-side) - `skills/AGENTS_FRONTEND_ORDER_HISTORY.md`, `docs/frontend.md` (wallet history section) --- ## Recommended direction ### A. Amount columns (UI) 1. Add **Amount in** / **Amount out** (or equivalent labels) to the swaps table from `offer_amount` / `return_amount`. 2. Add fill size columns for limit fills from `token0_amount` / `token1_amount` (label clearly vs base/quote). 3. Prefer reusing patterns from `TradesTable` (columns + formatting) or shared cell helpers to avoid drift. 4. Optional: pass pair metadata into the panel for decimal-aware `formatTokenAmount` when symbols/decimals are known. 5. Update unit tests for rendered headers/cells; update skill/docs so amounts are part of product intent. ### B. CSV download reliability (same panel) 1. Catch errors in `HistoryBlock.onCsv` and surface **inline error or toast** (same patterns as `RetryError` / existing alerts). 2. Align client CSV `limit` with server max (200) or show that export is capped. 3. Consider one retry on CSV fetch (parity with `fetchJson`). 4. Hardening for mobile browsers if blob+`<a download>` fails in QA (e.g. fallback open/`showSaveFilePicker` only where supported — keep simple). 5. Extend unit tests to mock fetch failure → visible error; extend E2E to click Download CSV and assert network `format=csv` success (and/or download event) when indexer is up — not visibility-only. --- ## Acceptance criteria - [ ] Swaps (AMM) table shows human-readable **offer** and **return** amounts for each row (in addition to existing columns). - [ ] Limit fills (maker) table shows fill amounts from indexed `token0_amount` / `token1_amount`. - [ ] Limit cancellations unchanged (no fake amount column). - [ ] Amounts match indexer values for the same `tx_hash` / row id (spot-check vs JSON or CSV). - [ ] Mobile: amount columns reachable via existing horizontal scroll (or better responsive layout without regressing #352). - [ ] Download CSV: on success, browser receives a `.csv` file (or equivalent download) for swaps/fills/cancels as applicable. - [ ] Download CSV: on indexer/network failure, user sees a clear error; no silent no-op. - [ ] CSV still contains amount columns; formula-injection escaping (#432) still passes indexer tests. - [ ] No Keplr / wallet signature prompt required for CSV. - [ ] Docs/skill mention amount columns in wallet pair history. --- ## Test plan (all paths) ### Unit / component - [ ] `WalletIndexerHistoryPanel` renders Amount in/out (or chosen labels) when swap rows include `offer_amount` / `return_amount`. - [ ] Limit fills render amount cells when `token0_amount` / `token1_amount` present. - [ ] Empty / loading / error query states unchanged. - [ ] CSV success path still calls `downloadTextAsFile` with expected filename prefix (`swaps-` / `limit-fills-` / `limit-cancellations-`). - [ ] CSV failure path shows UI error and clears `pending` so the button is re-enabled. - [ ] Panel still returns `null` when wallet or pair address invalid. ### Integration / indexer (regression) - [ ] `GET /api/v1/traders/{addr}/trades?format=csv&pair=…` returns header including `offer_amount,return_amount`. - [ ] Fills CSV still includes `token0_amount,token1_amount`. - [ ] Formula-injection unit tests in `text_csv` still pass. ### E2E / manual - [ ] `/limits` + connected wallet + selected pair with known swaps: amounts visible; CSV download works (desktop Chrome). - [ ] `/trade` swaps-only history: amounts + CSV after expanding disclosure if collapsed. - [ ] Mobile viewport (~375px): scroll to see amount columns; CSV — verify success or visible error (not silent). - [ ] Keplr connected vs Simulated Wallet: both can download CSV once address is present (proves not Keplr-signing). - [ ] Indexer stopped / wrong `VITE_INDEXER_URL`: table and/or CSV show failure messaging. - [ ] Pair with no history: empty copy; CSV still downloads header-only or empty body without crashing. ### Cross-browser smoke - [ ] Desktop Chrome, Firefox; mobile Safari / Chrome if available for blob download behavior. --- ## Test plan (attack / hack / abuse vectors) - [ ] **CSV injection** — offer/ask symbols or amounts crafted with leading `=`, `+`, `-`, `@` remain neutralized by server escaping (#432); do not strip escaping on the client. - [ ] **Path / address abuse** — malformed wallet or pair in URL must not enable arbitrary file write; download filename stays sanitized slug (existing `replace(/[^a-zA-Z0-9]/g, '')`). - [ ] **Large export DoS** — respect server `limit` max (200); client must not request unbounded rows; rapid repeated CSV clicks should not wedge UI (`pending` / disable). - [ ] **Open redirect / XSS via tx or amount cells** — amounts rendered as text; tx links still use `getExplorerTxUrl` + `rel="noopener noreferrer"`. - [ ] **CORS / cross-origin** — CSV fetch remains same-origin as configured indexer; no `credentials: 'include'` unless already required elsewhere. - [ ] **Content-Type sniffing** — keep `text/csv;charset=utf-8` blob type; do not execute CSV as script in-page. --- ## Verification criteria 1. On a LocalTerra or staging pair with ≥1 wallet swap, history shows non-empty amount cells matching indexer JSON for that row. 2. Clicking **Download CSV** produces a file whose `offer_amount`/`return_amount` match the UI (modulo formatting) for the same txs. 3. With indexer unreachable, CSV click shows an error within one interaction; DevTools shows the failed `format=csv` request; no unhandled rejection left unexplained. 4. `make test-frontend` (or targeted Vitest) and existing trader CSV indexer tests pass; E2E history spec covers CSV click when chain/indexer available. 5. Manual mobile check confirms amounts are discoverable and CSV is either successful or clearly failed — not “button does nothing.”
PlasticDigits commented 2026-07-12 09:10:52 +00:00 (Migrated from gitlab.com)

mentioned in commit 49b0195034

mentioned in commit 49b0195034055f31c55b0569ff91a67c54198e79
PlasticDigits commented 2026-07-12 09:11:40 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1016

mentioned in merge request !1016
PlasticDigits commented 2026-07-12 09:23:13 +00:00 (Migrated from gitlab.com)

mentioned in commit 4c97620dc5

mentioned in commit 4c97620dc55b9eef57b4ac40476d449d7617815c
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-07-12 09:23:13 +00:00
PlasticDigits commented 2026-08-18 00:29:03 +00:00 (Migrated from gitlab.com)

mentioned in issue #557

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