Skip to content

refactor: decompose price calculation into orthogonal components#65

Merged
suchapalaver merged 2 commits into
mainfrom
refactor/split-price-calculator
May 26, 2026
Merged

refactor: decompose price calculation into orthogonal components#65
suchapalaver merged 2 commits into
mainfrom
refactor/split-price-calculator

Conversation

@suchapalaver

Copy link
Copy Markdown
Contributor

Summary

PriceCalculator is now a thin orchestration façade over five focused submodules instead of one type that mixed scanning, decoding, decimals fetching/caching, normalization, and aggregation. This makes the price domain testable without a provider and easier to extend for alternate metadata or aggregation strategies. PriceSource stays the DEX extension point and the public API is unchanged. Closes #26.

What's in the diff

  • src/price/extractor.rs (new) — pure extract_swaps that walks logs through PriceSource::extract_swap_from_log + should_include_swap, tracing decode/invalid-swap errors without aborting.
  • src/price/normalize.rs (new) — pure normalize_against_pair (target/USDC, both directions), normalize_swap, and the involves_pair relevance predicate.
  • src/price/aggregator.rs (new) — PriceAggregator folds normalized swaps into a TokenPriceResult.
  • src/price/decimals.rs (new) — provider-free TokenDecimalsCache plus TokenMetadataProvider (single + parallel batch fetch, CallBatchLayer-friendly).
  • src/price/scanner.rs (new) — SwapLogScanner builds the router/topics filter and drives the chunked scan.
  • src/price/calculator.rs — rewritten as orchestration; keeps the public PriceCalculator, TokenPriceResult, RawSwapResult and their methods. Review first: process_gap_for_price and extract_raw_swaps.
  • src/price/mod.rs — declares the new private submodules.

Acceptance check (from #26)

  • Price aggregation testable with in-memory SwapData, no provider — aggregator.rs + normalize.rs unit tests.
  • Token decimal cache testable independently — decimals.rs TokenDecimalsCache tests.
  • PriceCalculator is smaller and reads as orchestration.
  • Existing price tests pass (range-cache + TokenPriceResult tests retained).
  • examples/custom_dex_integration.rs still compiles — cargo check --examples --all-features.
  • PriceSource remains the documented extension point.
  • new, with_config, calculate_price_between_blocks, raw-swap API kept source-compatible.
  • Batch decimals fetch + CallBatchLayer compatibility preserved (pair-scoped batch retained).

Test plan

  • cargo test --all-features — 509 lib + all integration suites pass
  • cargo test / cargo test --no-default-features — pass
  • cargo clippy --all-targets --all-features -- -D warnings clean (all three feature combos)
  • cargo fmt --check clean
  • cargo check --examples --all-features clean

Self-review notes

Pre-commit /code-review (5 angles, extra-high effort) found the refactor behavior-faithful. One pre-existing operator-facing gap surfaced — a persistent decimals-fetch failure caches an empty/partial TokenPriceResult for the range, so a later same-range query returns the bogus zero without rescanning. This is not introduced here (the original process_gap_for_price cached skipped-swap results identically), so it's left out of scope and filed as a follow-up.

`PriceCalculator` is now a thin orchestration façade over five focused
submodules: `SwapLogScanner` builds the price-source filter and drives
the chunked `eth_getLogs` scan, `extract_swaps` is a pure walk that
turns logs into accepted `SwapData`, `TokenMetadataProvider` reads
ERC-20 decimals into a provider-free `TokenDecimalsCache`,
`normalize_against_pair`/`normalize_swap` are pure amount conversions,
and `PriceAggregator` folds normalized swaps into a `TokenPriceResult`.
`PriceSource` remains the DEX extension point, and the public surface —
`new`, `with_config`, `calculate_price_between_blocks`, and
`extract_raw_swaps` — plus the `TokenPriceResult` and `RawSwapResult`
result types are unchanged.

The motivation is that price calculation previously mixed scanning,
decoding, decimals fetching/caching, normalization, and aggregation in
one type, making it hard to test or extend. Each step is now
independently unit-testable without a provider: swap extraction runs
against synthetic logs, relevance and normalization cover both swap
directions and missing-pair cases, aggregation folds in-memory amounts,
and the decimals cache is exercised directly. The pair-scoped batch
decimals fetch and the per-swap failure tolerance are preserved.

Closes #26.
These two cases asserted on raw `U256` division into `f64` without
calling any semioscan code, so they tested a dependency rather than the
crate's own normalization. The behaviour they stood in for is now
covered directly by the `normalize` module's unit tests.
@suchapalaver

Copy link
Copy Markdown
Contributor Author

Self-review pass

Pre-commit /code-review (5 angles, extra-high effort) + post-PR /pr-review (independent structural audit), findings triaged below.

# Finding Status
1 Two price tests asserted on raw U256f64 arithmetic — testing a dependency, not crate code (CLAUDE.md violation) Fixed: baf2c45 (removed; normalize module now covers this)
2 Persistent decimals-fetch failure caches an empty/partial result for the range, defeating retry on a later same-range query Filed: #66 — pre-existing (original process_gap_for_price cached skipped-swap results identically), out of scope for a behavior-preserving refactor
3 TokenMetadataProvider fetch/skip/retry path is untested (only the TokenDecimalsCache wrapper is) Skipped: the acceptance criterion ("token decimal cache testable independently") is met by the cache tests; the fetcher is a thin wrapper over LazyToken::decimals() and the crate keeps RPC-dependent paths in examples/ with no mock provider
4 PriceAggregator is a thin wrapper; TokenPriceResult could live in aggregator.rs Skipped (nit): TokenPriceResult is a public re-exported type; keeping it in calculator.rs avoids churn, and the named fold reads clearly in orchestration

Both reviews independently concluded the decomposition is complete and behavior-preserving (no dead code, no leaked internal pub, public API and mod.rs workflow rustdoc still accurate). No blockers remaining — confident to ship.

@suchapalaver suchapalaver merged commit 4a27eae into main May 26, 2026
11 checks passed
@suchapalaver suchapalaver deleted the refactor/split-price-calculator branch May 26, 2026 20:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant