Frontend: indexer candle parsing hardening and stale getCandles race tests #226

Closed
opened 2026-05-29 05:40:57 +00:00 by PlasticDigits · 15 comments
PlasticDigits commented 2026-05-29 05:40:57 +00:00 (Migrated from gitlab.com)

Summary

Harden indexer candle → chart point mapping and React Query candle fetching so malformed or reordered API responses cannot crash the trade workspace or paint the wrong pair. Pure parsing tests stay in default Vitest; race tests use mocked getCandles delays.

Bundled scope: priceChartCandles edge cases · out-of-order getCandles (stale pair wins)


Current codebase

Area Files
Candle mapping frontend-dapp/src/components/charts/priceChartCandles.ts — filters rows missing open/close; parseFloat on OHLC/volume
Tests (minimal) frontend-dapp/src/components/charts/__tests__/priceChartCandles.test.ts — empty input, drop empty open/close, sort, volume quote/base
Chart shell frontend-dapp/src/components/charts/PriceChart.tsx — useQuery(['candles', pairAddress, interval]), placeholderData for interval-only (#148)
Empty state PriceChartEmptyState.tsx; tests in PriceChart.test.tsx for [] and all-invalid rows
Placeholder helper priceChartCandlesPlaceholder.ts — keepPreviousCandlesForIntervalSwitch
React Query No dedicated test for slow pair B resolves after fast pair A

indexerCandlesToChartPoints does not validate NaN, extreme floats, or partial field garbage beyond the open/close presence check.


Why this is needed

  1. Indexer trust boundary: Malformed strings (1e309, NaN, objects) should not propagate into lightweight-charts and throw during setData.
  2. Pair-switch races: #180 / #148: rapid navigation can return candles for the wrong pair if responses are not superseded by query key + React Query cancellation semantics.
  3. #211 attack table: “Prototype pollution / weird JSON” and “out-of-order getCandles” listed but not fully automated.

Constraints and guardrails

  1. Pure mapping stays pure: No lightweight-charts import in priceChartCandles.ts tests.
  2. Conservative filtering: Prefer dropping bad rows over coercing magic values; empty result → existing empty state (no canvas).
  3. Do not change API contract without indexer coordination — client-side defense only.
  4. No dangerouslySetInnerHTML on candle fields (already true; verify in tests).
  5. Default npm run test:run only — no Postgres for these tests.

Relevant files

  • frontend-dapp/src/components/charts/priceChartCandles.ts
  • frontend-dapp/src/components/charts/__tests__/priceChartCandles.test.ts
  • frontend-dapp/src/components/charts/PriceChart.tsx
  • frontend-dapp/src/components/charts/priceChartCandlesPlaceholder.ts
  • frontend-dapp/src/components/charts/__tests__/PriceChart.test.tsx
  • frontend-dapp/src/services/indexer/client.ts (mock only)
  • docs/frontend.md, skills/AGENTS_FRONTEND_PRICE_CHART.md, skills/AGENTS_FRONTEND_TRADE_PAIR_SWITCH.md

  1. Extend priceChartCandles.test.ts: Non-numeric open/close, NaN/Infinity results, mixed valid/invalid rows, missing high/low, zero-length strings, absurd open_time (invalid date → drop or stable sort).
  2. Decide policy: Document whether parseFloat('') → NaN points are filtered (likely add Number.isFinite guard on mapped numbers).
  3. PriceChart.test.tsx race: Mock getCandles with delayed resolves for pairA vs pairB; switch to pairB before pairA returns; assert headline/canvas props reflect B only.
  4. If React Query cancellation is insufficient, add explicit pairAddress ref guard in PriceChart (only with failing test first).

Acceptance criteria

  • priceChartCandles.test.ts covers malformed/extreme numeric inputs; no NaN in output series.
  • PriceChart.test.tsx (or dedicated test) proves stale slower getCandles does not overwrite newer pair data.
  • Existing empty-state tests still pass ([], all-invalid open/close).
  • Policy documented in priceChartCandles.ts comment + docs/frontend.md if behavior changes.
  • No new permanent test.skip.

Test plan — functional paths

# Path Setup Expected
1 Valid multi-row Standard indexer rows Sorted ascending; finite OHLC
2 Empty / undefined [] / undefined []
3 Missing open or close open: '' Row dropped
4 Non-numeric open open: 'abc' Row dropped or no NaN points
5 NaN / Infinity strings close: 'NaN' Row dropped
6 Extreme float 1e309 Row dropped or clamped per policy
7 Mixed valid/invalid 3 rows, 1 bad 2 points
8 Quote/base volume edge Already covered; regression Unchanged
9 Interval switch Same pair, new interval Placeholder keeps chart; new data applies
10 Pair A slow, B fast Delayed mocks UI shows B candles only
11 Pair switch mid-fetch A loading → B No flash of A after B selected

Test plan — attack vectors

Vector Approach Expected
Weird types in JSON open as object/array Dropped; no throw
XSS strings in fields <script> in open_time Text-only downstream; no execution
50k row payload Optional perf test or documented cap CI stable; no hang in unit job
Duplicate timestamps Two rows same open_time Stable sort; no throw
Stale interval on same pair Slow 1h after 1d selected React Query serves latest key

Verification criteria

  1. cd frontend-dapp && npm run test:run — all candle + PriceChart tests green.
  2. No regression in npm run test:charts (uses fixtures, not indexer).
  3. Manual: rapid pair switch on /trade — chart matches URL pair.
  4. Update skills/AGENTS_FRONTEND_TRADE_PAIR_SWITCH.md if race guard added.

## Summary Harden **indexer candle → chart point** mapping and **React Query** candle fetching so malformed or reordered API responses cannot crash the trade workspace or paint the wrong pair. Pure parsing tests stay in default Vitest; race tests use mocked `getCandles` delays. **Bundled scope:** `priceChartCandles` edge cases · out-of-order `getCandles` (stale pair wins) --- ## Current codebase | Area | Files | |------|--------| | Candle mapping | `frontend-dapp/src/components/charts/priceChartCandles.ts` — filters rows missing `open`/`close`; `parseFloat` on OHLC/volume | | Tests (minimal) | `frontend-dapp/src/components/charts/__tests__/priceChartCandles.test.ts` — empty input, drop empty open/close, sort, volume quote/base | | Chart shell | `frontend-dapp/src/components/charts/PriceChart.tsx` — `useQuery(['candles', pairAddress, interval])`, `placeholderData` for interval-only ([#148](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/148)) | | Empty state | `PriceChartEmptyState.tsx`; tests in `PriceChart.test.tsx` for `[]` and all-invalid rows | | Placeholder helper | `priceChartCandlesPlaceholder.ts` — `keepPreviousCandlesForIntervalSwitch` | | React Query | No dedicated test for **slow pair B resolves after fast pair A** | `indexerCandlesToChartPoints` does **not** validate `NaN`, extreme floats, or partial field garbage beyond the open/close presence check. --- ## Why this is needed 1. **Indexer trust boundary:** Malformed strings (`1e309`, `NaN`, objects) should not propagate into lightweight-charts and throw during `setData`. 2. **Pair-switch races:** [#180](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/180) / [#148](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/148): rapid navigation can return candles for the **wrong** pair if responses are not superseded by query key + React Query cancellation semantics. 3. **#211 attack table:** “Prototype pollution / weird JSON” and “out-of-order getCandles” listed but not fully automated. --- ## Constraints and guardrails 1. **Pure mapping stays pure:** No `lightweight-charts` import in `priceChartCandles.ts` tests. 2. **Conservative filtering:** Prefer dropping bad rows over coercing magic values; empty result → existing empty state (no canvas). 3. **Do not change API contract** without indexer coordination — client-side defense only. 4. **No `dangerouslySetInnerHTML`** on candle fields (already true; verify in tests). 5. Default `npm run test:run` only — no Postgres for these tests. --- ## Relevant files - `frontend-dapp/src/components/charts/priceChartCandles.ts` - `frontend-dapp/src/components/charts/__tests__/priceChartCandles.test.ts` - `frontend-dapp/src/components/charts/PriceChart.tsx` - `frontend-dapp/src/components/charts/priceChartCandlesPlaceholder.ts` - `frontend-dapp/src/components/charts/__tests__/PriceChart.test.tsx` - `frontend-dapp/src/services/indexer/client.ts` (mock only) - `docs/frontend.md`, `skills/AGENTS_FRONTEND_PRICE_CHART.md`, `skills/AGENTS_FRONTEND_TRADE_PAIR_SWITCH.md` --- ## Recommended direction 1. **Extend `priceChartCandles.test.ts`:** Non-numeric `open`/`close`, `NaN`/`Infinity` results, mixed valid/invalid rows, missing `high`/`low`, zero-length strings, absurd `open_time` (invalid date → drop or stable sort). 2. **Decide policy:** Document whether `parseFloat('')` → `NaN` points are filtered (likely add `Number.isFinite` guard on mapped numbers). 3. **`PriceChart.test.tsx` race:** Mock `getCandles` with delayed resolves for `pairA` vs `pairB`; switch to `pairB` before `pairA` returns; assert headline/canvas props reflect **B** only. 4. If React Query cancellation is insufficient, add explicit `pairAddress` ref guard in `PriceChart` (only with failing test first). --- ## Acceptance criteria - [ ] `priceChartCandles.test.ts` covers malformed/extreme numeric inputs; no `NaN` in output series. - [ ] `PriceChart.test.tsx` (or dedicated test) proves stale slower `getCandles` does not overwrite newer pair data. - [ ] Existing empty-state tests still pass (`[]`, all-invalid open/close). - [ ] Policy documented in `priceChartCandles.ts` comment + `docs/frontend.md` if behavior changes. - [ ] No new permanent `test.skip`. --- ## Test plan — functional paths | # | Path | Setup | Expected | |---|------|--------|----------| | 1 | Valid multi-row | Standard indexer rows | Sorted ascending; finite OHLC | | 2 | Empty / undefined | `[]` / `undefined` | `[]` | | 3 | Missing open or close | `open: ''` | Row dropped | | 4 | Non-numeric open | `open: 'abc'` | Row dropped or no NaN points | | 5 | `NaN` / `Infinity` strings | `close: 'NaN'` | Row dropped | | 6 | Extreme float | `1e309` | Row dropped or clamped per policy | | 7 | Mixed valid/invalid | 3 rows, 1 bad | 2 points | | 8 | Quote/base volume edge | Already covered; regression | Unchanged | | 9 | Interval switch | Same pair, new interval | Placeholder keeps chart; new data applies | | 10 | Pair A slow, B fast | Delayed mocks | UI shows B candles only | | 11 | Pair switch mid-fetch | A loading → B | No flash of A after B selected | --- ## Test plan — attack vectors | Vector | Approach | Expected | |--------|----------|----------| | Weird types in JSON | `open` as object/array | Dropped; no throw | | XSS strings in fields | `<script>` in `open_time` | Text-only downstream; no execution | | 50k row payload | Optional perf test or documented cap | CI stable; no hang in unit job | | Duplicate timestamps | Two rows same `open_time` | Stable sort; no throw | | Stale interval on same pair | Slow 1h after 1d selected | React Query serves latest key | --- ## Verification criteria 1. `cd frontend-dapp && npm run test:run` — all candle + PriceChart tests green. 2. No regression in `npm run test:charts` (uses fixtures, not indexer). 3. Manual: rapid pair switch on `/trade` — chart matches URL pair. 4. Update `skills/AGENTS_FRONTEND_TRADE_PAIR_SWITCH.md` if race guard added. --- ## Related issues - [#211](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/211), [#148](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/148), [#180](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/180), [#113](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/113)
PlasticDigits commented 2026-05-29 05:40:58 +00:00 (Migrated from gitlab.com)

marked as related to #211

marked as related to #211
PlasticDigits commented 2026-05-29 05:40:59 +00:00 (Migrated from gitlab.com)

marked as related to #148

marked as related to #148
PlasticDigits commented 2026-05-29 05:41:00 +00:00 (Migrated from gitlab.com)

marked as related to #180

marked as related to #180
PlasticDigits commented 2026-05-29 05:41:01 +00:00 (Migrated from gitlab.com)

marked as related to #113

marked as related to #113
PlasticDigits commented 2026-05-29 07:56:35 +00:00 (Migrated from gitlab.com)

mentioned in commit fe73f455b1

mentioned in commit fe73f455b174caba6a8af65c483b08b27a6d84c3
PlasticDigits commented 2026-05-29 07:56:44 +00:00 (Migrated from gitlab.com)

Implementation summary (#226)

Hardened the indexer candle → chart trust boundary and added automated coverage for stale getCandles on pair switch.

Code

  • priceChartCandles.ts: Rows are dropped (not coerced) unless open/close are present, open_time parses to a finite Unix second, and all OHLC fields are Number.isFinite after parseFloat (covers NaN, Infinity, 1e309, non-numeric strings, empty high/low). Exported helpers parseChartFiniteNumber / candleOpenTimeSeconds for tests.
  • priceChartCandles.test.ts: Malformed/extreme/mixed-batch/duplicate-timestamp cases; asserts no NaN in output.
  • PriceChart.test.tsx: does not apply stale slower getCandles after switching to a faster pair — delayed mock for pair A, fast pair B; verifies OHLC/headline stay on B after A resolves. No extra ref guard was needed (React Query key + cancellation sufficient).

Docs / agent playbooks

Tests run (green)

  • cd frontend-dapp && npm run test:run (697 tests)
  • npm run test:charts (15 tests)

Merged to main at fe73f45.


Verification checklist

  • cd frontend-dapp && npm run test:run — priceChartCandles.test.ts + PriceChart.test.tsx green
  • npm run test:charts — no regression
  • Manual /trade: rapid pair switch — chart headline and candles match URL pair (no flash of prior pair)
  • Malformed candle payload (if you have a test indexer): chart shows empty state, not a crash
  • Interval switch on same pair still uses placeholder overlay (price-chart-interval-loading), not full remount

@brouie — please verify the checklist above on your environment when convenient. Leaving this issue open until QA sign-off.

## Implementation summary (#226) Hardened the **indexer candle → chart** trust boundary and added automated coverage for **stale `getCandles`** on pair switch. ### Code - **`priceChartCandles.ts`**: Rows are **dropped** (not coerced) unless `open`/`close` are present, `open_time` parses to a finite Unix second, and all OHLC fields are `Number.isFinite` after `parseFloat` (covers `NaN`, `Infinity`, `1e309`, non-numeric strings, empty `high`/`low`). Exported helpers `parseChartFiniteNumber` / `candleOpenTimeSeconds` for tests. - **`priceChartCandles.test.ts`**: Malformed/extreme/mixed-batch/duplicate-timestamp cases; asserts no `NaN` in output. - **`PriceChart.test.tsx`**: *does not apply stale slower getCandles after switching to a faster pair* — delayed mock for pair A, fast pair B; verifies OHLC/headline stay on B after A resolves. **No extra ref guard** was needed (React Query key + cancellation sufficient). ### Docs / agent playbooks - [docs/frontend.md](docs/frontend.md) — new invariants: indexer candle parsing (#226), stale `getCandles` on pair switch - [docs/testing.md](docs/testing.md) — test matrix row for #226 - [skills/AGENTS_FRONTEND_PRICE_CHART.md](skills/AGENTS_FRONTEND_PRICE_CHART.md) — invariants 12–13 - [skills/AGENTS_FRONTEND_TRADE_PAIR_SWITCH.md](skills/AGENTS_FRONTEND_TRADE_PAIR_SWITCH.md) — stale race note ### Tests run (green) - `cd frontend-dapp && npm run test:run` (697 tests) - `npm run test:charts` (15 tests) Merged to `main` at `fe73f45`. --- ### Verification checklist - [ ] `cd frontend-dapp && npm run test:run` — `priceChartCandles.test.ts` + `PriceChart.test.tsx` green - [ ] `npm run test:charts` — no regression - [ ] Manual `/trade`: rapid pair switch — chart headline and candles match URL pair (no flash of prior pair) - [ ] Malformed candle payload (if you have a test indexer): chart shows empty state, not a crash - [ ] Interval switch on **same** pair still uses placeholder overlay (`price-chart-interval-loading`), not full remount @brouie — please verify the checklist above on your environment when convenient. Leaving this issue **open** until QA sign-off.
PlasticDigits commented 2026-05-29 12:15:55 +00:00 (Migrated from gitlab.com)

mentioned in issue #211

mentioned in issue #211
PlasticDigits commented 2026-05-30 06:36:44 +00:00 (Migrated from gitlab.com)

Verification complete (GitLab #226)

Verified on worktree verify/issue-226 against main @ 54a6346 (includes prior implementation @ fe73f45).

What was checked

Automated (all green)

  • cd frontend-dapp && npm run test:run — 710 tests passed (includes 16 priceChartCandles + 20 PriceChart cases)
  • Targeted: stale-pair race test does not apply stale slower getCandles after switching to a faster pair (GitLab #226) — pass
  • npm run test:charts — 21 passed, 1 pre-existing skip (real-library perf guard)
  • No new test.skip in candle/chart tests

Implementation invariants (confirmed)

  • priceChartCandles.ts: drop-row policy with Number.isFinite on all OHLC + valid open_time; exported parseChartFiniteNumber / candleOpenTimeSeconds
  • PriceChart.tsx: React Query key ['candles', pairAddress, interval] + canvas key={pairAddress} — no ref guard needed (unit test proves cancellation sufficient)
  • No dangerouslySetInnerHTML in chart components

Docs / agent playbooks (cross-linked)

  • docs/frontend.md — indexer candle parsing + stale getCandles invariants
  • docs/testing.md — test matrix row
  • skills/AGENTS_FRONTEND_PRICE_CHART.md — invariants 12–13
  • skills/AGENTS_FRONTEND_TRADE_PAIR_SWITCH.md — stale race note

Manual (localnet)

  • Frontend on :5173 against indexer :3001 + LocalTerra LCD :1317
  • EMBER/CORAL /trade chart headline and candles match URL pair
  • Pair selector switch CORAL → JADE: URL and ticket updated to JADE; chart entered loading state (no CORAL flash observed during switch)
  • Interval buttons remain responsive on same pair

Verification checklist

  • npm run test:run — priceChartCandles.test.ts + PriceChart.test.tsx green
  • npm run test:charts — no regression
  • Manual /trade: pair switch via selector — chart/headline track selected pair (no stale-prior-pair flash)
  • Malformed candle rows filtered client-side (unit tests; no crash path to lightweight-charts)
  • Interval switch on same pair uses placeholder overlay (price-chart-interval-loading), not full remount
  • Policy documented in module comment + docs/frontend.md
  • Agent skills cross-linked

All acceptance criteria and verification criteria from the issue body pass. Closing.

## Verification complete (GitLab #226) Verified on worktree `verify/issue-226` against `main` @ `54a6346` (includes prior implementation @ `fe73f45`). ### What was checked **Automated (all green)** - `cd frontend-dapp && npm run test:run` — **710** tests passed (includes 16 `priceChartCandles` + 20 `PriceChart` cases) - Targeted: stale-pair race test *does not apply stale slower getCandles after switching to a faster pair (GitLab #226)* — pass - `npm run test:charts` — **21 passed**, 1 pre-existing skip (real-library perf guard) - No new `test.skip` in candle/chart tests **Implementation invariants (confirmed)** - `priceChartCandles.ts`: drop-row policy with `Number.isFinite` on all OHLC + valid `open_time`; exported `parseChartFiniteNumber` / `candleOpenTimeSeconds` - `PriceChart.tsx`: React Query key `['candles', pairAddress, interval]` + canvas `key={pairAddress}` — no ref guard needed (unit test proves cancellation sufficient) - No `dangerouslySetInnerHTML` in chart components **Docs / agent playbooks (cross-linked)** - docs/frontend.md — indexer candle parsing + stale getCandles invariants - docs/testing.md — test matrix row - skills/AGENTS_FRONTEND_PRICE_CHART.md — invariants 12–13 - skills/AGENTS_FRONTEND_TRADE_PAIR_SWITCH.md — stale race note **Manual (localnet)** - Frontend on `:5173` against indexer `:3001` + LocalTerra LCD `:1317` - EMBER/CORAL `/trade` chart headline and candles match URL pair - Pair selector switch CORAL → JADE: URL and ticket updated to JADE; chart entered loading state (no CORAL flash observed during switch) - Interval buttons remain responsive on same pair ### Verification checklist - [x] `npm run test:run` — `priceChartCandles.test.ts` + `PriceChart.test.tsx` green - [x] `npm run test:charts` — no regression - [x] Manual `/trade`: pair switch via selector — chart/headline track selected pair (no stale-prior-pair flash) - [x] Malformed candle rows filtered client-side (unit tests; no crash path to lightweight-charts) - [x] Interval switch on same pair uses placeholder overlay (`price-chart-interval-loading`), not full remount - [x] Policy documented in module comment + docs/frontend.md - [x] Agent skills cross-linked All acceptance criteria and verification criteria from the issue body pass. Closing.
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-05-30 06:36:47 +00:00
PlasticDigits commented 2026-08-15 12:19:49 +00:00 (Migrated from gitlab.com)

mentioned in issue #524

mentioned in issue #524
PlasticDigits commented 2026-08-17 03:51:35 +00:00 (Migrated from gitlab.com)

mentioned in issue #543

mentioned in issue #543
PlasticDigits commented 2026-08-17 03:51:37 +00:00 (Migrated from gitlab.com)

marked as related to #543

marked as related to #543
PlasticDigits commented 2026-08-19 00:57:40 +00:00 (Migrated from gitlab.com)

mentioned in issue #568

mentioned in issue #568
PlasticDigits commented 2026-08-27 00:20:53 +00:00 (Migrated from gitlab.com)

mentioned in issue #680

mentioned in issue #680
PlasticDigits commented 2026-08-29 14:16:58 +00:00 (Migrated from gitlab.com)

mentioned in issue #705

mentioned in issue #705
PlasticDigits commented 2026-09-01 08:14:38 +00:00 (Migrated from gitlab.com)

mentioned in issue #717

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