Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions crates/cdk-integration-tests/src/bin/start_regtest_mints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
15 changes: 9 additions & 6 deletions crates/cdk-integration-tests/src/shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
26 changes: 26 additions & 0 deletions crates/cdk-mintd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
12 changes: 10 additions & 2 deletions crates/cdk-mintd/example.config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -175,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
Expand All @@ -202,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"
Expand Down
143 changes: 142 additions & 1 deletion crates/cdk-mintd/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,17 +177,33 @@ impl std::str::FromStr for LnBackend {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ln {
pub ln_backend: LnBackend,
#[serde(default)]
pub unit: CurrencyUnit,
#[serde(default)]
pub invoice_description: Option<String>,
#[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 {
ln_backend: LnBackend::default(),
unit: CurrencyUnit::default(),
invoice_description: None,
min_mint: 1.into(),
max_mint: 500_000.into(),
Expand All @@ -197,6 +213,23 @@ impl Default for Ln {
}
}

fn deserialize_ln<'de, D>(deserializer: D) -> Result<Vec<Ln>, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum LnOneOrMany {
Many(Vec<Ln>),
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 {
Expand Down Expand Up @@ -438,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<CurrencyUnit>,
#[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,
Expand Down Expand Up @@ -476,6 +512,21 @@ fn default_reserve_fee_min() -> Amount {
2.into()
}

#[cfg(feature = "fakewallet")]
fn default_fake_wallet_supported_units() -> Vec<CurrencyUnit> {
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
Expand Down Expand Up @@ -647,7 +698,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<Ln>,
/// Transaction limits for DoS protection
#[serde(default)]
pub limits: Limits,
Expand Down Expand Up @@ -859,6 +911,95 @@ mod tests {
assert!(debug_output.contains("<hashed: "));
}

#[cfg(feature = "fakewallet")]
#[test]
fn test_multi_backend_config_parses() {
use std::{env, fs};

let temp_dir = env::temp_dir().join("cdk_test_multi_backend_config");
fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
let config_path = temp_dir.join("config.toml");

let config_content = r#"
[[ln]]
ln_backend = "fakewallet"
unit = "sat"
min_mint = 1
max_mint = 500000
min_melt = 1
max_melt = 500000

[[ln]]
ln_backend = "fakewallet"
unit = "eur"
min_mint = 1
max_mint = 1000
min_melt = 1
max_melt = 1000
"#;
fs::write(&config_path, config_content).expect("Failed to write config file");

let settings = Settings::new(Some(&config_path));

assert_eq!(settings.ln.len(), 2);

assert_eq!(settings.ln[0].ln_backend, LnBackend::FakeWallet);
assert_eq!(settings.ln[0].unit, CurrencyUnit::Sat);
let max_mint_0: u64 = settings.ln[0].max_mint.into();
assert_eq!(max_mint_0, 500_000);

assert_eq!(settings.ln[1].ln_backend, LnBackend::FakeWallet);
assert_eq!(settings.ln[1].unit, CurrencyUnit::Eur);
let max_mint_1: u64 = settings.ln[1].max_mint.into();
assert_eq!(max_mint_1, 1_000);

let _ = fs::remove_dir_all(&temp_dir);
}

#[cfg(feature = "fakewallet")]
#[test]
fn test_legacy_ln_block_parses() {
use std::{env, fs};

let temp_dir = env::temp_dir().join("cdk_test_legacy_ln_block");
fs::create_dir_all(&temp_dir).expect("Failed to create temp dir");
let config_path = temp_dir.join("config.toml");

let config_content = r#"
[ln]
ln_backend = "fakewallet"
min_mint = 1
max_mint = 500000
min_melt = 1
max_melt = 500000
"#;
fs::write(&config_path, config_content).expect("Failed to write config file");

let settings = Settings::new(Some(&config_path));

assert_eq!(settings.ln.len(), 1);
assert_eq!(settings.ln[0].ln_backend, LnBackend::FakeWallet);
assert_eq!(settings.ln[0].unit, CurrencyUnit::Sat);

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.
///
Expand Down
Loading
Loading