From 04df63303e41d7f5aa12e618df442f215167c34d Mon Sep 17 00:00:00 2001 From: micah-shallom Date: Thu, 30 Apr 2026 18:05:21 +0100 Subject: [PATCH 1/6] feat(mintd): allow multiple lightning backends per currency unit A single mintd instance can now run several Lightning backends, each serving a different currency unit (for example CLN for sat, LNbits for eur). The legacy single [ln] config block is still accepted unchanged; multi-backend setups repeat [[ln]] with a unit field on each entry. Closes #1465 --- .../src/bin/start_regtest_mints.rs | 5 +- crates/cdk-integration-tests/src/shared.rs | 15 +- crates/cdk-mintd/src/config.rs | 23 +- crates/cdk-mintd/src/env_vars/mod.rs | 79 +++-- crates/cdk-mintd/src/lib.rs | 312 ++++++++++-------- 5 files changed, 261 insertions(+), 173 deletions(-) diff --git a/crates/cdk-integration-tests/src/bin/start_regtest_mints.rs b/crates/cdk-integration-tests/src/bin/start_regtest_mints.rs index 81c3dbf49b..e9f10d904f 100644 --- a/crates/cdk-integration-tests/src/bin/start_regtest_mints.rs +++ b/crates/cdk-integration-tests/src/bin/start_regtest_mints.rs @@ -302,14 +302,15 @@ fn create_ldk_settings( }, mint_info: cdk_mintd::config::MintInfo::default(), limits: cdk_mintd::config::Limits::default(), - ln: cdk_mintd::config::Ln { + ln: vec![cdk_mintd::config::Ln { ln_backend: cdk_mintd::config::LnBackend::LdkNode, + unit: cdk::nuts::CurrencyUnit::Sat, invoice_description: None, min_mint: 1.into(), max_mint: 500_000.into(), min_melt: 1.into(), max_melt: 500_000.into(), - }, + }], cln: None, lnbits: None, lnd: None, diff --git a/crates/cdk-integration-tests/src/shared.rs b/crates/cdk-integration-tests/src/shared.rs index f977f6c96f..f28ad285da 100644 --- a/crates/cdk-integration-tests/src/shared.rs +++ b/crates/cdk-integration-tests/src/shared.rs @@ -220,14 +220,15 @@ pub fn create_fake_wallet_settings( }, mint_info: cdk_mintd::config::MintInfo::default(), limits: cdk_mintd::config::Limits::default(), - ln: cdk_mintd::config::Ln { + ln: vec![cdk_mintd::config::Ln { ln_backend: cdk_mintd::config::LnBackend::FakeWallet, + unit: cdk::nuts::CurrencyUnit::Sat, invoice_description: None, min_mint: DEFAULT_MIN_MINT.into(), max_mint: DEFAULT_MAX_MINT.into(), min_melt: DEFAULT_MIN_MELT.into(), max_melt: DEFAULT_MAX_MELT.into(), - }, + }], cln: None, lnbits: None, lnd: None, @@ -276,14 +277,15 @@ pub fn create_cln_settings( }, mint_info: cdk_mintd::config::MintInfo::default(), limits: cdk_mintd::config::Limits::default(), - ln: cdk_mintd::config::Ln { + ln: vec![cdk_mintd::config::Ln { ln_backend: cdk_mintd::config::LnBackend::Cln, + unit: cdk::nuts::CurrencyUnit::Sat, invoice_description: None, min_mint: DEFAULT_MIN_MINT.into(), max_mint: DEFAULT_MAX_MINT.into(), min_melt: DEFAULT_MIN_MELT.into(), max_melt: DEFAULT_MAX_MELT.into(), - }, + }], cln: Some(cln_config), lnbits: None, lnd: None, @@ -327,14 +329,15 @@ pub fn create_lnd_settings( }, mint_info: cdk_mintd::config::MintInfo::default(), limits: cdk_mintd::config::Limits::default(), - ln: cdk_mintd::config::Ln { + ln: vec![cdk_mintd::config::Ln { ln_backend: cdk_mintd::config::LnBackend::Lnd, + unit: cdk::nuts::CurrencyUnit::Sat, invoice_description: None, min_mint: DEFAULT_MIN_MINT.into(), max_mint: DEFAULT_MAX_MINT.into(), min_melt: DEFAULT_MIN_MELT.into(), max_melt: DEFAULT_MAX_MELT.into(), - }, + }], cln: None, lnbits: None, ldk_node: None, diff --git a/crates/cdk-mintd/src/config.rs b/crates/cdk-mintd/src/config.rs index a750c9dbd4..6602611f3b 100644 --- a/crates/cdk-mintd/src/config.rs +++ b/crates/cdk-mintd/src/config.rs @@ -177,6 +177,8 @@ impl std::str::FromStr for LnBackend { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Ln { pub ln_backend: LnBackend, + #[serde(default)] + pub unit: CurrencyUnit, pub invoice_description: Option, pub min_mint: Amount, pub max_mint: Amount, @@ -188,6 +190,7 @@ impl Default for Ln { fn default() -> Self { Ln { ln_backend: LnBackend::default(), + unit: CurrencyUnit::default(), invoice_description: None, min_mint: 1.into(), max_mint: 500_000.into(), @@ -197,6 +200,23 @@ impl Default for Ln { } } +fn deserialize_ln<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum LnOneOrMany { + Many(Vec), + One(Ln), + } + + match LnOneOrMany::deserialize(deserializer)? { + LnOneOrMany::Many(v) => Ok(v), + LnOneOrMany::One(l) => Ok(vec![l]), + } +} + #[cfg(feature = "lnbits")] #[derive(Clone, Serialize, Deserialize)] pub struct LNbits { @@ -647,7 +667,8 @@ fn default_blind() -> AuthType { pub struct Settings { pub info: Info, pub mint_info: MintInfo, - pub ln: Ln, + #[serde(default, deserialize_with = "deserialize_ln")] + pub ln: Vec, /// Transaction limits for DoS protection #[serde(default)] pub limits: Limits, diff --git a/crates/cdk-mintd/src/env_vars/mod.rs b/crates/cdk-mintd/src/env_vars/mod.rs index 97b5c780b4..15b0d21a6f 100644 --- a/crates/cdk-mintd/src/env_vars/mod.rs +++ b/crates/cdk-mintd/src/env_vars/mod.rs @@ -56,7 +56,7 @@ pub use mint_info::*; #[cfg(feature = "prometheus")] pub use prometheus::*; -use crate::config::{DatabaseEngine, LnBackend, Settings}; +use crate::config::{DatabaseEngine, Ln, LnBackend, Settings}; impl Settings { pub fn from_env(&mut self) -> Result { @@ -90,7 +90,23 @@ impl Settings { self.info = self.info.clone().from_env(); self.mint_info = self.mint_info.clone().from_env(); - self.ln = self.ln.clone().from_env(); + // CDK_MINTD_LN_* env vars only apply when there is exactly one (or zero) [[ln]] entry. + match self.ln.len() { + 0 => { + let ln = Ln::default().from_env(); + if ln.ln_backend != LnBackend::None { + self.ln.push(ln); + } + } + 1 => { + self.ln[0] = self.ln[0].clone().from_env(); + } + _ => { + tracing::warn!( + "CDK_MINTD_LN_* environment variables ignored: multiple [[ln]] entries configured" + ); + } + } self.limits = self.limits.clone().from_env(); { @@ -120,35 +136,38 @@ impl Settings { self.prometheus = Some(self.prometheus.clone().unwrap_or_default().from_env()); } - match self.ln.ln_backend { - #[cfg(feature = "cln")] - LnBackend::Cln => { - self.cln = Some(self.cln.clone().unwrap_or_default().from_env()); - } - #[cfg(feature = "lnbits")] - LnBackend::LNbits => { - self.lnbits = Some(self.lnbits.clone().unwrap_or_default().from_env()); - } - #[cfg(feature = "fakewallet")] - LnBackend::FakeWallet => { - self.fake_wallet = Some(self.fake_wallet.clone().unwrap_or_default().from_env()); - } - #[cfg(feature = "lnd")] - LnBackend::Lnd => { - self.lnd = Some(self.lnd.clone().unwrap_or_default().from_env()); - } - #[cfg(feature = "ldk-node")] - LnBackend::LdkNode => { - self.ldk_node = Some(self.ldk_node.clone().unwrap_or_default().from_env()); - } - #[cfg(feature = "grpc-processor")] - LnBackend::GrpcProcessor => { - self.grpc_processor = - Some(self.grpc_processor.clone().unwrap_or_default().from_env()); + if self.ln.len() == 1 { + match self.ln[0].ln_backend { + #[cfg(feature = "cln")] + LnBackend::Cln => { + self.cln = Some(self.cln.clone().unwrap_or_default().from_env()); + } + #[cfg(feature = "lnbits")] + LnBackend::LNbits => { + self.lnbits = Some(self.lnbits.clone().unwrap_or_default().from_env()); + } + #[cfg(feature = "fakewallet")] + LnBackend::FakeWallet => { + self.fake_wallet = + Some(self.fake_wallet.clone().unwrap_or_default().from_env()); + } + #[cfg(feature = "lnd")] + LnBackend::Lnd => { + self.lnd = Some(self.lnd.clone().unwrap_or_default().from_env()); + } + #[cfg(feature = "ldk-node")] + LnBackend::LdkNode => { + self.ldk_node = Some(self.ldk_node.clone().unwrap_or_default().from_env()); + } + #[cfg(feature = "grpc-processor")] + LnBackend::GrpcProcessor => { + self.grpc_processor = + Some(self.grpc_processor.clone().unwrap_or_default().from_env()); + } + LnBackend::None => bail!("Ln backend must be set"), + #[allow(unreachable_patterns)] + _ => bail!("Selected Ln backend is not enabled in this build"), } - LnBackend::None => bail!("Ln backend must be set"), - #[allow(unreachable_patterns)] - _ => bail!("Selected Ln backend is not enabled in this build"), } Ok(self.clone()) diff --git a/crates/cdk-mintd/src/lib.rs b/crates/cdk-mintd/src/lib.rs index 70eb3e99a4..b0b62a7a7b 100644 --- a/crates/cdk-mintd/src/lib.rs +++ b/crates/cdk-mintd/src/lib.rs @@ -476,171 +476,207 @@ async fn configure_lightning_backend( work_dir: &Path, _kv_store: Option + Send + Sync>>, ) -> Result { - let mint_melt_limits = MintMeltLimits { - mint_min: settings.ln.min_mint, - mint_max: settings.ln.max_mint, - melt_min: settings.ln.min_melt, - melt_max: settings.ln.max_melt, - }; + if settings.ln.is_empty() { + bail!("Lightning backend must be configured"); + } - tracing::debug!("Ln backend: {:?}", settings.ln.ln_backend); - - match settings.ln.ln_backend { - #[cfg(feature = "cln")] - LnBackend::Cln => { - let cln_settings = settings - .cln - .clone() - .expect("Config checked at load that cln is some"); - let cln = cln_settings - .setup(settings, CurrencyUnit::Msat, None, work_dir, _kv_store) - .await?; - #[cfg(feature = "prometheus")] - let cln = MetricsMintPayment::new(cln); + let mut seen: HashSet<(CurrencyUnit, PaymentMethod)> = HashSet::new(); - mint_builder = configure_backend_for_unit( - settings, - mint_builder, - CurrencyUnit::Sat, - mint_melt_limits, - Arc::new(cln), - ) - .await?; - } - #[cfg(feature = "lnbits")] - LnBackend::LNbits => { - let lnbits_settings = settings.clone().lnbits.expect("Checked on config load"); - let lnbits = lnbits_settings - .setup(settings, CurrencyUnit::Sat, None, work_dir, None) - .await?; - #[cfg(feature = "prometheus")] - let lnbits = MetricsMintPayment::new(lnbits); + for ln_entry in &settings.ln { + let mint_melt_limits = MintMeltLimits { + mint_min: ln_entry.min_mint, + mint_max: ln_entry.max_mint, + melt_min: ln_entry.min_melt, + melt_max: ln_entry.max_melt, + }; - mint_builder = configure_backend_for_unit( - settings, - mint_builder, - CurrencyUnit::Sat, - mint_melt_limits, - Arc::new(lnbits), - ) - .await?; - } - #[cfg(feature = "lnd")] - LnBackend::Lnd => { - let lnd_settings = settings.clone().lnd.expect("Checked at config load"); - let lnd = lnd_settings - .setup(settings, CurrencyUnit::Msat, None, work_dir, _kv_store) + tracing::debug!( + "Ln backend: {:?} (unit: {:?})", + ln_entry.ln_backend, + ln_entry.unit + ); + + match ln_entry.ln_backend { + #[cfg(feature = "cln")] + LnBackend::Cln => { + let cln_settings = settings + .cln + .clone() + .expect("Config checked at load that cln is some"); + let cln = cln_settings + .setup( + settings, + CurrencyUnit::Msat, + None, + work_dir, + _kv_store.clone(), + ) + .await?; + #[cfg(feature = "prometheus")] + let cln = MetricsMintPayment::new(cln); + + mint_builder = configure_backend_for_unit( + settings, + mint_builder, + ln_entry.unit.clone(), + mint_melt_limits, + Arc::new(cln), + &mut seen, + ) .await?; - #[cfg(feature = "prometheus")] - let lnd = MetricsMintPayment::new(lnd); + } + #[cfg(feature = "lnbits")] + LnBackend::LNbits => { + let lnbits_settings = settings.clone().lnbits.expect("Checked on config load"); + let lnbits = lnbits_settings + .setup(settings, CurrencyUnit::Sat, None, work_dir, None) + .await?; + #[cfg(feature = "prometheus")] + let lnbits = MetricsMintPayment::new(lnbits); - mint_builder = configure_backend_for_unit( - settings, - mint_builder, - CurrencyUnit::Sat, - mint_melt_limits, - Arc::new(lnd), - ) - .await?; - } - #[cfg(feature = "fakewallet")] - LnBackend::FakeWallet => { - let fake_wallet = settings.clone().fake_wallet.expect("Fake wallet defined"); - tracing::info!("Using fake wallet: {:?}", fake_wallet); - - for unit in fake_wallet.clone().supported_units { - let fake = fake_wallet - .setup(settings, unit.clone(), None, work_dir, _kv_store.clone()) + mint_builder = configure_backend_for_unit( + settings, + mint_builder, + ln_entry.unit.clone(), + mint_melt_limits, + Arc::new(lnbits), + &mut seen, + ) + .await?; + } + #[cfg(feature = "lnd")] + LnBackend::Lnd => { + let lnd_settings = settings.clone().lnd.expect("Checked at config load"); + let lnd = lnd_settings + .setup( + settings, + CurrencyUnit::Msat, + None, + work_dir, + _kv_store.clone(), + ) .await?; #[cfg(feature = "prometheus")] - let fake = MetricsMintPayment::new(fake); + let lnd = MetricsMintPayment::new(lnd); mint_builder = configure_backend_for_unit( settings, mint_builder, - unit.clone(), + ln_entry.unit.clone(), mint_melt_limits, - Arc::new(fake), + Arc::new(lnd), + &mut seen, ) .await?; } + #[cfg(feature = "fakewallet")] + LnBackend::FakeWallet => { + let fake_wallet = settings.clone().fake_wallet.expect("Fake wallet defined"); + tracing::info!("Using fake wallet: {:?}", fake_wallet); + + for unit in fake_wallet.clone().supported_units { + let fake = fake_wallet + .setup(settings, unit.clone(), None, work_dir, _kv_store.clone()) + .await?; + #[cfg(feature = "prometheus")] + let fake = MetricsMintPayment::new(fake); + + mint_builder = configure_backend_for_unit( + settings, + mint_builder, + unit.clone(), + mint_melt_limits, + Arc::new(fake), + &mut seen, + ) + .await?; + } - for rotation_cfg in &fake_wallet.keyset_rotations { - use cdk::mint::KeysetRotation; + for rotation_cfg in &fake_wallet.keyset_rotations { + use cdk::mint::KeysetRotation; - let amounts = cdk::mint::UnitConfig::default().amounts; - let final_expiry = if rotation_cfg.expired { - Some(cdk::util::unix_time().saturating_sub(3600)) - } else { - None - }; + let amounts = cdk::mint::UnitConfig::default().amounts; + let final_expiry = if rotation_cfg.expired { + Some(cdk::util::unix_time().saturating_sub(3600)) + } else { + None + }; - mint_builder = mint_builder.with_keyset_rotation(KeysetRotation { - unit: rotation_cfg.unit.clone(), - amounts, - input_fee_ppk: rotation_cfg.input_fee_ppk, - use_keyset_v2: rotation_cfg.version == "v2", - final_expiry, - }); + mint_builder = mint_builder.with_keyset_rotation(KeysetRotation { + unit: rotation_cfg.unit.clone(), + amounts, + input_fee_ppk: rotation_cfg.input_fee_ppk, + use_keyset_v2: rotation_cfg.version == "v2", + final_expiry, + }); + } } - } - #[cfg(feature = "grpc-processor")] - LnBackend::GrpcProcessor => { - let grpc_processor = settings - .clone() - .grpc_processor - .expect("grpc processor config defined"); + #[cfg(feature = "grpc-processor")] + LnBackend::GrpcProcessor => { + let grpc_processor = settings + .clone() + .grpc_processor + .expect("grpc processor config defined"); - tracing::info!( - "Attempting to start with gRPC payment processor at {}:{}.", - grpc_processor.addr, - grpc_processor.port - ); + tracing::info!( + "Attempting to start with gRPC payment processor at {}:{}.", + grpc_processor.addr, + grpc_processor.port + ); - for unit in grpc_processor.clone().supported_units { - tracing::debug!("Adding unit: {:?}", unit); - let processor = grpc_processor - .setup(settings, unit.clone(), None, work_dir, None) + for unit in grpc_processor.clone().supported_units { + tracing::debug!("Adding unit: {:?}", unit); + let processor = grpc_processor + .setup(settings, unit.clone(), None, work_dir, None) + .await?; + #[cfg(feature = "prometheus")] + let processor = MetricsMintPayment::new(processor); + + mint_builder = configure_backend_for_unit( + settings, + mint_builder, + unit.clone(), + mint_melt_limits, + Arc::new(processor), + &mut seen, + ) + .await?; + } + } + #[cfg(feature = "ldk-node")] + LnBackend::LdkNode => { + let ldk_node_settings = settings.clone().ldk_node.expect("Checked at config load"); + tracing::info!("Using LDK Node backend: {:?}", ldk_node_settings); + + let ldk_node = ldk_node_settings + .setup( + settings, + CurrencyUnit::Sat, + _runtime.clone(), + work_dir, + None, + ) .await?; - #[cfg(feature = "prometheus")] - let processor = MetricsMintPayment::new(processor); mint_builder = configure_backend_for_unit( settings, mint_builder, - unit.clone(), + ln_entry.unit.clone(), mint_melt_limits, - Arc::new(processor), + Arc::new(ldk_node), + &mut seen, ) .await?; } + LnBackend::None => { + tracing::error!( + "Payment backend was not set or feature disabled. {:?}", + ln_entry.ln_backend + ); + bail!("Lightning backend must be configured"); + } } - #[cfg(feature = "ldk-node")] - LnBackend::LdkNode => { - let ldk_node_settings = settings.clone().ldk_node.expect("Checked at config load"); - tracing::info!("Using LDK Node backend: {:?}", ldk_node_settings); - - let ldk_node = ldk_node_settings - .setup(settings, CurrencyUnit::Sat, _runtime, work_dir, None) - .await?; - - mint_builder = configure_backend_for_unit( - settings, - mint_builder, - CurrencyUnit::Sat, - mint_melt_limits, - Arc::new(ldk_node), - ) - .await?; - } - LnBackend::None => { - tracing::error!( - "Payment backend was not set or feature disabled. {:?}", - settings.ln.ln_backend - ); - bail!("Lightning backend must be configured"); - } - }; + } Ok(mint_builder) } @@ -652,6 +688,7 @@ async fn configure_backend_for_unit( unit: cdk::nuts::CurrencyUnit, mint_melt_limits: MintMeltLimits, backend: Arc + Send + Sync>, + seen: &mut HashSet<(cdk::nuts::CurrencyUnit, PaymentMethod)>, ) -> Result { let payment_settings = backend.get_settings().await?; @@ -674,6 +711,13 @@ async fn configure_backend_for_unit( // Add all supported payment methods to the mint builder for method in &methods { + if !seen.insert((unit.clone(), method.clone())) { + bail!( + "Duplicate (unit, method) configured: ({:?}, {:?}). Each (unit, method) pair must be served by exactly one backend.", + unit, + method + ); + } mint_builder .add_payment_processor( unit.clone(), From 1763ea27abb29c958284909c6da3e6ad107a56b5 Mon Sep 17 00:00:00 2001 From: micah-shallom Date: Thu, 30 Apr 2026 18:09:40 +0100 Subject: [PATCH 2/6] test(mintd): cover legacy and multi-backend config parsing Adds two round-trip parsing tests: - test_multi_backend_config_parses verifies a TOML with two [[ln]] blocks (sat and eur) deserializes into a 2-element Vec with correct units and per-entry limits. - test_legacy_ln_block_parses verifies the old single [ln] form produces a 1-element Vec with unit defaulted to Sat, locking in backwards compatibility. --- crates/cdk-mintd/src/config.rs | 73 ++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/crates/cdk-mintd/src/config.rs b/crates/cdk-mintd/src/config.rs index 6602611f3b..bb2ffabff2 100644 --- a/crates/cdk-mintd/src/config.rs +++ b/crates/cdk-mintd/src/config.rs @@ -880,6 +880,79 @@ mod tests { assert!(debug_output.contains(" Date: Thu, 30 Apr 2026 18:10:53 +0100 Subject: [PATCH 3/6] docs(mintd): document multi-backend lightning configuration Updates example.config.toml to show the new [[ln]] array form alongside the existing single [ln] block, and adds a "Multiple Lightning Backends" section to the cdk-mintd README explaining per-unit configuration, duplicate (unit, method) rejection, and backwards compatibility. --- crates/cdk-mintd/README.md | 26 ++++++++++++++++++++++++++ crates/cdk-mintd/example.config.toml | 10 ++++++++++ 2 files changed, 36 insertions(+) diff --git a/crates/cdk-mintd/README.md b/crates/cdk-mintd/README.md index 7f06a50e36..b3bc6431c6 100644 --- a/crates/cdk-mintd/README.md +++ b/crates/cdk-mintd/README.md @@ -186,6 +186,32 @@ engine = "postgres" url = "postgresql://mint_user:password@localhost:5432/cdk_mint" ``` +### With Multiple Lightning Backends + +A single mint can serve more than one currency unit by configuring a separate backend per unit. Replace the single `[ln]` block with one `[[ln]]` block per `(unit, method)` pair, and keep the existing per-backend config sections (`[cln]`, `[lnbits]`, etc.) as-is. + +```toml +[[ln]] +ln_backend = "cln" +unit = "sat" + +[[ln]] +ln_backend = "lnbits" +unit = "eur" + +[cln] +rpc_path = "/home/bitcoin/.lightning/bitcoin/lightning-rpc" + +[lnbits] +admin_api_key = "..." +invoice_api_key = "..." +lnbits_api = "https://lnbits.example.com" +``` + +Each `[[ln]]` block carries its own `min_mint`, `max_mint`, `min_melt`, `max_melt` if you want different limits per unit. Two entries colliding on the same `(unit, method)` pair are rejected at startup. + +The legacy single `[ln]` form is still accepted; it's equivalent to one `[[ln]]` entry with `unit = "sat"` (the default). `CDK_MINTD_LN_*` environment variables only apply when there is exactly one (or zero) `[[ln]]` entry — multi-backend setups must be configured via the file. + ## Directory Structure After setup and first run, your directory will look like: diff --git a/crates/cdk-mintd/example.config.toml b/crates/cdk-mintd/example.config.toml index aa13d27bdb..efb8552670 100644 --- a/crates/cdk-mintd/example.config.toml +++ b/crates/cdk-mintd/example.config.toml @@ -93,9 +93,19 @@ max_connections = 20 # Connection timeout in seconds (optional, defaults to 10) connection_timeout_seconds = 10 +# Lightning backends. Use [ln] for a single backend, or repeat [[ln]] for one +# backend per (unit, method) pair, e.g.: +# [[ln]] +# ln_backend = "cln" +# unit = "sat" +# [[ln]] +# ln_backend = "lnbits" +# unit = "eur" + [ln] # Required ln backend `cln`, `lnd`, `fakewallet`, 'lnbits', 'ldknode' ln_backend = "fakewallet" +# unit = "sat" # Optional, defaults to "sat" # min_mint=1 # max_mint=500000 # min_melt=1 From 2e4741eba84e339133ded92edb9d7b35672f55a1 Mon Sep 17 00:00:00 2001 From: micah-shallom Date: Mon, 11 May 2026 20:56:37 +0100 Subject: [PATCH 4/6] feat(mintd): use [[ln]].unit for fakewallet and grpc_processor Replaces the internal supported_units iteration with a single registration driven by [[ln]].unit, matching the pattern used by the other Lightning backends. Operators no longer need to declare the same unit twice (once in [fake_wallet]/[grpc_processor] and again in [[ln]]). The supported_units field stays in the config structs so legacy configs still parse, but it no longer affects registration. Operators using multiple supported_units should switch to one [[ln]] block per unit. --- crates/cdk-mintd/example.config.toml | 2 - crates/cdk-mintd/src/lib.rs | 68 ++++++++++++++-------------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/crates/cdk-mintd/example.config.toml b/crates/cdk-mintd/example.config.toml index efb8552670..469082f4c4 100644 --- a/crates/cdk-mintd/example.config.toml +++ b/crates/cdk-mintd/example.config.toml @@ -185,7 +185,6 @@ ln_backend = "fakewallet" # webserver_port = 0 # 0 = auto-assign available port [fake_wallet] -supported_units = ["sat"] fee_percent = 0.02 reserve_fee_min = 1 min_delay_time = 1 @@ -212,7 +211,6 @@ max_delay_time = 3 # [grpc_processor] # gRPC Payment Processor configuration -# supported_units = ["sat"] # addr = "127.0.0.1" # port = 50051 # tls_dir = "/path/to/tls" diff --git a/crates/cdk-mintd/src/lib.rs b/crates/cdk-mintd/src/lib.rs index b0b62a7a7b..5899787715 100644 --- a/crates/cdk-mintd/src/lib.rs +++ b/crates/cdk-mintd/src/lib.rs @@ -574,23 +574,27 @@ async fn configure_lightning_backend( let fake_wallet = settings.clone().fake_wallet.expect("Fake wallet defined"); tracing::info!("Using fake wallet: {:?}", fake_wallet); - for unit in fake_wallet.clone().supported_units { - let fake = fake_wallet - .setup(settings, unit.clone(), None, work_dir, _kv_store.clone()) - .await?; - #[cfg(feature = "prometheus")] - let fake = MetricsMintPayment::new(fake); - - mint_builder = configure_backend_for_unit( + let fake = fake_wallet + .setup( settings, - mint_builder, - unit.clone(), - mint_melt_limits, - Arc::new(fake), - &mut seen, + ln_entry.unit.clone(), + None, + work_dir, + _kv_store.clone(), ) .await?; - } + #[cfg(feature = "prometheus")] + let fake = MetricsMintPayment::new(fake); + + mint_builder = configure_backend_for_unit( + settings, + mint_builder, + ln_entry.unit.clone(), + mint_melt_limits, + Arc::new(fake), + &mut seen, + ) + .await?; for rotation_cfg in &fake_wallet.keyset_rotations { use cdk::mint::KeysetRotation; @@ -619,29 +623,27 @@ async fn configure_lightning_backend( .expect("grpc processor config defined"); tracing::info!( - "Attempting to start with gRPC payment processor at {}:{}.", + "Attempting to start with gRPC payment processor at {}:{} for unit {:?}.", grpc_processor.addr, - grpc_processor.port + grpc_processor.port, + ln_entry.unit ); - for unit in grpc_processor.clone().supported_units { - tracing::debug!("Adding unit: {:?}", unit); - let processor = grpc_processor - .setup(settings, unit.clone(), None, work_dir, None) - .await?; - #[cfg(feature = "prometheus")] - let processor = MetricsMintPayment::new(processor); - - mint_builder = configure_backend_for_unit( - settings, - mint_builder, - unit.clone(), - mint_melt_limits, - Arc::new(processor), - &mut seen, - ) + let processor = grpc_processor + .setup(settings, ln_entry.unit.clone(), None, work_dir, None) .await?; - } + #[cfg(feature = "prometheus")] + let processor = MetricsMintPayment::new(processor); + + mint_builder = configure_backend_for_unit( + settings, + mint_builder, + ln_entry.unit.clone(), + mint_melt_limits, + Arc::new(processor), + &mut seen, + ) + .await?; } #[cfg(feature = "ldk-node")] LnBackend::LdkNode => { From f87057a13679fddac0a80c989d830b966b23b41d Mon Sep 17 00:00:00 2001 From: micah-shallom Date: Mon, 11 May 2026 23:45:50 +0100 Subject: [PATCH 5/6] test(mintd): assert fakewallet dispatcher uses [[ln]].unit Verifies that when an operator configures [[ln]] backend = "fakewallet" with a non-default unit, the resulting mint registers that unit and not whatever [fake_wallet].supported_units says. This guards against the supported_units iteration creeping back into the dispatcher. --- crates/cdk-mintd/src/lib.rs | 43 +++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/crates/cdk-mintd/src/lib.rs b/crates/cdk-mintd/src/lib.rs index 5899787715..b1323fc807 100644 --- a/crates/cdk-mintd/src/lib.rs +++ b/crates/cdk-mintd/src/lib.rs @@ -1507,6 +1507,49 @@ mod tests { use super::*; + #[cfg(feature = "fakewallet")] + #[tokio::test] + async fn fakewallet_dispatcher_uses_ln_entry_unit() { + use cdk::mint::MintBuilder; + use cdk_sqlite::mint::memory; + + use crate::config::{FakeWallet, Ln, LnBackend}; + + let settings = config::Settings { + ln: vec![Ln { + ln_backend: LnBackend::FakeWallet, + unit: CurrencyUnit::Eur, + ..Default::default() + }], + fake_wallet: Some(FakeWallet::default()), + ..Default::default() + }; + + let localstore = Arc::new(memory::empty().await.unwrap()); + let builder = MintBuilder::new(localstore); + let builder = + configure_lightning_backend(&settings, builder, None, &std::env::temp_dir(), None) + .await + .expect("dispatcher should succeed"); + + let mint_info = builder.current_mint_info(); + let units: Vec<_> = mint_info + .nuts + .nut04 + .methods + .iter() + .map(|m| m.unit.clone()) + .collect(); + assert!( + units.contains(&CurrencyUnit::Eur), + "expected Eur, got {units:?}" + ); + assert!( + !units.contains(&CurrencyUnit::Sat), + "Sat would only appear if supported_units leaked through; got {units:?}" + ); + } + #[test] fn test_postgres_auth_url_validation() { // Test that the auth database config requires explicit configuration From d89f7d4e5f3fccac9c35b07ff11bc2eeea76464d Mon Sep 17 00:00:00 2001 From: asmo Date: Fri, 15 May 2026 23:34:16 +0200 Subject: [PATCH 6/6] fix fakewallet defaults --- crates/cdk-mintd/src/config.rs | 47 ++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/crates/cdk-mintd/src/config.rs b/crates/cdk-mintd/src/config.rs index bb2ffabff2..49de7e7159 100644 --- a/crates/cdk-mintd/src/config.rs +++ b/crates/cdk-mintd/src/config.rs @@ -179,13 +179,26 @@ pub struct Ln { pub ln_backend: LnBackend, #[serde(default)] pub unit: CurrencyUnit, + #[serde(default)] pub invoice_description: Option, + #[serde(default = "default_ln_min")] pub min_mint: Amount, + #[serde(default = "default_ln_max")] pub max_mint: Amount, + #[serde(default = "default_ln_min")] pub min_melt: Amount, + #[serde(default = "default_ln_max")] pub max_melt: Amount, } +fn default_ln_min() -> Amount { + 1.into() +} + +fn default_ln_max() -> Amount { + 500_000.into() +} + impl Default for Ln { fn default() -> Self { Ln { @@ -458,8 +471,11 @@ fn default_keyset_version() -> String { #[cfg(feature = "fakewallet")] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FakeWallet { + #[serde(default = "default_fake_wallet_supported_units")] pub supported_units: Vec, + #[serde(default = "default_fake_wallet_fee_percent")] pub fee_percent: f32, + #[serde(default = "default_fake_wallet_reserve_fee_min")] pub reserve_fee_min: Amount, #[serde(default = "default_min_delay_time")] pub min_delay_time: u64, @@ -496,6 +512,21 @@ fn default_reserve_fee_min() -> Amount { 2.into() } +#[cfg(feature = "fakewallet")] +fn default_fake_wallet_supported_units() -> Vec { + vec![CurrencyUnit::Sat] +} + +#[cfg(feature = "fakewallet")] +fn default_fake_wallet_fee_percent() -> f32 { + 0.02 +} + +#[cfg(feature = "fakewallet")] +fn default_fake_wallet_reserve_fee_min() -> Amount { + 2.into() +} + #[cfg(feature = "fakewallet")] fn default_min_delay_time() -> u64 { 1 @@ -953,6 +984,22 @@ max_melt = 500000 let _ = fs::remove_dir_all(&temp_dir); } + #[cfg(feature = "fakewallet")] + #[test] + fn test_example_config_loads() { + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("example.config.toml"); + let settings = Settings::new_from_default(&Settings::default(), Some(&path)) + .expect("example.config.toml must parse"); + assert_eq!( + settings.ln.len(), + 1, + "example.config.toml ln backend was dropped: {:?}", + settings.ln + ); + assert_eq!(settings.ln[0].ln_backend, LnBackend::FakeWallet); + assert_eq!(settings.ln[0].unit, CurrencyUnit::Sat); + } + /// Test that configuration can be loaded purely from environment variables /// without requiring a config.toml file with backend sections. ///