Skip to content

feat(price): Phase 1 — Yadio provider + PriceManager wiring#753

Merged
grunch merged 6 commits into
mainfrom
feat/price-phase-1-yadio
Jun 11, 2026
Merged

feat(price): Phase 1 — Yadio provider + PriceManager wiring#753
grunch merged 6 commits into
mainfrom
feat/price-phase-1-yadio

Conversation

@grunch

@grunch grunch commented May 28, 2026

Copy link
Copy Markdown
Member

Summary

Second atomic PR of the multi-source price rollout (docs/PRICE_PROVIDERS.md §9, Phase 1). Builds on Phase 0 (#747).

  • Wires Phase 0 into the daemon at single-source parity. New PriceManager owns the provider registry + store + HTTP client; the scheduler's job_update_bitcoin_prices now drives PriceManager::update_all, and util::get_bitcoin_price reads through it.
  • YadioProvider lives at src/price/providers/yadio.rs (GET {url}/exrates/BTC, lenient Option<f64> parse so null rates are dropped — preserves the d3f5bfa fix). Captured-payload fixture + parse tests.
  • Legacy config migration (§10.1). When [price] is absent we synthesise a single-yadio config from [mostro].bitcoin_price_api_url + exchange_rates_update_interval_seconds + publish_exchange_rates_to_nostr, so existing settings.toml files keep working byte-for-byte.
  • src/bitcoin_price.rs shrunk to the §9 Phase 1 shim (BitcoinPriceManager::get_price delegates to PriceManager); 423 lines removed, the rest retires in Phase 5.
  • Phase 1 staleness behaviour preserved (§9). get_price logs a single warn! when a value ages past one update interval but still returns it — Phase 1 never refuses an order that would have priced today. Enforcement turns on in Phase 4.

What is NOT in this PR

  • Keyless backups (CoinGecko, currency-api, Blockchain) — Phase 2.
  • El Toque fiat-cross adapter — Phase 3.
  • get_market_quote unification + staleness enforcement — Phase 4.
  • Aggregated-source Nostr publishing polish, info-event exposure, bitcoin_price.rs removal — Phase 5.

Single designated extension point (§5.4 Step 3)

build_provider is the single match arm a new provider touches; the aggregation core, the store, and the scheduler stay untouched. An enabled-but-unimplemented provider fails startup; an unknown id is warn-and-skipped (forward-compat with newer config from an older binary).

Test evidence

  • Phase 1 acceptance (§9):
    • single-yadio tick returns the captured fixture's values verbatim,
    • yadio-down preserves prior values (no panic, no wipe),
    • no enabled providers → NoAPIResponse matching today's "no data yet".
  • Per-provider only / except scoping (§6.6) enforced at the manager boundary.
  • Captured Yadio payload exercises USD/EUR/ARS/CUP plus the BGN: null regression case.
  • Deterministic Nostr source-tag ordering across ticks.

cargo fmt, cargo clippy --all-targets --all-features, cargo test (365 passing) all green.

Test plan

  • cargo fmt --check && cargo clippy --all-targets --all-features -- -D warnings && cargo test green on CI
  • Smoke run: start a node with the legacy settings.toml (no [price] block) — confirm logs show PriceManager installed, price: yadio ok (N currencies) each tick, and orders that exercise get_bitcoin_price (market-priced create) succeed
  • Smoke run: enable the new [price.providers.yadio] block — same behaviour
  • Smoke run: kill the network briefly (iptables block on api.yadio.io) — confirm the store keeps serving last-known-good values, warn! fires once the value ages past one update interval, and orders still succeed (Phase 1 logs but does not refuse)
  • Verify the Nostr kind-30078 event still publishes with source tag yadio (now derived from the contributing-providers list, not hard-coded)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Multi-source price providers with a Yadio adapter and optional publishing of aggregated rates to Nostr; new global price manager and a consumer entry point for BTC price reads.
  • Refactor

    • Replaced legacy in-process cache with centralized provider wiring and aggregation; startup now installs the global manager.
  • Bug Fixes / Improvements

    • Per-tick reporting, staleness handling, contributor-aware aggregation, and clearer outage logging.
  • Documentation

    • Configuration template updated; legacy price key marked deprecated.
  • Tests

    • New provider fixtures and updated aggregation/manager tests.

…ource parity)

Second atomic PR of the multi-source price rollout
(docs/PRICE_PROVIDERS.md §9, Phase 1). Wires Phase 0's foundation into the
daemon at single-source parity: Yadio behind the new abstraction, the rest
of the daemon untouched outside the call sites.

Provider:
- src/price/providers/yadio.rs: YadioProvider via GET {url}/exrates/BTC,
  lenient Option<f64> parse (drops `null` / non-finite rates, preserving
  the db99f94 fix), parse() helper unit-tested against a captured fixture.
- tests/fixtures/price/yadio_btc.json: captured payload exercising
  USD/EUR/ARS/CUP plus the `BGN: null` regression case.

Manager:
- src/price/manager.rs: PriceManager owning Vec<Box<dyn PriceProvider>> +
  PriceStore + reqwest::Client. update_all() polls each provider with its
  own tokio::time::timeout, runs aggregate_tick (applying per-provider
  only/except scoping at the boundary so aggregate.rs stays generic), and
  writes the store; failed providers contribute nothing so prior values
  survive as last-known-good (§6.4). get_price() logs a single warn! when
  a value ages past one update interval but still returns it — Phase 1
  never refuses an order that would have priced today (§9, enforcement
  lands in Phase 4). build_provider() is the single designated
  extension point (§5.4 Step 3); unknown ids in config are warn-and-skip
  (forward-compat); an enabled-but-unimplemented adapter fails startup.
- Nostr publishing preserved with the legacy {"BTC": {ccy: value}}
  wrapper; the `source` tag becomes the contributing provider list
  (deterministically sorted), so today it's "yadio" and Phase 2 widens
  it without changing the schema.
- Process-wide PriceManager::global() singleton via OnceLock; installed
  in main right after settings_init().

Wiring:
- scheduler::job_update_bitcoin_prices now drives PriceManager::update_all;
  interval comes from [price].update_interval_seconds, MIN_INTERVAL guard
  preserved.
- util::get_bitcoin_price reads through PriceManager.
- src/bitcoin_price.rs shrunk to the shim required by §9 Phase 1
  (BitcoinPriceManager::get_price delegates to PriceManager); the rest
  retires in Phase 5.

Config migration (§10.1):
- When [price] is absent, synthesise_legacy_price_settings() builds a
  single yadio provider from bitcoin_price_api_url +
  exchange_rates_update_interval_seconds + publish_exchange_rates_to_nostr,
  so existing settings.toml files keep working byte-for-byte.
- settings.tpl.toml documents the new [price] block as opt-in and marks
  bitcoin_price_api_url deprecated.

Tests (+11, 365 total green):
- single-yadio tick matches today, yadio-down keeps prior values, no
  providers → NoAPIResponse (§9 Phase 1 acceptance criteria).
- only-scoping enforced at the manager boundary (§6.6).
- from_settings rejects enabled-but-not-implemented (CoinGecko etc.),
  ignores unknown ids, skips disabled.
- legacy migration validates; deterministic source-tag ordering.
- Yadio fixture parse + null/non-finite drop + trailing-slash URL +
  parse-error surfacing.

cargo fmt, cargo clippy --all-targets --all-features, cargo test all
green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e891cf6e-f9d7-4da3-9843-98a9be624f1e

📥 Commits

Reviewing files that changed from the base of the PR and between 9a12d01 and be7ecc0.

📒 Files selected for processing (1)
  • src/util.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/util.rs

Walkthrough

Phase 1 price subsystem: adds a process-global PriceManager with multi-provider aggregation and optional Nostr publishing, a Yadio provider adapter and fixture, startup installation with legacy synthesis, aggregation contributor tracking, and replaces the old BitcoinPriceManager with a compatibility shim.

Changes

Price Subsystem Phase 1 Implementation

Layer / File(s) Summary
Configuration and serde changes
settings.tpl.toml, src/config/mod.rs, src/config/types.rs, src/price/config.rs
Template marks mostro.bitcoin_price_api_url deprecated, adds commented multi-source [price] example, adds tests for omitted legacy key, and provides a serde default for legacy URL.
Yadio provider adapter and fixtures
src/price/providers/mod.rs, src/price/providers/yadio.rs, tests/fixtures/price/yadio_btc.json
Adds YadioProvider that fetches GET {url}/exrates/BTC, parses JSON into PerBtc quotes, filters null/non-finite/non-positive values, and includes unit tests plus a fixture.
Aggregation changes (contributors) and tests
src/price/aggregate.rs
AggregateResult now includes contributors: Vec<ProviderId>; aggregate_tick accepts (ProviderId, ProviderQuotes) and computes sources and contributors with outlier-aware filtering; tests updated/extended.
PriceManager core and helpers
src/price/manager.rs
Adds PriceManager singleton, provider wiring, update_all() tick with per-provider timeouts, scoping/aggregation, TickReport (with contributors), staleness warnings, last-known-good preservation, optional Nostr publishing, helpers, and unit tests.
Price module exports and consumer API
src/price/mod.rs
Exports manager and providers, re-exports PriceManager/TickReport/synthesis helper, and adds get_bitcoin_price(currency) delegating to the global manager or returning NoAPIResponse when uninitialized.
Startup integration and manager installation
src/main.rs
Adds install_price_manager() called after settings init; synthesises legacy settings when needed, constructs and installs the global PriceManager, and logs/install errors.
Legacy shim, scheduler, and util migrations
src/bitcoin_price.rs, src/scheduler.rs, src/util.rs
Reduces bitcoin_price.rs to a shim delegating to crate::price::get_bitcoin_price; scheduler uses PriceManager::global() and update_all() with refined logging and interval sourcing; util delegates to the new API and adds yadio_base_url() for URL selection.
ProviderId derive and store helper
src/price/provider.rs, src/price/store.rs
ProviderId gains PartialOrd/Ord; store test helper updated to construct AggregateResult with contributors: Vec::new().

Sequence Diagram

sequenceDiagram
  participant Scheduler
  participant PriceManager
  participant YadioProvider
  participant PriceStore
  Scheduler->>PriceManager: update_all()
  PriceManager->>YadioProvider: fetch (with timeout)
  YadioProvider-->>PriceManager: ProviderQuotes
  PriceManager->>PriceManager: aggregate_tick (compute contributors)
  PriceManager->>PriceStore: upsert aggregated prices
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • MostroP2P/mostro#747: Phase-0 price foundations that this PR wires into Phase 1.
  • MostroP2P/mostro#495: Prior work on Yadio base-URL configurability and legacy bitcoin_price_api_url usage referenced by this change.
  • MostroP2P/mostro#459: Earlier changes to legacy bitcoin_price.rs/util wiring that are impacted by this shim and delegation.

Suggested reviewers

  • arkanoider
  • Catrya
  • BraCR10

Poem

🐰 I hopped through configs, code, and test,
Building managers to gather the best,
I stitched old URLs into a new nest,
Aggregates hum and contributors rest,
Legacy shims wink — the new flow's blessed.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main objective: Phase 1 implementation with the Yadio provider and PriceManager wiring system.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/price-phase-1-yadio

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/scheduler.rs (1)

565-565: ⚡ Quick win

Use the update report to surface provider failures.

At Line 565, _report is dropped. Please emit at least a warn/error from the returned report when no providers succeed (or when failures spike), so price-source outages are visible from scheduler logs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/scheduler.rs` at line 565, The update report returned by
manager.update_all() is currently ignored (assigned to _report); change this so
you capture the returned report (e.g., report) and inspect its success/failure
metrics and per-provider results, then emit a log.warn or log.error from the
scheduler when no providers succeeded or when failure counts spike; locate the
call to manager.update_all() and the binding of _report, read fields on the
returned Report (or its methods) to determine total successes/failures and
provider-level failures, and add conditional logging that surfaces which
providers failed and summary counts so price-source outages are visible in
scheduler logs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/price/manager.rs`:
- Around line 211-213: The Nostr `source` tag is being built from
report.successes (used in publish_rates_to_nostr and sources_to_tag) which can
include providers filtered out or excluded by aggregation; change the pipeline
so provider identities are those that actually contributed to the final
aggregates: modify aggregate_tick (or the aggregation code) to return
contributor metadata (e.g., a Vec of provider IDs or embed a contributors field
in the aggregate type), update callers (publish_rates_to_nostr and any place
building sources_to_tag) to use that returned contributor list instead of
report.successes, and adjust sources_to_tag to accept and format the actual
contributors; ensure function signatures (aggregate_tick,
publish_rates_to_nostr, sources_to_tag) are updated consistently and tests
updated accordingly.
- Around line 273-285: The TooStale branch currently logs on every call
(PriceError::TooStale in the match), which can spam logs; change this to a
one-shot warning by tracking whether we've already warned for that currency
(e.g., add a per-manager or per-store tracking structure like a HashSet/flag for
warned currencies), check that flag before calling warn! in the
Err(PriceError::TooStale) branch (use self.store.snapshot(currency) as now),
emit warn! only if not already warned, and set the flag after warning; also
clear/unset that flag when a fresh entry is served (where
self.store.snapshot(currency) returns a fresh entry) so future staleness can
warn again.

---

Nitpick comments:
In `@src/scheduler.rs`:
- Line 565: The update report returned by manager.update_all() is currently
ignored (assigned to _report); change this so you capture the returned report
(e.g., report) and inspect its success/failure metrics and per-provider results,
then emit a log.warn or log.error from the scheduler when no providers succeeded
or when failure counts spike; locate the call to manager.update_all() and the
binding of _report, read fields on the returned Report (or its methods) to
determine total successes/failures and provider-level failures, and add
conditional logging that surfaces which providers failed and summary counts so
price-source outages are visible in scheduler logs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8c9b5471-d76c-4688-9fa5-ec90d7c76aa6

📥 Commits

Reviewing files that changed from the base of the PR and between 2c022b1 and f8bf7df.

📒 Files selected for processing (10)
  • settings.tpl.toml
  • src/bitcoin_price.rs
  • src/main.rs
  • src/price/manager.rs
  • src/price/mod.rs
  • src/price/providers/mod.rs
  • src/price/providers/yadio.rs
  • src/scheduler.rs
  • src/util.rs
  • tests/fixtures/price/yadio_btc.json

Comment thread src/price/manager.rs
Comment thread src/price/manager.rs
…r outage log

Addresses the three review findings on #753.

1. Nostr `source` tag now reflects **actual contributors**, not the broader
   "polled successfully" list. A provider scoped out by `only`/`except`
   lands in `report.successes` (it did poll OK, so the Phase 2 circuit
   breaker stays happy) but **not** in the new `report.contributors`, so
   the kind-30078 `source` tag never names a provider that didn't move
   the aggregate. Contributors are computed at the manager boundary from
   the post-scope quote maps; tracking outlier-rejected individual quotes
   would require pairing every `Quote` with a `ProviderId` through the
   pure `aggregate_tick`, which is out of scope for Phase 1 and noted as
   a Phase 2 follow-up.

2. `Err(PriceError::TooStale)` in `get_price` no longer logs on every
   call. The single `warned_currencies` HashMap (which also collided
   `Stale` and `SingleSource` flags for the same currency) is split into
   two independent `HashSet`s — `warned_stale` and
   `warned_single_source` — so neither flag clobbers the other. The
   TooStale branch now warns at most once between fresh reads; a fresh
   `Ok` read clears `warned_stale` so a later regression past the TTL
   warns again.

3. The scheduler no longer drops the `TickReport`. PriceManager already
   logs each provider's outcome per tick, so the scheduler only surfaces
   the outage condition that ops cares about: an `error!` when **every**
   provider failed (the store is reading last-known-good across the
   board), and a `warn!` summary on partial outages.

Tests (+2, 367 total):
- `scoped_out_provider_is_success_but_not_contributor`: a provider whose
  only quote is filtered by `only` appears in `report.successes` but not
  in `report.contributors`.
- `stale_warning_is_one_shot_then_re_arms_on_fresh_read`: 10 reads
  against a stale value populate `warned_stale` with one entry; a fresh
  tick + read clears it so future regressions warn again.

cargo fmt, cargo clippy --all-targets --all-features, cargo test all
green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Actionable comments posted: 0

@ermeme ermeme Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes: the source metadata is built from providers that merely returned non-empty quotes before aggregation. That means a provider whose quotes are later discarded as outliers can still be advertised as a contributor in the Nostr source tag. Please derive contributors from the quotes that actually survive aggregation, or have aggregate_tick return provenance so the tag reflects reality.

Addresses the Hermes review on #753: the prior fix computed
contributors at the manager boundary as "post-scope non-empty quotes",
which still claimed a provider as contributor when `combine`'s outlier
filter dropped its value. The Nostr `source` tag advertised providers
that had no effect on the aggregate.

Threads provenance through the pure-function aggregation core instead:

- `aggregate_tick` now takes `&[(ProviderId, ProviderQuotes)]`. Direct
  (PerBtc) quotes and resolved PerBase quotes carry their source id
  through the pipeline.
- `AggregateResult` gains `contributors: Vec<ProviderId>` — sorted,
  deduplicated, and populated by the new `kept_contributors` helper,
  which mirrors `combine`'s "kept" predicate exactly:
  - n≤2: every clean provider contributes,
  - n≥3: only providers whose value is within `outlier_pct` percent of
    the median contribute,
  - bimodal even-length fallback: every clean provider stays (no single
    source is demonstrably the outlier; `combine` falls back to the
    median itself).
- Fiat-cross resolution attributes the resolved candidate to the
  fiat-cross provider only. Anchor contributors (the direct quoters
  whose values built the USD/BTC anchor, say) are an intermediate of
  the cross math, not upstreams of the cross currency.
- Manager derives the tick-wide Nostr `source` tag as the union of
  every per-currency contributor list — using a BTreeSet so the result
  is deterministic without re-sorting in `sources_to_tag`. The
  manager-boundary "non-empty post-scope" heuristic is gone.
- `ProviderId` gains `Ord` / `PartialOrd` so the sorted contributor
  lists are stable.

Tests (+3, 370 total):
- `aggregate_tick_outlier_drops_provider_from_contributors`: three
  providers, one outlier (75_000 against a 50_000/50_200 median) — the
  outlier appears in `sources=3` but NOT in `contributors`.
- `aggregate_tick_bimodal_fallback_keeps_all_clean_contributors`: four
  values across two clusters land in the bimodal fallback; all four
  providers stay contributors.
- `aggregate_tick_non_finite_value_drops_provider_from_contributors`: a
  NaN-emitting provider is dropped from the cleaned set, not advertised
  as a contributor.

Existing aggregate_tick tests assert on the new contributors field;
the unions-partial-coverage test pins the CUP contributor list to
[Yadio (direct), ElToque (fiat-cross)] — confirming the anchor's own
contributors are NOT propagated into the resolved-currency tag.

cargo fmt, cargo clippy --all-targets --all-features, cargo test all
green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/price/aggregate.rs (1)

470-493: ⚡ Quick win

Assert the exact contributor list in the bimodal fallback test.

len() == 4 won't catch a regression where this path returns the right set in nondeterministic order. Since contributors is part of the contract now, it's worth pinning the full sorted vector here too.

🧪 Tighten the assertion
         approx(out["USD"].value, 51.0); // (2+100)/2
-                                        // All four clean providers survive the bimodal fallback.
-        assert_eq!(out["USD"].contributors.len(), 4);
+        // All four clean providers survive the bimodal fallback, in deterministic order.
+        let mut expected = vec![
+            ProviderId::Yadio,
+            ProviderId::CoinGecko,
+            ProviderId::CurrencyApi,
+            ProviderId::Blockchain,
+        ];
+        expected.sort();
+        assert_eq!(out["USD"].contributors, expected);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/price/aggregate.rs` around lines 470 - 493, The test
aggregate_tick_bimodal_fallback_keeps_all_clean_contributors should assert the
exact contributor list rather than just contributors.len(); replace the loose
length check with a deterministic equality check of the sorted contributors
vector so nondeterministic ordering can't hide regressions: extract the provider
identifiers from out["USD"].contributors, sort them deterministically (e.g. by
provider enum/name), and assert they equal the expected list [ProviderId::Yadio,
ProviderId::CoinGecko, ProviderId::CurrencyApi, ProviderId::Blockchain]; update
the test around aggregate_tick and the contributors field to perform that exact
sorted equality check.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/price/aggregate.rs`:
- Around line 470-493: The test
aggregate_tick_bimodal_fallback_keeps_all_clean_contributors should assert the
exact contributor list rather than just contributors.len(); replace the loose
length check with a deterministic equality check of the sorted contributors
vector so nondeterministic ordering can't hide regressions: extract the provider
identifiers from out["USD"].contributors, sort them deterministically (e.g. by
provider enum/name), and assert they equal the expected list [ProviderId::Yadio,
ProviderId::CoinGecko, ProviderId::CurrencyApi, ProviderId::Blockchain]; update
the test around aggregate_tick and the contributors field to perform that exact
sorted equality check.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cffa729e-e165-4367-99f6-43c14cec6b3f

📥 Commits

Reviewing files that changed from the base of the PR and between 79f12c7 and f98f093.

📒 Files selected for processing (4)
  • src/price/aggregate.rs
  • src/price/manager.rs
  • src/price/provider.rs
  • src/price/store.rs
✅ Files skipped from review due to trivial changes (2)
  • src/price/provider.rs
  • src/price/store.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/price/manager.rs

Replace the loose `.len() == 4` check with the deterministic sorted-list
assertion. `kept_contributors` -> `dedup_sort` orders by the derived
`Ord` on `ProviderId` (which follows enum-variant declaration order:
Yadio, CoinGecko, CurrencyApi, Blockchain, ElToque), so the expected
result is fully determined. A future refactor that perturbs ordering or
silently drops a contributor will now be caught here rather than slipping
past the count.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@arkanoider

Copy link
Copy Markdown
Collaborator

tACK for me! One detail, maybe it's intended, but from specs seems not. Now we still have to leave the legacy [mostro].bitcoin_price_api_url in settings.toml file to make mostro start, otherwise it will close with an error related to that, because it's still mandatory. Maybe a flag to keep in mind for next step.

@codaMW

codaMW commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Checked out the branch locally and ran all four smoke tests from the test plan against a live stack (bitcoind regtest + LND + nostr-rs-relay + local Mostro daemon).

55 price tests pass, zero failures.

Smoke test 1: legacy config (no [price] block)

Confirmed PriceManager installed on startup and price: yadio ok (127 currencies) on the first tick. Legacy config migration works correctly existing settings.toml files with no [price] block start cleanly.

Smoke test 2: market-priced order

Submitted a market-priced sell order (amount=0) via mostro-cli. The order reached the daemon and triggered get_bitcoin_price through the new PriceManager path. The InvalidTradeIndex response is a pre-existing CLI state issue unrelated to this PR price lookup worked correctly.

Smoke test 3: Yadio network failure

Blocked the Yadio endpoint. On the next scheduled tick the daemon logged:

WARN  price: yadio error: http error: yadio GET https://api.yadio.io/exrates/BTC: error sending request
WARN  price: tick produced no fresh aggregates keeping last-known-good
ERROR price: all 1 providers failed this tick serving last-known-good [yadio=http error: ...]

No panic, no wipe, last-known-good preserved exactly as specified in §9. The daemon continued running and would have served stale-but-valid prices to any incoming order.

Smoke test 4: Nostr publishing

Exchange rates published successfully to both configured relays:

INFO price: published exchange rates to Nostr (127 currencies).
     Output: success: {RelayUrl("wss://relay.damus.io"), RelayUrl("wss://nos.lol")}, failed: {}

One observation confirming arkanoider's finding

[mostro].bitcoin_price_api_url remains required even with this PR present. Starting the daemon without it produces a config validation error. This is expected behavior for Phase 1 per the spec but worth adding an explicit comment in settings.tpl.toml marking it as deprecated-but-still-required until Phase 5 removes it, so operators are not surprised.


tACK - all four smoke tests pass on a live stack. The last-known-good behavior under network failure is solid and matches the spec precisely.

mostronatorcoder[bot]
mostronatorcoder Bot previously approved these changes Jun 8, 2026

@mostronatorcoder mostronatorcoder Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed on current head c391414. I re-checked the previously raised review points and the ones that were still relevant do look addressed on this head: the Nostr source tag now uses actual aggregate contributors instead of all successful providers, and stale-price warnings are guarded as one-shot transitions instead of spamming on every read. I also read through the PR discussion / comments to make sure there were no unresolved change requests left behind. Local validation on this head passed: cargo test price -- --nocapture and cargo clippy --all-targets --all-features -- -D warnings both completed successfully.

…adio URL

Human-review follow-ups on Phase 1 (spec §10.1 backward compatibility):

- types.rs: add #[serde(default)] to bitcoin_price_api_url so a settings.toml
  that has migrated to a [price] block may omit the deprecated key instead of
  failing deserialization and aborting startup (arkanoider, codaMW). Field
  stays — util.rs and install_price_manager() still read it until Phase 4/5.
- settings.tpl.toml: document the key's full lifecycle (now optional, still
  read by the live /convert path until Phase 4, removed in Phase 5).
- util.rs: route the live /convert + /currencies path through a yadio_base_url()
  helper that prefers [price.providers.yadio].url when a [price] block is
  present, falling back to the legacy key. Stops the live and cached paths
  silently hitting different Yadio bases when only the new key is customised.
- config.rs / manager.rs: point the three [price] validation errors at
  docs/PRICE_PROVIDERS.md §7 and make the unimplemented-provider message
  actionable.
- tests: prove a [mostro] block without bitcoin_price_api_url deserializes to
  the default URL, and that legacy synthesis is identical whether the key is
  present-at-default or omitted.

Does not touch price/aggregate.rs, price/store.rs, or the scheduler tick
(§5.4 extension-contract invariant holds).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@ermeme ermeme Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strict review after reading the discussion and prior commits:

  • The legacy/live Yadio URL helper can emit broken URLs. It prefers the [price] Yadio URL whenever that section exists, even if the provider is disabled or blank, and it only calls trim(), not trim_end_matches('/'). That means a configured trailing slash turns into //convert / //currencies, while a disabled or empty Yadio config can suppress the legacy fallback entirely. Please gate on a usable URL and normalize it the same way YadioProvider::new does.

Comment thread src/util.rs Outdated
ermeme review on PR #753: the live-path Yadio URL helper could emit broken
URLs. It only `trim()`med (not `trim_end_matches('/')` like
`YadioProvider::new`), so a configured trailing slash produced `//convert` /
`//currencies` and diverged from the aggregate path the helper exists to match.
It also used the `[price.providers.yadio]` URL even when the provider was
disabled, overriding the legacy `bitcoin_price_api_url` fallback.

- Normalize the chosen URL with `trim_end_matches('/')`, matching
  `YadioProvider::new` (applied to the legacy fallback too).
- Use the provider URL only when *usable*: enabled and non-empty after
  normalization; otherwise fall back to the legacy key.

Split the selection out into a pure `select_yadio_base_url` so it is unit
testable without the write-once global `Settings`; add tests for the
prefer-enabled, strip-trailing-slash, and fall-back-when-unusable cases.

(The review's "empty config suppresses the fallback" point did not hold — the
existing `!url.is_empty()` guard already fell through — but the trailing-slash
and disabled-provider issues were real.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@grunch grunch merged commit c7db272 into main Jun 11, 2026
8 checks passed
@grunch grunch deleted the feat/price-phase-1-yadio branch June 11, 2026 19:01
grunch added a commit that referenced this pull request Jun 12, 2026
…skew (#774)

PR #753 removed the BITCOIN_PRICES static when it migrated price reads
to the PriceManager, while PR #770 (merged independently) added
BitcoinPriceManager::set_price_for_test writing to that static — the
combination doesn't compile under cargo test on main.

Re-point the test seam at a small cfg(test) override map consulted by
price::get_bitcoin_price before the global manager, so unit tests keep
seeding deterministic prices without installing the global PriceManager
(whose OnceLock would leak one test's configuration into the rest of
the binary).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
grunch added a commit that referenced this pull request Jun 15, 2026
* fix(price): repair test-only price seeding broken by #753/#770 merge skew

PR #753 removed the BITCOIN_PRICES static when it migrated price reads
to the PriceManager, while PR #770 (merged independently) added
BitcoinPriceManager::set_price_for_test writing to that static — the
combination doesn't compile under cargo test on main.

Re-point the test seam at a small cfg(test) override map consulted by
price::get_bitcoin_price before the global manager, so unit tests keep
seeding deterministic prices without installing the global PriceManager
(whose OnceLock would leak one test's configuration into the rest of
the binary).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(bond): Phase 7 — maker timeout slash

Fill the maker-responsible rows of the §9.2 responsibility table: a
waiting-state timeout now slashes the maker's bond when the maker is
the responsible party, gated per posting role (apply_to must cover the
responsible side — taker since Phase 4, maker since Phase 7).

- slash_or_release_on_timeout resolves the responsible bond through the
  range-aware resolve_slash_target (the maker bond of a range order
  lives on the range root), and gates the slash on applies_to_maker()
  / applies_to_taker() per the responsible side.
- A maker-responsible timeout on a range order goes through the Phase 6
  partial-slash path: record_maker_slice_slash inserts a proportional
  child row with reason=Timeout and the parent HTLC stays Locked; the
  single settle happens at range close. The helper now returns whether
  it inserted, so the BondSlashed notice fires exactly once (a
  scheduler retry that finds the child already recorded reports None).
- The scheduler's terminal cancel branch now runs
  resolve_range_maker_bond_at_close_or_warn after the Canceled status
  persists, so a slashed range settles and distributes promptly instead
  of waiting for the 5-minute reconciliation sweep (which remains the
  backstop). The republish branch never closes — the maker stays
  committed there.
- The timeout release loop retains a range maker parent bond (resolved
  only at range close), alongside the existing republish carve-out.

Tests mirror Phase 4 from the maker side: non-range sell/buy slashes,
the per-role apply_to gate, and the range path (proportional child,
parent stays Locked, root resolution from a descendant slice,
per-slice idempotency without re-notification).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(bond): mark Phase 7 shipped + implementation notes; fix stale Phase 4.5 row

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

3 participants