feat: fiat price feed with per-feed operator control (GET /api/v1/prices)

Cached crypto→fiat price feed, refreshed by a background task and served from an
in-memory snapshot — no upstream call on the request path. Public and
feature-flagged: when FEATURE_PRICES is off, /prices is 404 and no upstream is
ever contacted.

Per-feed control, as a public server needs: PRICES_ASSETS narrows which feeds an
instance quotes (unset = all supported; an explicit list serves a subset; empty
serves none), validated fail-closed against SUPPORTED_PRICE_ASSETS. Provider
(PRICES_PROVIDER, coingecko) and currency (PRICES_CURRENCY, usd) are configurable;
the refresh interval is clamped to a provider-friendly minimum.

- infra/prices: PriceClient (feed whitelist → provider coin ids), bounded by the
  HTTP client timeout; non-finite/negative/missing quotes are skipped, not faked.
  A fetch failure keeps the last good snapshot. Drift guard: a test asserts the
  provider mapping covers exactly SUPPORTED_PRICE_ASSETS.
- main: background refresh task only when enabled and at least one feed is set.
- AppState.prices snapshot; handler 404s when disabled.
- openapi (prices tag); unit tests + L1 test over both feature-flag states.
This commit is contained in:
jwinterm
2026-06-25 16:41:40 -04:00
parent 1e2bdf3f16
commit d8b6058d40
11 changed files with 464 additions and 1 deletions
+3
View File
@@ -17,6 +17,7 @@ use utoipa::OpenApi;
paths(
crate::api::health::health,
crate::api::capabilities::capabilities,
crate::api::prices::prices,
crate::api::auth::pow_challenge,
crate::api::auth::extension_register,
crate::api::auth::check_restore,
@@ -64,6 +65,7 @@ use utoipa::OpenApi;
crate::api::capabilities::ChainCapabilities,
crate::api::capabilities::ChainCapability,
crate::api::capabilities::FeatureCapabilities,
crate::api::prices::PricesResponse,
crate::api::auth::AuthResponse,
crate::api::auth::UserInfo,
crate::api::auth::AssetPublicKey,
@@ -133,6 +135,7 @@ use utoipa::OpenApi;
)),
tags(
(name = "system", description = "Service health and metadata."),
(name = "prices", description = "Fiat price feed (cached; per-feed configurable; feature-flagged)."),
(name = "auth", description = "Wallet authentication: registration, restore, refresh, sessions, Nostr identity."),
(name = "btc_ltc", description = "Bitcoin/Litecoin chain access via Electrum (balance, UTXOs, history, tip, fee, broadcast)."),
(name = "xmr_wow", description = "Monero/Wownero chain access via LWS (register, balance, history, unspent/random outs, submit, height, confirmations)."),