fix: Charts Price (USD) history flattened to live hub mark; idle USTC/UST1 never mark-to-market #568
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:implement
agent:implement
agent:open_issues
agent:ready
agent:research
agent:security_audit
agent:verify
architecture
backend
blocker:hybrid
blocker:launch
blocker:limit-orders
blocker:v2
block:log_only
block:security
bug
ci
contracts
correctness
deploy
dev
devops
docs
documentation
duplicate
e2e
enhancement
epic
feature
frontend
functional-completion
gas
good first issue
governance
help wanted
high-risk
hooks
hybrid
indexer
infra
infrastructure
integrators
invalid
launch-blocker
limit-orders
localnet
localterra
low priority
missing-implementation
needs-design
ops
performance
priority
high
priority
medium
product
qa
QA
question
ready
ready
research
scripts
security
security-hardening
smartcontracts
tech-debt
testing
ux
UX
v2
verification
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-dex-terraclassic#568
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
/chartsand/tradePrice (USD) history for UST1/cUSTC and UST1/USTR is a flat line at the live hub/oracle tick. CoinGecko USTC/USD moves, and CL8Y has DEX volume, but inverted candles do not. cLUNC/UST1 still moves because the chart is factory-oriented (cLUNC isasset_0) and the DEX ratio changed. Users expect USTC-quoted USD (and therefore UST1 USD) to follow the CEX USTC oracle even when that pair has no new swaps.This is one product bug with two coupled causes in the indexer:
swap_events.price_usdand candle USD OHLC withhuman × current hub_prices.update_candles_for_swapnever writes a bar unless a swap lands, so idle intervals cannot track oracle/hub ticks.Fixing only (1) leaves sparse step-functions. Fixing only (2) is immediately undone by the next rewrite. Bundle both.
Related: #556 (hub USD + this rewrite), #522 (P522
price_usd), #543 / #524 (per-barinvertUsd), #515 (CEX USTC/LUNC).Environment (production, 2026-08-19)
https://dex.cl8y.com, indexerhttps://indexer.dex.cl8y.com)$0.004788(KuCoin/MEXC; CoinGecko also polled).GET /api/v1/oracle/history/ustc?limit=1000spans only ~8.5h at 30s polls and already moves ~1.4%. Multi-day CoinGecko USTC is larger; pair charts should reflect that path, not a single tick.$0.999; USTR ≈$0.01151.terra1ceprjsxp86ggftf5e38wwt34l83e5gq7penkdnv4wsatkwcs8v6qccw55fterra16vxrhpvpcucu05y0nr862vf9hnqeh274uaff4s7hz4n0ea74006qf5hgqyterra1su5363453fj326u4t0kqar30f35cm3n0dc9yksg379u6875z350s4mm7h4human× live UST1 → movesMeasured on
GET /api/v1/pairs/{addr}/candles?interval=1h:close / close_humanis identical on every bar and equals the currenthub_pricesquote (cUSTC0.004788, USTR0.01151452, UST10.999285). Rel range of invertUsd = 0%. That isinvertUsd(#543) applied to as-of-now rewritten factory USD — not a frontend plot bug.Current codebase
Candle write path is swap-only
indexer/src/indexer/candle_builder.rsupdate_candles_for_swapis the only live candle writer. Parserindexer/src/indexer/parser.rsstampsprice_usdat ingest viapair_price_usd::price_usd_for_human_quote_per_base(P522-2: USD of 1 humanasset_0= human quote-per-base × quote USD). Missing / non-positiveprice_usdskips the candle (P522-4 / #543). Rebuildrebuild_candles_from_swapsis also swap-bucket only. Oracle ticks, hub refreshes, andpair_reservessnapshots do not create or extend candles.GET /api/v1/pairs/{addr}/candles(indexer/src/api/pairs.rs) returns stored rows (default 90-day window,limitclamp 1–1000). The dAppPriceChart.tsxplots that series afterapplyChartDisplayInvert/invertUsd. It does not mix inGET /api/v1/oracle/history/{ticker}(that feed is Protocol-only,useProtocolOracleQueries.ts).Hub refresh rewrites all history to now
refresh_hub_pricesruns on book-snapshot cadence (~10s,BOOK_SNAPSHOT_INTERVAL_MS) inpoller.rs. After replacing the currenthub_pricessnapshot it always callsbackfill_usd_from_hub:hub_pricesis a single current row per ticker (custc/ust1/ustr) — no history table (indexer/migrations/20260818180000_hub_prices.sql).pair_reservesis also one row per pair (upsert), not a time series.Consequences:
factory_usd / human= quote USD. After the rewrite that equals live cUSTC/USTR, so the chart is a horizontal line at the current CoinGecko-ish USTC (or current USTR). That matches the report: “USTC barely/never changed”; “UST1 never changed” (UST1 hub ≈ $1 and/or inverted USTR mark stuck at now).human × live UST1.human(UST1 per cLUNC) actually moved on-chain, so that pair still “has a chart.” The rewrite still flattens its implied quote USD to live UST1 (invertUsdunique = 1).hub_prices, so they are not rewritten this way. There is still no mark-to-market without swaps.volume_usdis not rewritten (ingest-time, #548 C8). Tapeprice_usdand volume USD disagree in time basis.UPDATE swap_events+UPDATE candlesevery ~10s is load, not O(1). H10 / #556 said GET must not scanswap_events; the background loop now does worse than a GET scan.What is already correct (do not regress)
asset_0; P522-Q catalog (USTC/cUSTC/uusd= #515ustc; LUNC/cLUNC/uluna= #515lunc; UST1/USTR = hub, not$1/ not2.5×).usd(cUSTC)= USTC oracle;usd(UST1)from deepest cUSTC/UST1 reserves (not last print);usd(USTR)from deepest vs already-priced hub. Snapshot GET stays O(1).invertUsdper bar, never1/xof USD, never human on a USD axis.last_pricestays human (H10). Advisory feeds (X5) — not settlement, TWAP, or/ust1window.ustc|lunc|vfdusd./oracle/price/ust1(andustr/custc) stay 400 (H6).Why the new implementation is needed
oracle_prices.reserves(cUSTC/UST1) × usd(USTC)(H2). That can change when USTC CEX moves with zero UST1/cUSTC swaps. Retail charts must follow that mark; last-trade-only candles cannot.humanchops, not a correct USD clock. LUNC-quoted pairs should also pick up #515luncticks between prints so behavior is consistent.swap_events.price_usdis an as-of-ingest advisory. Replacing it every 10s also corrupts Open/Close stats, limit USD notionals that read tape, and any integrator usingprice_usdas a print.Constraints / guardrails
UPDATEhistoricalswap_events.price_usdor candle USD from the livehub_pricessnapshot. Live hub is for new ingest, current GET/hub-prices, overview fields, and new mark bars only.trade_count/volume_*for oracle ticks. Histogram volume stays swap volume (C5 spirit).open/high/low/closeremain factory USD ofasset_0fromprice_usdmath. No human-on-USD-axis fallback. UI invert stays frontendinvertUsdper bar (C543-1–C543-9, T524-4).uusd, LUNC/cLUNC/uluna, UST1, or USTR identified by contract/denom, not symbol. Gem natives spoofingUSTRmust not get USD bars. Unknown quote → skip (NULL), never invent.$1, or2.5×. Non-finite /fits_numeric_38_18overflow → skip write.high ≥ low ≥ 0, merge invariants inmerge_candle_ohlc.ustr/ust1/custcto CEXOracleTicker. Do not use/oracle/pricefor hub.swap_eventson GET. Materialize marks on oracle/hub cadence. Cap work: catalog-priced active factory pairs only (listed, hub/oracle quote). No unboundedUPDATEof the whole tape.last_pricestays human. Do not switch aggregators to USD.validate_limit_order_price, wrap-mapper, or/ust1window.intervalalready allowlisted; keep it. Any new ticker/query params: unknown /..//javascript:→ 400. Numericlimitstays clamped.oracle_pricesas-ofblock_timestamp(USTC/LUNC quotes) plus documented hub reconstruction is allowed as a migration or ops job, not per refresh.Relevant files
indexer/src/db/queries/hub_prices.rsbackfill_usd_from_hub(remove from refresh);refresh_hub_pricesloopindexer/src/indexer/hub_usd.rsindexer/src/indexer/candle_builder.rsindexer/src/db/queries/candles.rsrebuild_candles_from_swapsindexer/src/indexer/pair_price_usd.rsprice_usd/volume_usdindexer/src/indexer/parser.rsindexer/src/indexer/oracle.rs+db/queries/oracle.rsoracle_priceshistoryindexer/src/indexer/poller.rsindexer/src/db/queries/pair_reserves.rsindexer/src/api/pairs.rsGET .../candlesindexer/tests/api_hub_prices.rs,candle_human_usd.rs,candle_skip_zero_price.rsfrontend-dapp/src/components/charts/priceChartCandles.tsfrontend-dapp/src/utils/tradePairDisplayOrientation.tsdocs/indexer-invariants.md,skills/AGENTS_INDEXER_HUB_USD.md,skills/AGENTS_FRONTEND_USD_CANDLE_INVERT.md,docs/frontend.md§ Trade pair display invertscripts/qa/verify-issue-556.shverify-issue-NNNRecommended direction
backfill_usd_from_hubfrom the refresh transaction.refresh_hub_pricesonly replaceshub_prices. Ingest continues to stampprice_usdfrom the then hub/oracle. Add a regression test: two swaps at different USTC oracles keep differentprice_usdafter a later hub refresh; candle USD OHLC is not allhuman × latest_hub.usd = last_human × as-of quote USD(quote USD = #515 ticker or current hub mark per P522-Q). Prefer last swap human; if none, reserve spot humanized (H2/H3) so a seeded idle UST1/cUSTC pool still charts.1m…1w): merge USD OHLC withmerge_candle_ohlc;trade_count/ volumes unchanged (0 on a new empty bar).price_usd/ candle USD fromse.price×oracle_pricesaverage as-ofblock_timestampfor USTC/LUNC quotes. For UST1/USTR quotes, document reconstruction (cannot replay destroyed hub history; going forward hub is live-only). Idempotent SQL; not the 10s loop./oracle/historyonto pair charts (CORS, X5, duplicate clocks). After indexer fix, inverted UST1/cUSTC 1h/1d history must not be a unique singleclose/close_human.trade_count=0). Playbook +make verify-issue-NNN. Optional later:hub_prices_historyif product wants Protocol UST1/USTR sparkline — out of scope unless needed for reconstruction; pair charts should not require a new HTTP ticker.Acceptance criteria
swap_events.price_usdor candle USD for existing rows.$0.005then$0.004, storedprice_usdand candle USD differ by that CEX move even ifhumanis unchanged. Invertedclose/close_humanequals the as-of USTC, not the latest tick.uusd-quoted catalog pairs) within one hub/oracle period. Inverted Price (USD) tracks USTC. Non-inverted tracks UST1 hub (reserves × USTC), not a frozen last print.lunc.trade_count = 0and zero volume; swap bars still increment counts. GET/candlesincludes mark bars. Chart volume histogram does not show fake volume.humanmoves; implied quote USD (invertUsd) may now also move with UST1 hub/USTC instead of sticking to one live tick.$1freeze (H1).last_priceunchanged (human)./oracle/price/ust1remains 400.GET /hub-pricesstill O(1) current snapshot.priceFormat(#543) still applies. No client CoinGecko stitch.docs/indexer-invariants.md+docs/frontend.md+make verify-issue-NNN. Existingmake verify-issue-556,verify-issue-543,verify-issue-522,verify-issue-515still pass.Test plan (all paths)
Indexer unit / lib
merge_candle_ohlcstill holds for mark merges (proptest untouched).usd = human × quote_usdfor mark payload; skip ≤0 / overflowfits_numeric_38_18.Indexer integration (Postgres)
0.005;refresh_hub_priceswith0.004; assertswap_events.price_usdand candle USD unchanged.price_usd; rebuild candles matches ingest stamps.0.005→0.0045; current1m/1h/1dclose USD moves;trade_countstays 0 on mark-only bar; human unchanged if last-trade carry-forward.ustc=None→ hub empty (existinghub_refresh_oracle_down_clears_marks); no new USD marks; no leftover$1.symbol=USTR.price_usdNULL/0 still does not write USD columns (candle_skip_zero_price).intervaljunk → 400;limitclamp; pair 404.UPDATEallswap_events(test via rowupdated_at/ query plan assertion or hook that counts updates).invertUsdper bar; a series with varying as-of quote USD is not flattened by the client; empty/invalid bars still dropped (#226/#543). No new fetch to CoinGecko fromPriceChart.make verify-issue-556verify-issue-543verify-issue-522verify-issue-515verify-issue-524verify-issue-548.Test plan (attack, hack, and abuse)
UPDATE swap_events+UPDATE candlesis an availability bug. After the fix, hub refresh must not rewrite historical tape. Fuzz: many pairs × intervals; mark writer must be bounded to catalog pairs + current buckets, not all 90d rows every tick.oracle_prices ⋈ swap_eventson eachGET /candles. Materialize. Existinglimitcap 1000 / 90d default stay.symbol=USTRor dust pool< $100TVL must not win hub ranking or receive invented USD candles. Identity = hub CW20 address /uusd/ulunadenom./candlesinterval,/oracle/history/{ticker},/hub-prices/{ticker}. Homoglyphs,../,javascript:, extra underscores → 400. No SQL string paste of interval except the existing match table in rebuild.1e309, NaN, negative human,high_human * huboverflowNUMERIC(38,18)/POWER(10,20)— skip write, never 500, never Inf in JSON (X6).trade_countorvolume_*(would inflate Charts 24h volume if someone later summed candle volume). Overviewtotal_volume_24h_usdstaysswap_events.volume_usdonly (C5 / C8).truncate_to_interval(oraclefetched_at/ hubupdated_at), not LCD block time of an absent swap. Do not back-fill arbitrary past gaps on every tick (that is a bounded migration). Open of an existing in-progress bucket stays first print/mark in that bucket (openunchanged on merge).Verification criteria
Local / CI
make verify-issue-NNN(new) +make verify-issue-556verify-issue-543verify-issue-522verify-issue-515.cd indexer && cargo test --test api_hub_prices --test candle_human_usd -- --test-threads=1plus new mark-to-market test file.priceChartCandles.test.ts,tradePairDisplayOrientation.test.ts.Production / QA (columbus-5 or LocalTerra with oracle+hub)
GET /api/v1/pairs/<UST1_cUSTC>/candles?interval=1h:close/close_humanis not a single unique value across days when USTC moved; inverted series tracksoracle/history/ustcdirectionally (advisory, sources may differ).trade_counton new bars = 0.GET /api/v1/hub-pricesstill current snapshot; Protocol CEX card still uses/oracle/history/{ticker}independently.Done when a reviewer can open
/chartsUST1/cUSTC (default invert) over several days and see USTC-like USD movement with or without new CL8Y volume, and cLUNC is no longer the only pair whose Price (USD) history moves.marked as related to #556
marked as related to #522
marked as related to #543
marked as related to #524
marked as related to #515
mentioned in merge request !1100
mentioned in merge request !1101
mentioned in commit
1a18a62011mentioned in commit
38c5252de3mentioned in commit
9c802c1aacMerged via !1101.
Core
VERIFY_ISSUE_568_SKIP_RELATED=1 make verify-issue-568passed (docs, no as-of-now hub rewrite, lib +candle_usd_markintegration, invertUsd frontend). Remaining:oracle_prices; UST1/USTR-quoted history still cannot be replayed (no hub time series)mentioned in issue #570
mentioned in issue #583
marked as related to #583
mentioned in issue #586
mentioned in issue #589
mentioned in issue #613
mentioned in issue #614
mentioned in issue #619
mentioned in issue #680
mentioned in issue #682
mentioned in issue #683
mentioned in issue #687
mentioned in issue #689
mentioned in issue #705
mentioned in merge request !1197