Multi-currency Phase 4 — rate fetcher with Kraken/Coinbase/CoinGecko fallback

src/rates.rs adds an in-memory rate cache (60s TTL) with a 3-source
fallback chain. AppState gains `rates: Arc<RateCache>`. Manual pins
via the settings table override the chain — used by tests for
deterministic conversions and by operators during maintenance
windows.

Admin endpoints:
- GET /v1/admin/rates: cache snapshot
- POST /v1/admin/rates/refresh: force re-fetch (audit-logged)

Two new tests (network-free, manual-pin path):
- rate_cache_honors_manual_pin_from_settings
- admin_rates_endpoint_reflects_manual_pin

Test count: 36 (was 34).
This commit is contained in:
Grant
2026-05-08 12:16:22 -05:00
parent 356d17fdde
commit eb885502ba
7 changed files with 459 additions and 0 deletions
+11
View File
@@ -71,6 +71,7 @@ pub mod tier;
pub mod validate;
pub mod community;
pub mod db_info;
pub mod rates_admin;
pub mod recover;
pub mod webhook;
pub mod webhook_deliveries;
@@ -103,6 +104,10 @@ pub struct AppState {
/// Keysat-licenses-Keysat tier. Read at boot, swapped when the
/// operator activates a fresh license via the admin endpoint.
pub self_tier: Arc<RwLock<crate::license_self::Tier>>,
/// BTC/fiat rate cache for multi-currency products. See
/// src/rates.rs. Process-global so cached rates aren't refetched
/// per-request.
pub rates: Arc<crate::rates::RateCache>,
}
impl AppState {
@@ -325,6 +330,12 @@ pub fn router(state: AppState) -> Router {
// Database health snapshot — operator-facing sanity check
// against the catastrophic-loss risk; see db_info.rs.
.route("/v1/admin/db-info", get(db_info::get))
// BTC/fiat rate cache — operator-facing view of what the
// daemon would quote for fiat-priced products. See
// src/rates.rs for the source chain (Kraken → Coinbase
// → CoinGecko) and TTL caching semantics.
.route("/v1/admin/rates", get(rates_admin::get))
.route("/v1/admin/rates/refresh", post(rates_admin::refresh))
// Opt-in community analytics. Off by default; toggling on
// requires the operator to confirm a collector URL.
.route(