From b7e3199200edadaf64872ea02913cb374c41031f Mon Sep 17 00:00:00 2001 From: jwinterm Date: Tue, 23 Jun 2026 10:39:25 -0400 Subject: [PATCH] feat: wire chain clients into AppState (feature-gated) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add ChainClients (infra/chains.rs): per-chain data-source handles built once at startup from config and shared via AppState. Each client is present only when its chain feature flag is on, and a malformed chain endpoint now fails startup (fail-closed) rather than the first request. Cloneable (grin's session state is shared via Arc) so it lives in the cloneable AppState. Foundation for the wallet-handlers phase — handlers will read these clients from State. No endpoints yet (openapi unchanged). --- src/infra/chains.rs | 53 +++++++++++++++++++++++++++++++++++++++++++++ src/infra/mod.rs | 1 + src/lib.rs | 3 +++ src/main.rs | 5 ++++- tests/common/mod.rs | 5 ++++- 5 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 src/infra/chains.rs diff --git a/src/infra/chains.rs b/src/infra/chains.rs new file mode 100644 index 0000000..1cd5d0b --- /dev/null +++ b/src/infra/chains.rs @@ -0,0 +1,53 @@ +//! Per-chain client handles, constructed once at startup and shared via +//! [`crate::AppState`]. Each chain's client is config-pointed at an external node +//! service (Fulcrum/Electrum, monero/wownero-lws, grin-wallet + grin node) and is +//! present only when that chain's feature flag is enabled. A malformed endpoint +//! fails startup (fail-closed), not the first request. + +use std::sync::Arc; + +use crate::config::Config; +use crate::error::AppError; +use crate::infra::electrum::ElectrumClient; +use crate::infra::grin::GrinClient; +use crate::infra::lws::LwsClient; + +/// The enabled chains' data-source clients. Cloneable (the heavyweight grin +/// session state is shared via `Arc`) so it can live in the cloneable `AppState`. +#[derive(Clone)] +pub struct ChainClients { + pub btc: Option, + pub ltc: Option, + pub xmr: Option, + pub wow: Option, + pub grin: Option>, +} + +impl ChainClients { + /// Build clients for the enabled chains from config. + pub fn from_config(cfg: &Config) -> Result { + let flags = &cfg.features.chains; + Ok(Self { + btc: flags + .btc + .then(|| ElectrumClient::bitcoin(&cfg.chains.btc)) + .transpose()?, + ltc: flags + .ltc + .then(|| ElectrumClient::litecoin(&cfg.chains.ltc)) + .transpose()?, + xmr: flags + .xmr + .then(|| LwsClient::monero(&cfg.chains.xmr)) + .transpose()?, + wow: flags + .wow + .then(|| LwsClient::wownero(&cfg.chains.wow)) + .transpose()?, + grin: flags + .grin + .then(|| GrinClient::new(&cfg.chains.grin).map(Arc::new)) + .transpose()?, + }) + } +} diff --git a/src/infra/mod.rs b/src/infra/mod.rs index 189b1b9..f28fab5 100644 --- a/src/infra/mod.rs +++ b/src/infra/mod.rs @@ -1,6 +1,7 @@ //! Infrastructure layer: adapters for external services (database, chain nodes, //! light-wallet servers). Chain clients are wired in as they land. +pub mod chains; pub mod db; pub mod electrum; pub mod grin; diff --git a/src/lib.rs b/src/lib.rs index 68fec43..b6ec84c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,7 @@ use tokio::sync::RwLock; use crate::config::Config; use crate::core::session::{SessionManager, WebChallenge}; +use crate::infra::chains::ChainClients; use crate::infra::db::Database; /// Shared application state injected into handlers via `State>`. @@ -31,6 +32,8 @@ pub struct AppState { pub config: Config, pub db: Database, pub sessions: SessionManager, + /// Per-chain data-source clients (present only for enabled chains). + pub chains: ChainClients, /// In-memory website-auth challenges, keyed by nonce. Single-node store; /// a shared/stateless variant is the load-balanced-fleet path. pub web_challenges: Arc>>, diff --git a/src/main.rs b/src/main.rs index ae9fcf2..23661ad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,8 @@ use std::sync::Arc; use sqlx::postgres::PgPoolOptions; use smirk_backend_core::{ - build_router, config::Config, core::session::SessionManager, infra::db::Database, AppState, + build_router, config::Config, core::session::SessionManager, infra::chains::ChainClients, + infra::db::Database, AppState, }; #[tokio::main] @@ -33,12 +34,14 @@ async fn main() -> Result<(), Box> { config.secrets.ip_salt.clone(), ); let sessions = SessionManager::new(&config.auth.jwt_secret, config.auth.jwt_expiry_hours); + let chains = ChainClients::from_config(&config)?; let addr = format!("{}:{}", config.server_host, config.server_port); let state = Arc::new(AppState { config, db, sessions, + chains, web_challenges: Arc::default(), }); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 938f772..da1289b 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -21,7 +21,8 @@ use tower::ServiceExt; use uuid::Uuid; use smirk_backend_core::{ - build_router, config::Config, core::session::SessionManager, infra::db::Database, AppState, + build_router, config::Config, core::session::SessionManager, infra::chains::ChainClients, + infra::db::Database, AppState, }; static INIT_ENV: Once = Once::new(); @@ -68,10 +69,12 @@ pub async fn try_app() -> Option { config.secrets.ip_salt.clone(), ); let sessions = SessionManager::new(&config.auth.jwt_secret, config.auth.jwt_expiry_hours); + let chains = ChainClients::from_config(&config).expect("build chain clients"); let state = Arc::new(AppState { config, db, sessions, + chains, web_challenges: Arc::default(), });