Skip to content

Commit 9a12d01

Browse files
grunchclaude
andcommitted
fix(price): make legacy bitcoin_price_api_url optional + unify live Yadio 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>
1 parent c391414 commit 9a12d01

6 files changed

Lines changed: 128 additions & 10 deletions

File tree

settings.tpl.toml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,15 @@ pow = 0
5656
publish_mostro_info_interval = 300
5757
# Bitcoin price API base URL.
5858
# DEPRECATED: prefer `[price.providers.yadio].url` in the new multi-source
59-
# `[price]` block below. Still honoured for backward compatibility when
60-
# `[price]` is absent (see docs/PRICE_PROVIDERS.md §10.1).
59+
# `[price]` block below (see docs/PRICE_PROVIDERS.md §10.1). Lifecycle:
60+
# (a) Now OPTIONAL — once you configure a `[price]` block you may delete this
61+
# key. When it is absent AND `[price]` is also absent, the default
62+
# "https://api.yadio.io" is used for the legacy single-source synthesis.
63+
# (b) Still read by the live market-quote path (/convert) until Phase 4. To
64+
# avoid the live and cached paths hitting different Yadio URLs, the live
65+
# path prefers `[price.providers.yadio].url` when a `[price]` block is
66+
# present and only falls back to this key otherwise.
67+
# (c) Fully removed in Phase 5.
6168
bitcoin_price_api_url = "https://api.yadio.io"
6269
# Fiat currencies accepted for orders - leave empty [] to accept all fiat currencies
6370
fiat_currencies_accepted = ['USD', 'EUR', 'ARS', 'CUP']

src/config/mod.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,4 +233,72 @@ mod tests {
233233
);
234234
assert_eq!(mostro_settings.mostro.max_orders_per_response, 10);
235235
}
236+
237+
// Same as MOSTRO_SETTINGS but with `bitcoin_price_api_url` omitted — the
238+
// shape an operator who has migrated to a `[price]` block and deleted the
239+
// deprecated key would have (spec §10.1).
240+
const MOSTRO_SETTINGS_NO_PRICE_URL: &str = r#"[mostro]
241+
fee = 0
242+
max_routing_fee = 0.002
243+
max_order_amount = 1000000
244+
min_payment_amount = 100
245+
expiration_hours = 24
246+
max_expiration_days = 15
247+
expiration_seconds = 900
248+
user_rates_sent_interval_seconds = 3600
249+
publish_relays_interval = 60
250+
pow = 0
251+
publish_mostro_info_interval = 300
252+
fiat_currencies_accepted = ['USD', 'EUR', 'ARS', 'CUP']
253+
max_orders_per_response = 10
254+
dev_fee_percentage = 0.30"#;
255+
256+
#[test]
257+
fn test_mostro_settings_without_bitcoin_price_api_url_defaults() {
258+
// A settings.toml that omits the deprecated key must still
259+
// deserialize (it is `#[serde(default)]`), falling back to the same
260+
// URL the `Default` impl uses.
261+
let parsed: StubSettingsMostro =
262+
toml::from_str(MOSTRO_SETTINGS_NO_PRICE_URL).expect("must deserialize without the key");
263+
assert_eq!(
264+
parsed.mostro.bitcoin_price_api_url, "https://api.yadio.io",
265+
"omitted legacy key must fall back to the default URL"
266+
);
267+
}
268+
269+
#[test]
270+
fn test_legacy_synthesis_same_whether_key_present_or_defaulted() {
271+
// Legacy synthesis (`[price]` absent) must produce the same
272+
// single-yadio config whether the deprecated key was present at its
273+
// default value or omitted entirely (spec §10.1 byte-for-byte
274+
// compatibility).
275+
let omitted: StubSettingsMostro =
276+
toml::from_str(MOSTRO_SETTINGS_NO_PRICE_URL).expect("deserialize (omitted)");
277+
let present: StubSettingsMostro =
278+
toml::from_str(MOSTRO_SETTINGS).expect("deserialize (present)");
279+
280+
let synth = |m: &MostroSettings| {
281+
crate::price::synthesise_legacy_price_settings(
282+
&m.bitcoin_price_api_url,
283+
m.exchange_rates_update_interval_seconds,
284+
m.publish_exchange_rates_to_nostr,
285+
)
286+
};
287+
let from_omitted = synth(&omitted.mostro);
288+
let from_present = synth(&present.mostro);
289+
290+
// Exactly one provider (yadio) in both, with the default URL.
291+
for cfg in [&from_omitted, &from_present] {
292+
assert_eq!(cfg.providers.len(), 1);
293+
let yadio = cfg.providers.get("yadio").expect("yadio provider present");
294+
assert!(yadio.enabled);
295+
assert_eq!(yadio.url, "https://api.yadio.io");
296+
}
297+
// And the synthesised cadence / publish flag match across the two.
298+
assert_eq!(
299+
from_omitted.update_interval_seconds,
300+
from_present.update_interval_seconds
301+
);
302+
assert_eq!(from_omitted.publish_to_nostr, from_present.publish_to_nostr);
303+
}
236304
}

src/config/types.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,15 @@ pub struct MostroSettings {
340340
pub pow: u8,
341341
/// Publish mostro info interval
342342
pub publish_mostro_info_interval: u32,
343-
/// Bitcoin price API base URL
343+
/// Bitcoin price API base URL.
344+
///
345+
/// DEPRECATED (spec §10.1): superseded by `[price.providers.yadio].url`.
346+
/// `#[serde(default)]` so a `settings.toml` that has migrated to a
347+
/// `[price]` block may omit this key entirely without failing
348+
/// deserialization. Still read by the live `/convert` path
349+
/// (`src/util.rs`) and by `install_price_manager()` legacy synthesis when
350+
/// `[price]` is absent, so the field itself stays until Phase 4/5.
351+
#[serde(default = "default_bitcoin_price_api_url")]
344352
pub bitcoin_price_api_url: String,
345353
/// Fiat currencies accepted for orders (empty list accepts all)
346354
pub fiat_currencies_accepted: Vec<String>,
@@ -365,6 +373,12 @@ pub struct MostroSettings {
365373
pub exchange_rates_update_interval_seconds: u64,
366374
}
367375

376+
fn default_bitcoin_price_api_url() -> String {
377+
// Matches the `Default` impl below so an omitted legacy key and an
378+
// explicit one behave identically for legacy synthesis (spec §10.1).
379+
"https://api.yadio.io".to_string()
380+
}
381+
368382
fn default_publish_exchange_rates() -> bool {
369383
true // Enable by default for censorship resistance
370384
}

src/price/config.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,14 @@ impl ProviderConfig {
7474
pub fn validate(&self, id: &str) -> Result<(), String> {
7575
if self.only.is_some() && self.except.is_some() {
7676
return Err(format!(
77-
"price provider '{id}': `only` and `except` are mutually exclusive"
77+
"price provider '{id}': `only` and `except` are mutually exclusive \
78+
(see docs/PRICE_PROVIDERS.md §7)"
7879
));
7980
}
8081
if self.enabled && self.url.trim().is_empty() {
8182
return Err(format!(
82-
"price provider '{id}': enabled provider must have a non-empty `url`"
83+
"price provider '{id}': enabled provider must have a non-empty `url` \
84+
(see docs/PRICE_PROVIDERS.md §7)"
8385
));
8486
}
8587
Ok(())

src/price/manager.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,9 @@ fn build_provider(id: ProviderId, cfg: &ProviderConfig) -> Result<Box<dyn PriceP
453453
| ProviderId::CurrencyApi
454454
| ProviderId::Blockchain
455455
| ProviderId::ElToque => Err(format!(
456-
"price: provider `{id}` is configured but not yet implemented in this release"
456+
"price: provider `{id}` is configured (enabled) but not yet implemented in \
457+
this release — disable it or remove it from `[price.providers]` \
458+
(see docs/PRICE_PROVIDERS.md §7)"
457459
)),
458460
}
459461
}

src/util.rs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,37 @@ const MAX_RETRY: u16 = 4;
3636
// Redefined for convenience
3737
type OrderKind = mostro_core::order::Kind;
3838

39+
/// Resolve the Yadio base URL for the live market-quote path (`/convert`,
40+
/// `/currencies`).
41+
///
42+
/// Phase 1 transition (spec §10.1): the cached aggregate path reads
43+
/// `[price.providers.yadio].url`, but this live path historically read the
44+
/// legacy `[mostro].bitcoin_price_api_url`. If an operator customises only
45+
/// the new key, the two paths would silently hit different Yadio bases.
46+
/// Prefer the configured Yadio provider URL when a `[price]` block is present
47+
/// (falling back to the legacy key otherwise). Phase 4 removes this live HTTP
48+
/// path entirely, at which point the legacy key only feeds legacy synthesis.
49+
fn yadio_base_url() -> String {
50+
if let Some(price) = Settings::get_price() {
51+
if let Some(yadio) = price
52+
.providers
53+
.get(&crate::price::ProviderId::Yadio.to_string())
54+
{
55+
let url = yadio.url.trim();
56+
if !url.is_empty() {
57+
return url.to_string();
58+
}
59+
}
60+
}
61+
Settings::get_mostro().bitcoin_price_api_url.clone()
62+
}
63+
3964
pub async fn retries_yadio_request(
4065
req_string: &str,
4166
fiat_code: &str,
4267
) -> Result<(Option<reqwest::Response>, bool), MostroError> {
4368
// Get Fiat list and check if currency exchange is available
44-
let mostro_settings = Settings::get_mostro();
45-
let api_req_string = format!("{}/currencies", mostro_settings.bitcoin_price_api_url);
69+
let api_req_string = format!("{}/currencies", yadio_base_url());
4670
let fiat_list_check = HTTP_CLIENT
4771
.get(api_req_string)
4872
.send()
@@ -78,10 +102,11 @@ pub async fn get_market_quote(
78102
premium: i64,
79103
) -> Result<i64, MostroError> {
80104
// Add here check for market price
81-
let mostro_settings = Settings::get_mostro();
82105
let req_string = format!(
83106
"{}/convert/{}/{}/BTC",
84-
mostro_settings.bitcoin_price_api_url, fiat_amount, fiat_code
107+
yadio_base_url(),
108+
fiat_amount,
109+
fiat_code
85110
);
86111
info!("Requesting API price: {}", req_string);
87112

0 commit comments

Comments
 (0)