feat: /protocol UTC chart — Volume / Liquidity / Fees metric toggle #689
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#689
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
/protocolalready has a UTC Hourly / Daily / Monthly bar chart, but it is volume-only. Operators cannot inspect liquidity (stock) or treasury fees (flow) on the same grain. Bundle into one issue: metric buttons Volume / Liquidity / Fees on the existing census chart, with indexer UTC-grain rollups that match P668 GET/DoS rules.Do not split liquidity vs fees vs UI toggle into separate issues. Do not overlay three series by default. Do not reuse DeFiLlama daily volume/fees.
Related: #668 (USD axis + grain chart), #652 (UTC-day volume series), #569 (pool TVL stock + 24h Δ%), #677 (liquidity 24h-only chip + dense x-axis), #586 (trailing treasury fees), #613 / #614 / #683 (wrap / UST1 window / economic fee USD), #576 (trailing tiles ≠ calendar bars), #489 (short copy), #653 (one chrome layer), #631 (Llama daily — wrong methodology).
Current codebase
The chart exists and is readable as a volume time series. Liquidity and fees exist only as trailing tiles (and a fee breakdown table), not as UTC-grain bars.
ProtocolVolumeDailyChart.tsxinsideProtocolGlobalStats.tsx. Custom SVG (protocol-volume-daily-chart): USD Y-axis, pointer/keyboard tooltip, Hourly / Daily / Monthly tabs (protocol-volume-grain-*), default Daily. Title copy is UTC volume. No metric toggle.protocolVolumeGrain.ts:ResizeObserver→limitclamped hourly 12–168, daily 7–90, monthly 6–24. Query key(grain, limit). X-axis density P668-9.getProtocolVolumeSeries→GET /api/v1/protocol/volume/daily?grain=&limit=.useProtocolVolumeSeriesQuerykeys['indexer-protocol-volume-series', grain, limit]. Hide on 404/501. No liquidity / fees series helpers.GET /api/v1/protocol/volume/daily:grain=hourly|daily|monthly+ cappedlimitelse 400.from/to→ 400.days=7|30withoutgrainstays the #652 alias. 60s cache keyed by allowlisted(grain, limit)ordays. Readsprotocol_hourly_volume/protocol_daily_volume/protocol_monthly_volumeonly — neverswap_events/ Llama. Idle"0"; activity + unpricednull. Newest-last.volume_aggregator.rs(~5 min) callsrefresh_protocol_{hourly,daily,monthly}. Hourly prune ~10d; daily ≥ 95d; monthly ≥ 24 months. Methodology = Protocol catalog (gems / wrap / window included).protocol-stat-liquidity) is stock TVL fromGET /overview(total_liquidity_usd+ one 24h snapshot Δ%). P569 / P677: no 30d chip on the tile. Not a time series.global_liquidity_snapshots: ~5 min samples, retain ≥ 35 days,SNAPSHOT_MIN_INTERVAL4 min. Used only to compute trailing 24h/30d Δ% on the aggregator. GET/overviewmust not walk this table. There is no hourly/daily/monthly liquidity rollup and no series GET.protocol-fee-statssits after Global stats. Trailing 24h / 7d / 30d USD + source/token tables viaGET /api/v1/protocol/fees?window=(24h|7d|30delse 400). O(1) child rollups. Not UTC calendar buckets.protocol_fee_eventsingested with stampedfee_usd(PFee / I613 / I614 / EFee). Refresh SUMs into trailing windows off the GET path. GET must not SUMprotocol_fee_events. There is no UTC-hour/day/month fee series table for Protocol catalog (Llamadefillama_daily_feesis gem-excluded and must not feed/protocol).GET /api/v1/defillama/dailyis a different product: UTC-day, gem-exclude, wrap/window out of volume. Playbook forbids calling it from/protocol.trailingWindowCopy.ts:PROTOCOL_VOLUME_DAILY_LABEL= UTC volume; grain labels Hourly/Daily/Monthly; titles stress calendar ≠ trailing tiles. No liquidity/fees chart strings.ProtocolPage.test.tsx; Playwrighte2e/protocol-page.spec.ts(5 workers); indexerindexer_protocol_volume.rs,indexer_protocol_liquidity.rs,indexer_protocol_fees.rs. Gates:make verify-issue-668/652/569/677/586/576.Why this is needed
/protocolor mentally aligning trailing 24h/7d/30d tiles with calendar bars.global_liquidity_snapshotsorSUM(protocol_fee_events)on the request path would violate P550-5 / PFee-8 / indexer DoS invariants. Llama daily is the wrong catalog (gems excluded). New materialized hour/day/month tables are required, same pattern as volume.Constraints / guardrails
GET /protocol/fees?window=all stay. Chart is additive. Do not move the chart intoprotocol-fee-stats. Do not restoreprotocol-stat-liquidity-30d. Do not restore7d/30das the grain selector.SUMpriced USD in[bucket, next). Idle → JSON"0"; activity + all unpriced →null(outlined bar, tooltip em-dash — never invent$0then a filled bar). Missing rollup row → idle"0".SUMof samples, not average unless documented as a separate metric (v1 is last sample). No snapshot in bucket → JSONnull(unknown ≠$0TVL). Cold start /--fresh→ many nulls until samples exist; empty copy No liquidity yet. Do not interpolate on GET. Do not backfill 30d from zeros.SUMswap_events/protocol_fee_events/ fills on GET. Do not walkglobal_liquidity_snapshots/pair_reserveson GET. Do not N+1GET /defillama/daily.OVERVIEW_GLOBAL_STATS_LIVE=1still must not 60d-SUM.from/to→ 400. Extra query junk must not bust the 60s cache.metricconceptuallyvolume|liquidity|fees.grainhourly|daily|monthly. Integerlimit1..=max (hourly 168, daily 90, monthly 24) else 400. Unknown / arrays / negatives /javascript:/../→ 400. Client rejects non-allowlisted metric/grain/limit before fetch. Fetch only the selected metric (do not prefetch all three on every paint). Debounce resize (P668-4).GET /api/v1/protocol/volume/daily+days=7\|30alias stay somake verify-issue-652/668stay green. New series live on sibling routes, not a breakingmetric=on the volume path:GET /api/v1/protocol/liquidity/daily?grain=&limit=GET /api/v1/protocol/fees/daily?grain=&limit=Existing
GET /api/v1/protocol/fees?window=is the trailing breakdown — do not overload it withgrain.pair_reserves, P522-Q + hub). Not book escrow / parked dust. Not CGliquidity_in_usd. Unpriced / omitted pairs ≠$0. Never$1UST1,2.5×USTR, or vFDUSD conversion.swap_amm,book_take,limit_place,wrap,unwrap,ust1_mint,ust1_redeem). Hybrid = poolcommission_amount+ fillcommission_amount— not both fill commission and swapbook_commission_amount(PFee-5). Spread / burn tax / hook / community-tax extra-debit are not protocol fees. Stampfee_usdat ingest — do not rewrite non-null stamps from the live mark (#568). Headline series is priced SUM for the bucket (same idle/unpriced rule as volume). Unconfigured wrap mapper / UST1 window simply have no those events — do not invent idle wrap rows on the chart.--fresh/ young indexer shows idle zeros (flow) or nulls (stock) — not a GET-path backfill.protocol-global-stats. No nestedcard-glass(C653). Do not mountPriceChart. Tooltip is text (period +formatProtocolUsd; missing/unpriced → em-dash). NoinnerHTML/eval/javascript:URLs. XSS in period / usd fields render as text. USD axis never Inf /NaN/ rawuluna. Keyboard focus still shows tooltip (P668-2). X-axis density stays P668-9.title/aria-labelmay say the chart is not the trailing tile. Do not add a banner about stock vs flow in the Global stats lead.AGENTS_FRONTEND_PROTOCOL_STATS.mdwith P6xx invariants (do not silently weaken P668 / P569 / PFee). Updatedocs/frontend.md§ Protocol,docs/indexer-invariants.md,docs/runbooks/overview-global-stats-brin.md,AGENTS.md+make verify-issue-*. Keepverify-issue-668/652/667/677/569/586/576/577/631/653/613/614/683green.Relevant files
frontend-dapp/src/components/protocol/ProtocolVolumeDailyChart.tsxdata-testidor alias so #668 e2e stays greenfrontend-dapp/src/components/protocol/useProtocolVolumeSeriesQuery.ts(metric, grain, limit)frontend-dapp/src/components/protocol/ProtocolGlobalStats.tsxfrontend-dapp/src/utils/protocolVolumeGrain.tsfrontend-dapp/src/utils/trailingWindowCopy.tsfrontend-dapp/src/utils/formatProtocolStats.tsfrontend-dapp/src/services/indexer/client.tsfrontend-dapp/src/types/index.tsliquidity_usd/fees_usdor sharedvalue_usd)frontend-dapp/src/pages/ProtocolPage.test.tsxfrontend-dapp/src/utils/__tests__/trailingWindowCopy.test.tsfrontend-dapp/e2e/protocol-page.spec.tsindexer/src/api/protocol_volume.rsdaysaliasindexer/src/api/protocol_liquidity.rs(or sibling module)indexer/src/api/protocol_fees.rswindow=breakdown; add separate/protocol/fees/dailyhandler (do not overloadwindow)indexer/src/api/mod.rsindexer/src/db/queries/liquidity_snapshots.rsindexer/src/db/queries/protocol_fees.rsindexer/src/indexer/volume_aggregator.rsprotocol_{hourly,daily,monthly}_{liquidity,fees})indexer/tests/indexer_protocol_volume.rsskills/AGENTS_FRONTEND_PROTOCOL_STATS.mddocs/frontend.md§ Protocoldocs/indexer-invariants.mddocs/runbooks/overview-global-stats-brin.mdscripts/qa/verify-issue-*.sh+ Makefile targetRecommended direction
volume_usd/liquidity_usd/fees_usdthrough onevalue_usdhelper. Title + empty string +aria-labelfollow the selected metric. Keepprotocol-volume-daily-chartas the host testid (or addprotocol-utc-series-chartand alias the old id) so #668 e2e does not flake.protocol_hourly_liquidity (utc_hour, liquidity_usd NULLABLE, priced_pair_count, refreshed_at), plus daily (utc_day) and monthly (utc_month).liquidity_usdNULL = no snapshot in bucket.protocol_hourly_fees (utc_hour, fees_usd NULLABLE, event_count, unpriced_count, refreshed_at)(+ daily/monthly).fees_usd"0"idle /nullall-unpriced — same as volume.SUM(fee_usd)/ count unpriced in[bucket, next)fromprotocol_fee_eventsoff GET.protocol_volume.rsallowlist + 60sMutexcache keyed only by(grain, limit). Fill missing flow buckets with"0"; fill missing stock buckets withnull. Newest-last.timezone: "UTC".methodology: "protocol_catalog"(notdefillama).getProtocolLiquiditySeries/getProtocolFeesSerieswith the same grain/limit guards as volume. React Query key includes metric. Switching to a 404 metric hides the plot, not the whole Global stats card.from/to, CSV, unique-trader, or a second chart under Protocol fees.Acceptance criteria
/protocolGlobal stats chart shows Volume / Liquidity / Fees buttons; default Volume; default grain Daily.days=7\|30alias, and hide-on-404 behave as P668 / P652.$0. Title UTC liquidity. Empty: No liquidity yet.$0; all-unpriced outlined / em-dash. Title UTC fees. Empty: No fees yet.GET /protocol/fees?window=still 24h/7d/30d).GET /protocol/liquidity/dailyandGET /protocol/fees/daily: allowlistedgrain+limit;from/to400; 60s cache; EXPLAIN shows grain tables only.card-glass; noPriceChart.innerHTML.make verify-issue-<this>exist. Prior protocol verify targets stay green.Test plan (functional paths)
Frontend (Vitest)
getProtocolVolumeSeriescalled; liquidity/fees not fetched.null→ outlined bar + tooltip—; idle"0"→ zero-height.nullseries → No liquidity yet (not a flat$0chart). Mixed null/priced → outlined gaps."0"vs unpricednullmatch volume rules.utc_day/liquidity_usd/fees_usd→ text only.protocol-stat-liquidity-30dcount 0.TRAILING_WINDOW_VISIBLE_LABELSlength check.Indexer (Postgres integration,
--test-threads=1)from/to/window/days/metric→ 400 (or ignored without cache bust — prefer 400 forfrom/to).null, not"0".global_liquidity_snapshots/pair_reserves."0"; unpriced-only hournull; mixed priced+unpriced → priced SUM (do not null the whole bucket because one event is unpriced — match volume’s “activity + all unpriced” rule: if any priced, SUM priced).protocol_fee_events/swap_events.(grain, limit)does not hit a new scan; extra?foo=does not create a new cache entry.indexer_protocol_volume.rsstill pass (alias + grain).Playwright (
e2e/protocol-page.spec.ts, 5 workers, smoke)protocol-global-stats.$; tooltip on hover/focus.card-glass; liquidity tile still 24h-only Δ%.Manual / LocalTerra (optional)
--freshindexer: Volume shows idle zeros; Liquidity empty/null until snapshots; Fees idle zeros. After swaps + LP + a wrap/swap fee, Daily volume/fees move; liquidity level moves without matching volume height.Test plan (attack, hack, and abuse)
Treat these as must-fail-closed cases in indexer tests + Vitest. No exploit PoC in the repo — assert 400/ignore/text-escape only.
grain=hourly;DROP,limit[]=,limit=-1,limit=999999,grain=week,metric=javascript:alert(1),../,%00→ 400. Client never fetches.from=/to=/ unbounded ISO range on new routes → 400. No CSV.SUMevents or walk snapshots. Integration EXPLAIN +OVERVIEW_GLOBAL_STATS_LIVE=1still forbidden from 60d-SUM.utc_hour/utc_day/ usd fields containing<script>,"><img onerror=,javascript:render as text. Tooltip DOM is text nodes / React children, notinnerHTML.SUM(total_liquidity_usd)across snapshots in the hour (would inflate TVL). Abuse of “sum the bars” is mitigated by title UTC liquidity and null gaps — do not document bars as additive flow.$0bars (hides oracle holes). Liquidity null must not become$0(hides missing samples / omits as empty book).defillama_daily_*or CGliquidity_in_usd. Llama gem-exclude must not silently apply to Protocol fees/volume.limit_order_fills.commission_amountand swapbook_commission_amount. Placement maker fee islimit_placeonly.protocol_fee_events(existing ingest pins). Series SUM inherits that fail-closed ingest.fee_usdfrom the live hub (#568). Series uses stamped values.?ticker=on series routes ignored or 400; must not change methodology or open SSRF. Oracle chips stay allowlisted on the page (P550-2).card-glass/PriceChart/ trading overlay rejected by verify script greps (same as #668).Verification criteria
Ship is done when all of the following are true:
make verify-issue-<iid>(new script) PASSes: skill/docs invariants greps, no Llama N+1 / no GETswap_events/ no GET snapshot walk / noPriceChart, Vitest Protocol page + copy tests, indexer volume and new liquidity/fees series tests (--test-threads=1), Playwright protocol-page metric+grain (5 workers whenfrontend-dapp/node_modulesexists).make verify-issue-668652667677569586576613614683653631still green./protocol(LocalTerra or Coolify after indexer migrate): Volume default looks like today; Liquidity / Fees buttons switch USD bars; Hourly/Daily/Monthly still labeled; Total liquidity tile still 24h-only; fee panel still trailing 24h/7d/30d.docs/indexer-invariants.mdhas a row for Protocol UTC liquidity/fees grain GET (allowlist, stock vs flow, cache, prune).indexer/into root Docker for cargo.marked as related to #668
marked as related to #652
marked as related to #569
marked as related to #586
marked as related to #576
marked as related to #677
mentioned in merge request !1191
Implemented on
feat/689-protocol-utc-metric-toggle— MR !1191.Acceptance criteria done in this MR:
Not in this MR (issue listed as optional / later):
mentioned in commit
f7b8c8e85dmentioned in issue #703
marked as related to #703