From a56821c6d26e9691bcf3a5a02373a3ca5f094638 Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Wed, 20 Aug 2025 21:23:23 -0300 Subject: [PATCH 1/2] Add CLI args and env var support - Added `clap` dependency to `ldk-server` to support passing essential node config via CLI arguments and environment variables. - Implemented layered config loading: config file (full set of options) + environment variables + CLI arguments. Env vars and CLI args override values from the config file when present. - Added tests for the new config loading logic. - Updated README with usage instructions and explanation of config precedence. Close #42 --- Cargo.lock | 1 + README.md | 47 ++- ldk-server/Cargo.toml | 1 + ldk-server/src/main.rs | 31 +- ldk-server/src/util/config.rs | 688 ++++++++++++++++++++++++++-------- 5 files changed, 594 insertions(+), 174 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1aa8892..5b2baed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1698,6 +1698,7 @@ version = "0.1.0" dependencies = [ "async-trait", "bytes", + "clap", "futures-util", "hex-conservative 0.2.1", "http-body-util", diff --git a/README.md b/README.md index 8ea7071..404ebe7 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ We welcome your feedback and contributions to help shape the future of LDK Serve ### Configuration Refer `./ldk-server/ldk-server-config.toml` to see available configuration options. +You can configure the node via a TOML file, environment variables, or CLI arguments. All options are optional — values provided via CLI override environment variables, which override the values in the TOML file. + ### Building ``` git clone https://github.com/lightningdevkit/ldk-server.git @@ -45,12 +47,51 @@ cargo build ``` ### Running +- Using a config file: ``` cargo run --bin ldk-server ./ldk-server/ldk-server-config.toml ``` -Interact with the node using CLI: +- Using environment variables (all optional): +``` +export LDK_SERVER_NODE_NETWORK=regtest +export LDK_SERVER_NODE_LISTENING_ADDRESS=localhost:3001 +export LDK_SERVER_NODE_REST_SERVICE_ADDRESS=127.0.0.1:3002 +export LDK_SERVER_NODE_ALIAS=LDK-Server +export LDK_SERVER_BITCOIND_RPC_ADDRESS=127.0.0.1:18443 +export LDK_SERVER_BITCOIND_RPC_USER=your-rpc-user +export LDK_SERVER_BITCOIND_RPC_PASSWORD=your-rpc-password +export LDK_SERVER_STORAGE_DIR_PATH=/path/to/storage +cargo run --bin ldk-server +``` + +- Using CLI arguments (all optional): +``` +cargo run --bin ldk-server -- \ + --node-network regtest \ + --node-listening-address localhost:3001 \ + --node-rest-service-address 127.0.0.1:3002 \ + --node-alias LDK-Server \ + --bitcoind-rpc-address 127.0.0.1:18443 \ + --bitcoind-rpc-user your-rpc-user \ + --bitcoind-rpc-password your-rpc-password \ + --storage-dir-path /path/to/storage +``` + +- Mixed configuration example (config file + overrides): +``` +# config file sets listening_address = "localhost:3001" +cargo run --bin ldk-server ./ldk-server/ldk-server-config.toml --node-listening-address localhost:3009 +# Result: `localhost:3009` will be used because CLI overrides the config file +``` + +### Interacting with the Node + +Once running, use the CLI client: ``` -./target/debug/ldk-server-cli -b localhost:3002 onchain-receive # To generate onchain-receive address. -./target/debug/ldk-server-cli -b localhost:3002 help # To print help/available commands. +# Generate an on-chain receive address +./target/debug/ldk-server-cli -b localhost:3002 onchain-receive + +# View commands +./target/debug/ldk-server-cli -b localhost:3002 help ``` diff --git a/ldk-server/Cargo.toml b/ldk-server/Cargo.toml index fb3527a..28f825d 100644 --- a/ldk-server/Cargo.toml +++ b/ldk-server/Cargo.toml @@ -18,6 +18,7 @@ rusqlite = { version = "0.31.0", features = ["bundled"] } rand = { version = "0.8.5", default-features = false } async-trait = { version = "0.1.85", default-features = false } toml = { version = "0.8.9", default-features = false, features = ["parse"] } +clap = { version = "4.0.5", default-features = false, features = ["derive", "std", "error-context", "suggestions", "help", "env"] } # Required for RabittMQ based EventPublisher. Only enabled for `events-rabbitmq` feature. lapin = { version = "2.4.0", features = ["rustls"], default-features = false, optional = true } diff --git a/ldk-server/src/main.rs b/ldk-server/src/main.rs index 8421530..086a09f 100644 --- a/ldk-server/src/main.rs +++ b/ldk-server/src/main.rs @@ -13,6 +13,8 @@ use tokio::signal::unix::SignalKind; use hyper::server::conn::http1; use hyper_util::rt::TokioIo; +use clap::Parser; + use crate::io::events::event_publisher::{EventPublisher, NoopEventPublisher}; use crate::io::events::get_event_name; #[cfg(feature = "events-rabbitmq")] @@ -24,7 +26,7 @@ use crate::io::persist::{ FORWARDED_PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE, PAYMENTS_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENTS_PERSISTENCE_SECONDARY_NAMESPACE, }; -use crate::util::config::load_config; +use crate::util::config::{load_config, ArgsConfig}; use crate::util::proto_adapter::{forwarded_payment_to_proto, payment_to_proto}; use hex::DisplayHex; use ldk_node::config::Config; @@ -36,38 +38,19 @@ use ldk_server_protos::events::{event_envelope, EventEnvelope}; use ldk_server_protos::types::Payment; use prost::Message; use rand::Rng; -use std::fs; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use tokio::select; -const USAGE_GUIDE: &str = "Usage: ldk-server "; - fn main() { - let args: Vec = std::env::args().collect(); - - if args.len() < 2 { - eprintln!("{USAGE_GUIDE}"); - std::process::exit(-1); - } - - let arg = args[1].as_str(); - if arg == "-h" || arg == "--help" { - println!("{}", USAGE_GUIDE); - std::process::exit(0); - } - - if fs::File::open(arg).is_err() { - eprintln!("Unable to access configuration file."); - std::process::exit(-1); - } + let args_config = ArgsConfig::parse(); let mut ldk_node_config = Config::default(); - let config_file = match load_config(Path::new(arg)) { + let config_file = match load_config(&args_config) { Ok(config) => config, Err(e) => { - eprintln!("Invalid configuration file: {}", e); + eprintln!("Invalid configuration: {}", e); std::process::exit(-1); }, }; diff --git a/ldk-server/src/util/config.rs b/ldk-server/src/util/config.rs index 8256bbb..f4b3701 100644 --- a/ldk-server/src/util/config.rs +++ b/ldk-server/src/util/config.rs @@ -1,10 +1,10 @@ +use clap::Parser; use ldk_node::bitcoin::Network; use ldk_node::lightning::ln::msgs::SocketAddress; use ldk_node::lightning::routing::gossip::NodeAlias; use ldk_node::liquidity::LSPS2ServiceConfig; use serde::{Deserialize, Serialize}; use std::net::SocketAddr; -use std::path::Path; use std::str::FromStr; use std::{fs, io}; @@ -24,104 +24,21 @@ pub struct Config { pub lsps2_service_config: Option, } -impl TryFrom for Config { - type Error = io::Error; - - fn try_from(toml_config: TomlConfig) -> io::Result { - let listening_addr = - SocketAddress::from_str(&toml_config.node.listening_address).map_err(|e| { - io::Error::new( - io::ErrorKind::InvalidInput, - format!("Invalid listening address configured: {}", e), - ) - })?; - let rest_service_addr = SocketAddr::from_str(&toml_config.node.rest_service_address) - .map_err(|e| { - io::Error::new( - io::ErrorKind::InvalidInput, - format!("Invalid rest service address configured: {}", e), - ) - })?; - let bitcoind_rpc_addr = - SocketAddr::from_str(&toml_config.bitcoind.rpc_address).map_err(|e| { - io::Error::new( - io::ErrorKind::InvalidInput, - format!("Invalid bitcoind RPC address configured: {}", e), - ) - })?; - - let alias = if let Some(alias_str) = toml_config.node.alias { - let mut bytes = [0u8; 32]; - let alias_bytes = alias_str.trim().as_bytes(); - if alias_bytes.len() > 32 { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "node.alias must be at most 32 bytes long.".to_string(), - )); - } - bytes[..alias_bytes.len()].copy_from_slice(alias_bytes); - Some(NodeAlias(bytes)) - } else { - None - }; - - let (rabbitmq_connection_string, rabbitmq_exchange_name) = { - let rabbitmq = toml_config.rabbitmq.unwrap_or(RabbitmqConfig { - connection_string: String::new(), - exchange_name: String::new(), - }); - #[cfg(feature = "events-rabbitmq")] - if rabbitmq.connection_string.is_empty() || rabbitmq.exchange_name.is_empty() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Both `rabbitmq.connection_string` and `rabbitmq.exchange_name` must be configured if enabling `events-rabbitmq` feature.".to_string(), - )); - } - (rabbitmq.connection_string, rabbitmq.exchange_name) - }; - - #[cfg(not(feature = "experimental-lsps2-support"))] - let lsps2_service_config: Option = None; - #[cfg(feature = "experimental-lsps2-support")] - let lsps2_service_config = Some(toml_config.liquidity - .and_then(|l| l.lsps2_service) - .ok_or_else(|| io::Error::new( - io::ErrorKind::InvalidInput, - "`liquidity.lsps2_service` must be defined in config if enabling `experimental-lsps2-support` feature." - ))? - .into()); - - Ok(Config { - listening_addr, - network: toml_config.node.network, - alias, - rest_service_addr, - storage_dir_path: toml_config.storage.disk.dir_path, - bitcoind_rpc_addr, - bitcoind_rpc_user: toml_config.bitcoind.rpc_user, - bitcoind_rpc_password: toml_config.bitcoind.rpc_password, - rabbitmq_connection_string, - rabbitmq_exchange_name, - lsps2_service_config, - }) - } -} - /// Configuration loaded from a TOML file. #[derive(Deserialize, Serialize)] pub struct TomlConfig { - node: NodeConfig, - storage: StorageConfig, - bitcoind: BitcoindConfig, + node: Option, + storage: Option, + bitcoind: Option, rabbitmq: Option, liquidity: Option, } #[derive(Deserialize, Serialize)] struct NodeConfig { - network: Network, - listening_address: String, - rest_service_address: String, + network: Option, + listening_address: Option, + rest_service_address: Option, alias: Option, } @@ -132,14 +49,14 @@ struct StorageConfig { #[derive(Deserialize, Serialize)] struct DiskConfig { - dir_path: String, + dir_path: Option, } #[derive(Deserialize, Serialize)] struct BitcoindConfig { - rpc_address: String, - rpc_user: String, - rpc_password: String, + rpc_address: Option, + rpc_user: Option, + rpc_password: Option, } #[derive(Deserialize, Serialize)] @@ -194,75 +111,316 @@ impl Into for LSPS2ServiceTomlConfig { } } -/// Loads the configuration from a TOML file at the given path. -pub fn load_config>(config_path: P) -> io::Result { - let file_contents = fs::read_to_string(config_path.as_ref()).map_err(|e| { - io::Error::new( - e.kind(), - format!("Failed to read config file '{}': {}", config_path.as_ref().display(), e), - ) - })?; +#[derive(Parser, Debug)] +#[command(version, about = "LDK Server Configuration", long_about = None)] +pub struct ArgsConfig { + #[arg(required = false)] + config_file: Option, - let toml_config: TomlConfig = toml::from_str(&file_contents).map_err(|e| { - io::Error::new( - io::ErrorKind::InvalidData, - format!("Config file contains invalid TOML format: {}", e), - ) + #[arg(long, env = "LDK_SERVER_NODE_NETWORK")] + node_network: Option, + + #[arg(long, env = "LDK_SERVER_NODE_LISTENING_ADDRESS")] + node_listening_address: Option, + + #[arg(long, env = "LDK_SERVER_NODE_REST_SERVICE_ADDRESS")] + node_rest_service_address: Option, + + #[arg(long, env = "LDK_SERVER_NODE_ALIAS")] + node_alias: Option, + + #[arg(long, env = "LDK_SERVER_BITCOIND_RPC_ADDRESS")] + bitcoind_rpc_address: Option, + + #[arg(long, env = "LDK_SERVER_BITCOIND_RPC_USER")] + bitcoind_rpc_user: Option, + + #[arg(long, env = "LDK_SERVER_BITCOIND_RPC_PASSWORD")] + bitcoind_rpc_password: Option, + + #[arg(long, env = "LDK_SERVER_STORAGE_DIR_PATH")] + storage_dir_path: Option, +} + +pub fn load_config(args_config: &ArgsConfig) -> io::Result { + let toml_config = match &args_config.config_file { + Some(config_path) => { + let file_contents = fs::read_to_string(config_path).map_err(|e| { + io::Error::new( + e.kind(), + format!("Failed to read config file '{}': {}", config_path, e), + ) + })?; + Some(toml::from_str::(&file_contents).map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("Config file contains invalid TOML format: {}", e), + ) + })?) + }, + None => { + #[cfg(feature = "events-rabbitmq")] + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "To use the `events-rabbitmq` feature, you must provide a configuration file.", + )); + + #[cfg(feature = "experimental-lsps2-support")] + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "To use the `experimental-lsps2-support` feature, you must provide a configuration file.", + )); + + #[cfg(not(feature = "events-rabbitmq"))] + #[cfg(not(feature = "experimental-lsps2-support"))] + None + }, + }; + + // Node + let node = toml_config.as_ref().and_then(|t| t.node.as_ref()); + let network = args_config + .node_network + .or(node.and_then(|n| n.network)) + .ok_or_else(|| missing_field_err("network"))?; + + let listening_addr = args_config + .node_listening_address + .as_deref() + .or_else(|| node.and_then(|n| n.listening_address.as_deref())) + .ok_or_else(|| missing_field_err("node_listening_address")) + .and_then(|addr| { + SocketAddress::from_str(addr).map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!("Invalid listening address: {}", e), + ) + }) + })?; + + let rest_service_addr = args_config + .node_rest_service_address + .as_deref() + .or_else(|| node.and_then(|n| n.rest_service_address.as_deref())) + .ok_or_else(|| missing_field_err("rest_service_address")) + .and_then(|addr| { + addr.parse().map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!("Invalid rest service address: {}", e), + ) + }) + })?; + + let alias = args_config + .node_alias + .as_deref() + .or_else(|| node.and_then(|n| n.alias.as_deref())) + .map(|alias_str| { + let mut bytes = [0u8; 32]; + let alias_bytes = alias_str.trim().as_bytes(); + if alias_bytes.len() > 32 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "node.alias must be at most 32 bytes long.".to_string(), + )); + } + bytes[..alias_bytes.len()].copy_from_slice(alias_bytes); + Ok(NodeAlias(bytes)) + }) + .transpose()?; + + // Storage + let storage = toml_config.as_ref().and_then(|t| t.storage.as_ref()); + let storage_dir_path = args_config + .storage_dir_path + .as_deref() + .or_else(|| storage.and_then(|s| s.disk.dir_path.as_deref())) + .ok_or_else(|| missing_field_err("storage_dir_path"))? + .to_string(); + + // Bitcoind + let bitcoind = toml_config.as_ref().and_then(|t| t.bitcoind.as_ref()); + let bitcoind_rpc_addr_str = match args_config + .bitcoind_rpc_address + .as_deref() + .or(bitcoind.and_then(|b| b.rpc_address.as_deref())) + { + Some(addr) => addr, + None => return Err(missing_field_err("bitcoind_rpc_address")), + }; + let bitcoind_rpc_addr = SocketAddr::from_str(bitcoind_rpc_addr_str).map_err(|e| { + io::Error::new(io::ErrorKind::InvalidInput, format!("Invalid bitcoind_rpc_address: {}", e)) })?; - Ok(Config::try_from(toml_config)?) + let bitcoind_rpc_user = args_config + .bitcoind_rpc_user + .as_deref() + .or_else(|| bitcoind.and_then(|b| b.rpc_user.as_deref())) + .ok_or_else(|| missing_field_err("bitcoind_rpc_user"))? + .to_string(); + + let bitcoind_rpc_password = args_config + .bitcoind_rpc_password + .as_deref() + .or_else(|| bitcoind.and_then(|b| b.rpc_password.as_deref())) + .ok_or_else(|| missing_field_err("bitcoind_rpc_password"))? + .to_string(); + + // Load RabbitMQ and LSPS2 configurations + let mut rabbitmq_connection_string = String::new(); + let mut rabbitmq_exchange_name = String::new(); + let mut lsps2_service_config = None; + if let Some(toml_config) = toml_config { + (rabbitmq_connection_string, rabbitmq_exchange_name, lsps2_service_config) = + load_config_feature(toml_config)?; + } + + Ok(Config { + listening_addr, + alias, + network, + rest_service_addr, + storage_dir_path, + bitcoind_rpc_addr, + bitcoind_rpc_user, + bitcoind_rpc_password, + rabbitmq_connection_string, + rabbitmq_exchange_name, + lsps2_service_config, + }) +} + +fn missing_field_err(field: &str) -> io::Error { + io::Error::new( + io::ErrorKind::InvalidInput, + format!( + "Missing `{}`. Please provide it via config file, CLI argument, or environment variable.", + field + ), + ) +} + +fn load_config_feature( + toml_config: TomlConfig, +) -> io::Result<(String, String, Option)> { + let (rabbitmq_connection_string, rabbitmq_exchange_name) = { + let rabbitmq = toml_config.rabbitmq.unwrap_or(RabbitmqConfig { + connection_string: String::new(), + exchange_name: String::new(), + }); + #[cfg(feature = "events-rabbitmq")] + if rabbitmq.connection_string.is_empty() || rabbitmq.exchange_name.is_empty() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Both `rabbitmq.connection_string` and `rabbitmq.exchange_name` must be configured if enabling `events-rabbitmq` feature.".to_string(), + )); + } + (rabbitmq.connection_string, rabbitmq.exchange_name) + }; + + #[cfg(not(feature = "experimental-lsps2-support"))] + let lsps2_service_config: Option = None; + #[cfg(feature = "experimental-lsps2-support")] + let lsps2_service_config = Some(toml_config.liquidity + .and_then(|l| l.lsps2_service) + .ok_or_else(|| io::Error::new( + io::ErrorKind::InvalidInput, + "`liquidity.lsps2_service` must be defined in config if enabling `experimental-lsps2-support` feature." + ))? + .into()); + + Ok((rabbitmq_connection_string, rabbitmq_exchange_name, lsps2_service_config)) } #[cfg(test)] mod tests { use super::*; use ldk_node::{bitcoin::Network, lightning::ln::msgs::SocketAddress}; + + use crate::util::config::{load_config, ArgsConfig}; use std::str::FromStr; + const DEFAULT_CONFIG: &str = r#" + [node] + network = "regtest" + listening_address = "localhost:3001" + rest_service_address = "127.0.0.1:3002" + alias = "LDK Server" + + [storage.disk] + dir_path = "/tmp" + + [bitcoind] + rpc_address = "127.0.0.1:8332" + rpc_user = "bitcoind-testuser" + rpc_password = "bitcoind-testpassword" + + [rabbitmq] + connection_string = "rabbitmq_connection_string" + exchange_name = "rabbitmq_exchange_name" + + [liquidity.lsps2_service] + advertise_service = false + channel_opening_fee_ppm = 1000 # 0.1% fee + channel_over_provisioning_ppm = 500000 # 50% extra capacity + min_channel_opening_fee_msat = 10000000 # 10,000 satoshis + min_channel_lifetime = 4320 # ~30 days + max_client_to_self_delay = 1440 # ~10 days + min_payment_size_msat = 10000000 # 10,000 satoshis + max_payment_size_msat = 25000000000 # 0.25 BTC + "#; + + fn default_args_config() -> ArgsConfig { + ArgsConfig { + config_file: None, + node_network: Some(Network::Regtest), + node_listening_address: Some(String::from("localhost:3008")), + node_rest_service_address: Some(String::from("127.0.0.1:3009")), + bitcoind_rpc_address: Some(String::from("127.0.1.9:18443")), + bitcoind_rpc_user: Some(String::from("bitcoind-testuser_cli")), + bitcoind_rpc_password: Some(String::from("bitcoind-testpassword_cli")), + storage_dir_path: Some(String::from("/tmp_cli")), + node_alias: Some(String::from("LDK Server CLI")), + } + } + + fn missing_field_msg(field: &str) -> String { + format!( + "Missing `{}`. Please provide it via config file, CLI argument, or environment variable.", + field + ) + } + + fn parse_alias(alias_str: &str) -> NodeAlias { + let mut bytes = [0u8; 32]; + let alias_bytes = alias_str.trim().as_bytes(); + bytes[..alias_bytes.len()].copy_from_slice(alias_bytes); + NodeAlias(bytes) + } #[test] - fn test_read_toml_config_from_file() { + fn test_config_from_file() { let storage_path = std::env::temp_dir(); - let config_file_name = "config.toml"; - - let toml_config = r#" - [node] - network = "regtest" - listening_address = "localhost:3001" - rest_service_address = "127.0.0.1:3002" - alias = "LDK Server" - - [storage.disk] - dir_path = "/tmp" - - [bitcoind] - rpc_address = "127.0.0.1:8332" # RPC endpoint - rpc_user = "bitcoind-testuser" - rpc_password = "bitcoind-testpassword" - - [rabbitmq] - connection_string = "rabbitmq_connection_string" - exchange_name = "rabbitmq_exchange_name" - - [liquidity.lsps2_service] - advertise_service = false - channel_opening_fee_ppm = 1000 # 0.1% fee - channel_over_provisioning_ppm = 500000 # 50% extra capacity - min_channel_opening_fee_msat = 10000000 # 10,000 satoshis - min_channel_lifetime = 4320 # ~30 days - max_client_to_self_delay = 1440 # ~10 days - min_payment_size_msat = 10000000 # 10,000 satoshis - max_payment_size_msat = 25000000000 # 0.25 BTC - "#; - - fs::write(storage_path.join(config_file_name), toml_config).unwrap(); + let config_file_name = "test_config_from_file.toml"; - let mut bytes = [0u8; 32]; - let alias = "LDK Server"; - bytes[..alias.as_bytes().len()].copy_from_slice(alias.as_bytes()); + fs::write(storage_path.join(config_file_name), DEFAULT_CONFIG).unwrap(); + let args_config = ArgsConfig { + config_file: Some(storage_path.join(config_file_name).to_string_lossy().to_string()), + node_network: None, + node_listening_address: None, + node_rest_service_address: None, + bitcoind_rpc_address: None, + bitcoind_rpc_user: None, + bitcoind_rpc_password: None, + storage_dir_path: None, + node_alias: None, + }; - let config = load_config(storage_path.join(config_file_name)).unwrap(); + let config = load_config(&args_config).unwrap(); + + let alias = "LDK Server"; let expected = Config { listening_addr: SocketAddress::from_str("localhost:3001").unwrap(), - alias: Some(NodeAlias(bytes)), + alias: Some(parse_alias(alias)), network: Network::Regtest, rest_service_addr: SocketAddr::from_str("127.0.0.1:3002").unwrap(), storage_dir_path: "/tmp".to_string(), @@ -296,4 +454,240 @@ mod tests { #[cfg(feature = "experimental-lsps2-support")] assert_eq!(config.lsps2_service_config.is_some(), expected.lsps2_service_config.is_some()); } + + #[test] + fn test_config_missing_fields_in_file() { + let storage_path = std::env::temp_dir(); + let config_file_name = "test_config_missing_fields_in_file.toml"; + let mut toml_config = DEFAULT_CONFIG.to_string(); + let args_config = ArgsConfig { + config_file: Some(storage_path.join(config_file_name).to_string_lossy().to_string()), + node_network: None, + node_listening_address: None, + node_rest_service_address: None, + bitcoind_rpc_address: None, + bitcoind_rpc_user: None, + bitcoind_rpc_password: None, + storage_dir_path: None, + node_alias: None, + }; + + macro_rules! validate_missing { + ($field:expr, $err_msg:expr) => { + toml_config = remove_config_line(&toml_config, &format!("{} =", $field)); + fs::write(storage_path.join(config_file_name), &toml_config).unwrap(); + let result = load_config(&args_config); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + assert_eq!(err.to_string(), $err_msg); + }; + } + + #[cfg(feature = "experimental-lsps2-support")] + { + toml_config = remove_config_line(&toml_config, "[liquidity.lsps2_service]"); + validate_missing!( + "lsps2_service", + "`liquidity.lsps2_service` must be defined in config if enabling `experimental-lsps2-support` feature." + ); + } + + #[cfg(feature = "events-rabbitmq")] + { + toml_config = remove_config_line(&toml_config, "[rabbitmq]"); + validate_missing!( + "rabbitmq", + "Both `rabbitmq.connection_string` and `rabbitmq.exchange_name` must be configured if enabling `events-rabbitmq` feature." + ); + } + + // The order here is important: it is the reverse of the validation order in `load_config` + validate_missing!("rpc_password", missing_field_msg("bitcoind_rpc_password")); + validate_missing!("rpc_user", missing_field_msg("bitcoind_rpc_user")); + validate_missing!("rpc_address", missing_field_msg("bitcoind_rpc_address")); + validate_missing!("dir_path", missing_field_msg("storage_dir_path")); + validate_missing!("rest_service_address", missing_field_msg("rest_service_address")); + validate_missing!("listening_address", missing_field_msg("node_listening_address")); + validate_missing!("network", missing_field_msg("network")); + } + fn remove_config_line(config: &str, key: &str) -> String { + config + .lines() + .filter(|line| !line.trim_start().starts_with(key)) + .collect::>() + .join("\n") + } + + #[test] + #[cfg(not(feature = "experimental-lsps2-support"))] + #[cfg(not(feature = "events-rabbitmq"))] + fn test_config_from_args_config() { + let args_config = default_args_config(); + let config = load_config(&args_config).unwrap(); + + let expected = Config { + listening_addr: SocketAddress::from_str( + args_config.node_listening_address.as_deref().unwrap(), + ) + .unwrap(), + network: Network::Regtest, + rest_service_addr: SocketAddr::from_str( + args_config.node_rest_service_address.as_deref().unwrap(), + ) + .unwrap(), + alias: Some(parse_alias(args_config.node_alias.as_deref().unwrap())), + storage_dir_path: args_config.storage_dir_path.unwrap(), + bitcoind_rpc_addr: SocketAddr::from_str( + args_config.bitcoind_rpc_address.as_deref().unwrap(), + ) + .unwrap(), + bitcoind_rpc_user: args_config.bitcoind_rpc_user.unwrap(), + bitcoind_rpc_password: args_config.bitcoind_rpc_password.unwrap(), + rabbitmq_connection_string: String::new(), + rabbitmq_exchange_name: String::new(), + lsps2_service_config: None, + }; + + assert_eq!(config.listening_addr, expected.listening_addr); + assert_eq!(config.network, expected.network); + assert_eq!(config.rest_service_addr, expected.rest_service_addr); + assert_eq!(config.storage_dir_path, expected.storage_dir_path); + assert_eq!(config.bitcoind_rpc_addr, expected.bitcoind_rpc_addr); + assert_eq!(config.bitcoind_rpc_user, expected.bitcoind_rpc_user); + assert_eq!(config.bitcoind_rpc_password, expected.bitcoind_rpc_password); + assert_eq!(config.rabbitmq_connection_string, expected.rabbitmq_connection_string); + assert_eq!(config.rabbitmq_exchange_name, expected.rabbitmq_exchange_name); + assert!(config.lsps2_service_config.is_none()); + } + + #[test] + #[cfg(not(feature = "experimental-lsps2-support"))] + #[cfg(not(feature = "events-rabbitmq"))] + fn test_config_missing_fields_in_args_config() { + let mut args_config = default_args_config(); + + macro_rules! validate_missing { + ($field:ident, $err_msg:expr) => { + args_config.$field = None; + let result = load_config(&args_config); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + assert_eq!(err.to_string(), $err_msg); + }; + } + // The order here is important: it is the reverse of the validation order in `load_config` + validate_missing!(bitcoind_rpc_password, missing_field_msg("bitcoind_rpc_password")); + validate_missing!(bitcoind_rpc_user, missing_field_msg("bitcoind_rpc_user")); + validate_missing!(bitcoind_rpc_address, missing_field_msg("bitcoind_rpc_address")); + validate_missing!(storage_dir_path, missing_field_msg("storage_dir_path")); + validate_missing!(node_rest_service_address, missing_field_msg("rest_service_address")); + validate_missing!(node_listening_address, missing_field_msg("node_listening_address")); + validate_missing!(node_network, missing_field_msg("network")); + } + + #[test] + fn test_args_config_overrides_file() { + let storage_path = std::env::temp_dir(); + let config_file_name = "test_args_config_overrides_file.toml"; + + fs::write(storage_path.join(config_file_name), DEFAULT_CONFIG).unwrap(); + let mut args_config: ArgsConfig = default_args_config(); + args_config.config_file = + Some(storage_path.join(config_file_name).to_string_lossy().to_string()); + + let config = load_config(&args_config).unwrap(); + let expected = Config { + listening_addr: SocketAddress::from_str( + args_config.node_listening_address.as_deref().unwrap(), + ) + .unwrap(), + network: Network::Regtest, + rest_service_addr: SocketAddr::from_str( + args_config.node_rest_service_address.as_deref().unwrap(), + ) + .unwrap(), + alias: Some(parse_alias(args_config.node_alias.as_deref().unwrap())), + storage_dir_path: args_config.storage_dir_path.unwrap(), + bitcoind_rpc_addr: SocketAddr::from_str( + args_config.bitcoind_rpc_address.as_deref().unwrap(), + ) + .unwrap(), + bitcoind_rpc_user: args_config.bitcoind_rpc_user.unwrap(), + bitcoind_rpc_password: args_config.bitcoind_rpc_password.unwrap(), + rabbitmq_connection_string: "rabbitmq_connection_string".to_string(), + rabbitmq_exchange_name: "rabbitmq_exchange_name".to_string(), + lsps2_service_config: Some(LSPS2ServiceConfig { + require_token: None, + advertise_service: false, + channel_opening_fee_ppm: 1000, + channel_over_provisioning_ppm: 500000, + min_channel_opening_fee_msat: 10000000, + min_channel_lifetime: 4320, + max_client_to_self_delay: 1440, + min_payment_size_msat: 10000000, + max_payment_size_msat: 25000000000, + }), + }; + + assert_eq!(config.listening_addr, expected.listening_addr); + assert_eq!(config.network, expected.network); + assert_eq!(config.rest_service_addr, expected.rest_service_addr); + assert_eq!(config.storage_dir_path, expected.storage_dir_path); + assert_eq!(config.bitcoind_rpc_addr, expected.bitcoind_rpc_addr); + assert_eq!(config.bitcoind_rpc_user, expected.bitcoind_rpc_user); + assert_eq!(config.bitcoind_rpc_password, expected.bitcoind_rpc_password); + assert_eq!(config.rabbitmq_connection_string, expected.rabbitmq_connection_string); + assert_eq!(config.rabbitmq_exchange_name, expected.rabbitmq_exchange_name); + #[cfg(feature = "experimental-lsps2-support")] + assert_eq!(config.lsps2_service_config.is_some(), expected.lsps2_service_config.is_some()); + } + + #[test] + #[cfg(feature = "events-rabbitmq")] + fn test_error_if_rabbitmq_feature_without_config_file() { + let args_config = ArgsConfig { + config_file: None, + node_network: None, + node_listening_address: None, + node_rest_service_address: None, + node_alias: None, + bitcoind_rpc_host: None, + bitcoind_rpc_port: None, + bitcoind_rpc_user: None, + bitcoind_rpc_password: None, + storage_dir_path: None, + }; + let result = load_config(&args_config); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + assert_eq!( + err.to_string(), + "To use the `events-rabbitmq` feature, you must provide a configuration file." + ); + } + + #[test] + #[cfg(feature = "experimental-lsps2-support")] + fn test_error_if_lsps2_feature_without_config_file() { + let args_config = ArgsConfig { + config_file: None, + node_network: None, + node_listening_address: None, + node_rest_service_address: None, + node_alias: None, + bitcoind_rpc_host: None, + bitcoind_rpc_port: None, + bitcoind_rpc_user: None, + bitcoind_rpc_password: None, + storage_dir_path: None, + }; + let result = load_config(&args_config); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + assert_eq!(err.to_string(), "To use the `experimental-lsps2-support` feature, you must provide a configuration file."); + } } From 43d1a52f9014f78ff6a1b4d289d897fb3b22c4be Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Mon, 1 Sep 2025 14:28:01 -0300 Subject: [PATCH 2/2] Split `bitcoind_rpc_address` into `bitcoind_rpc_host` and `bitcoind_rpc_port` This change replaces the single RPC address field with separate host and port fields, allowing hostname support and improving compatibility with containerized environments. Closes #66 juntar com o segundo commit --- README.md | 6 ++- ldk-server/src/main.rs | 6 +-- ldk-server/src/util/config.rs | 79 ++++++++++++++++++++--------------- 3 files changed, 52 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 404ebe7..4b8d168 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,8 @@ export LDK_SERVER_NODE_NETWORK=regtest export LDK_SERVER_NODE_LISTENING_ADDRESS=localhost:3001 export LDK_SERVER_NODE_REST_SERVICE_ADDRESS=127.0.0.1:3002 export LDK_SERVER_NODE_ALIAS=LDK-Server -export LDK_SERVER_BITCOIND_RPC_ADDRESS=127.0.0.1:18443 +export LDK_SERVER_BITCOIND_RPC_HOST=127.0.0.1 +export LDK_SERVER_BITCOIND_RPC_PORT=18443 export LDK_SERVER_BITCOIND_RPC_USER=your-rpc-user export LDK_SERVER_BITCOIND_RPC_PASSWORD=your-rpc-password export LDK_SERVER_STORAGE_DIR_PATH=/path/to/storage @@ -72,7 +73,8 @@ cargo run --bin ldk-server -- \ --node-listening-address localhost:3001 \ --node-rest-service-address 127.0.0.1:3002 \ --node-alias LDK-Server \ - --bitcoind-rpc-address 127.0.0.1:18443 \ + --bitcoind-rpc-host 127.0.0.1 \ + --bitcoind-rpc-port 18443 \ --bitcoind-rpc-user your-rpc-user \ --bitcoind-rpc-password your-rpc-password \ --storage-dir-path /path/to/storage diff --git a/ldk-server/src/main.rs b/ldk-server/src/main.rs index 086a09f..9a9b470 100644 --- a/ldk-server/src/main.rs +++ b/ldk-server/src/main.rs @@ -62,11 +62,9 @@ fn main() { let mut builder = Builder::from_config(ldk_node_config); builder.set_log_facade_logger(); - let bitcoind_rpc_addr = config_file.bitcoind_rpc_addr; - builder.set_chain_source_bitcoind_rpc( - bitcoind_rpc_addr.ip().to_string(), - bitcoind_rpc_addr.port(), + config_file.bitcoind_rpc_host, + config_file.bitcoind_rpc_port, config_file.bitcoind_rpc_user, config_file.bitcoind_rpc_password, ); diff --git a/ldk-server/src/util/config.rs b/ldk-server/src/util/config.rs index f4b3701..980f3c5 100644 --- a/ldk-server/src/util/config.rs +++ b/ldk-server/src/util/config.rs @@ -16,7 +16,8 @@ pub struct Config { pub network: Network, pub rest_service_addr: SocketAddr, pub storage_dir_path: String, - pub bitcoind_rpc_addr: SocketAddr, + pub bitcoind_rpc_host: String, + pub bitcoind_rpc_port: u16, pub bitcoind_rpc_user: String, pub bitcoind_rpc_password: String, pub rabbitmq_connection_string: String, @@ -54,7 +55,8 @@ struct DiskConfig { #[derive(Deserialize, Serialize)] struct BitcoindConfig { - rpc_address: Option, + rpc_host: Option, + rpc_port: Option, rpc_user: Option, rpc_password: Option, } @@ -129,8 +131,11 @@ pub struct ArgsConfig { #[arg(long, env = "LDK_SERVER_NODE_ALIAS")] node_alias: Option, - #[arg(long, env = "LDK_SERVER_BITCOIND_RPC_ADDRESS")] - bitcoind_rpc_address: Option, + #[arg(long, env = "LDK_SERVER_BITCOIND_RPC_HOST")] + bitcoind_rpc_host: Option, + + #[arg(long, env = "LDK_SERVER_BITCOIND_RPC_PORT")] + bitcoind_rpc_port: Option, #[arg(long, env = "LDK_SERVER_BITCOIND_RPC_USER")] bitcoind_rpc_user: Option, @@ -241,17 +246,18 @@ pub fn load_config(args_config: &ArgsConfig) -> io::Result { // Bitcoind let bitcoind = toml_config.as_ref().and_then(|t| t.bitcoind.as_ref()); - let bitcoind_rpc_addr_str = match args_config - .bitcoind_rpc_address + let bitcoind_rpc_host = args_config + .bitcoind_rpc_host .as_deref() - .or(bitcoind.and_then(|b| b.rpc_address.as_deref())) - { - Some(addr) => addr, - None => return Err(missing_field_err("bitcoind_rpc_address")), - }; - let bitcoind_rpc_addr = SocketAddr::from_str(bitcoind_rpc_addr_str).map_err(|e| { - io::Error::new(io::ErrorKind::InvalidInput, format!("Invalid bitcoind_rpc_address: {}", e)) - })?; + .or_else(|| bitcoind.and_then(|b| b.rpc_host.as_deref())) + .ok_or_else(|| missing_field_err("bitcoind_rpc_host"))? + .to_string(); + + let bitcoind_rpc_port = args_config + .bitcoind_rpc_port + .or_else(|| bitcoind.and_then(|b| b.rpc_port)) + .ok_or_else(|| missing_field_err("bitcoind_rpc_port"))?; + let bitcoind_rpc_user = args_config .bitcoind_rpc_user .as_deref() @@ -281,7 +287,8 @@ pub fn load_config(args_config: &ArgsConfig) -> io::Result { network, rest_service_addr, storage_dir_path, - bitcoind_rpc_addr, + bitcoind_rpc_host, + bitcoind_rpc_port, bitcoind_rpc_user, bitcoind_rpc_password, rabbitmq_connection_string, @@ -350,7 +357,8 @@ mod tests { dir_path = "/tmp" [bitcoind] - rpc_address = "127.0.0.1:8332" + rpc_host = "127.0.0.1" + rpc_port = 8332 rpc_user = "bitcoind-testuser" rpc_password = "bitcoind-testpassword" @@ -375,7 +383,8 @@ mod tests { node_network: Some(Network::Regtest), node_listening_address: Some(String::from("localhost:3008")), node_rest_service_address: Some(String::from("127.0.0.1:3009")), - bitcoind_rpc_address: Some(String::from("127.0.1.9:18443")), + bitcoind_rpc_host: Some(String::from("127.0.1.9")), + bitcoind_rpc_port: Some(18443), bitcoind_rpc_user: Some(String::from("bitcoind-testuser_cli")), bitcoind_rpc_password: Some(String::from("bitcoind-testpassword_cli")), storage_dir_path: Some(String::from("/tmp_cli")), @@ -408,7 +417,8 @@ mod tests { node_network: None, node_listening_address: None, node_rest_service_address: None, - bitcoind_rpc_address: None, + bitcoind_rpc_host: None, + bitcoind_rpc_port: None, bitcoind_rpc_user: None, bitcoind_rpc_password: None, storage_dir_path: None, @@ -424,7 +434,8 @@ mod tests { network: Network::Regtest, rest_service_addr: SocketAddr::from_str("127.0.0.1:3002").unwrap(), storage_dir_path: "/tmp".to_string(), - bitcoind_rpc_addr: SocketAddr::from_str("127.0.0.1:8332").unwrap(), + bitcoind_rpc_host: "127.0.0.1".to_string(), + bitcoind_rpc_port: 8332, bitcoind_rpc_user: "bitcoind-testuser".to_string(), bitcoind_rpc_password: "bitcoind-testpassword".to_string(), rabbitmq_connection_string: "rabbitmq_connection_string".to_string(), @@ -446,7 +457,8 @@ mod tests { assert_eq!(config.network, expected.network); assert_eq!(config.rest_service_addr, expected.rest_service_addr); assert_eq!(config.storage_dir_path, expected.storage_dir_path); - assert_eq!(config.bitcoind_rpc_addr, expected.bitcoind_rpc_addr); + assert_eq!(config.bitcoind_rpc_host, expected.bitcoind_rpc_host); + assert_eq!(config.bitcoind_rpc_port, expected.bitcoind_rpc_port); assert_eq!(config.bitcoind_rpc_user, expected.bitcoind_rpc_user); assert_eq!(config.bitcoind_rpc_password, expected.bitcoind_rpc_password); assert_eq!(config.rabbitmq_connection_string, expected.rabbitmq_connection_string); @@ -465,7 +477,8 @@ mod tests { node_network: None, node_listening_address: None, node_rest_service_address: None, - bitcoind_rpc_address: None, + bitcoind_rpc_host: None, + bitcoind_rpc_port: None, bitcoind_rpc_user: None, bitcoind_rpc_password: None, storage_dir_path: None, @@ -505,7 +518,8 @@ mod tests { // The order here is important: it is the reverse of the validation order in `load_config` validate_missing!("rpc_password", missing_field_msg("bitcoind_rpc_password")); validate_missing!("rpc_user", missing_field_msg("bitcoind_rpc_user")); - validate_missing!("rpc_address", missing_field_msg("bitcoind_rpc_address")); + validate_missing!("rpc_port", missing_field_msg("bitcoind_rpc_port")); + validate_missing!("rpc_host", missing_field_msg("bitcoind_rpc_host")); validate_missing!("dir_path", missing_field_msg("storage_dir_path")); validate_missing!("rest_service_address", missing_field_msg("rest_service_address")); validate_missing!("listening_address", missing_field_msg("node_listening_address")); @@ -538,10 +552,8 @@ mod tests { .unwrap(), alias: Some(parse_alias(args_config.node_alias.as_deref().unwrap())), storage_dir_path: args_config.storage_dir_path.unwrap(), - bitcoind_rpc_addr: SocketAddr::from_str( - args_config.bitcoind_rpc_address.as_deref().unwrap(), - ) - .unwrap(), + bitcoind_rpc_host: args_config.bitcoind_rpc_host.unwrap(), + bitcoind_rpc_port: args_config.bitcoind_rpc_port.unwrap(), bitcoind_rpc_user: args_config.bitcoind_rpc_user.unwrap(), bitcoind_rpc_password: args_config.bitcoind_rpc_password.unwrap(), rabbitmq_connection_string: String::new(), @@ -553,7 +565,8 @@ mod tests { assert_eq!(config.network, expected.network); assert_eq!(config.rest_service_addr, expected.rest_service_addr); assert_eq!(config.storage_dir_path, expected.storage_dir_path); - assert_eq!(config.bitcoind_rpc_addr, expected.bitcoind_rpc_addr); + assert_eq!(config.bitcoind_rpc_host, expected.bitcoind_rpc_host); + assert_eq!(config.bitcoind_rpc_port, expected.bitcoind_rpc_port); assert_eq!(config.bitcoind_rpc_user, expected.bitcoind_rpc_user); assert_eq!(config.bitcoind_rpc_password, expected.bitcoind_rpc_password); assert_eq!(config.rabbitmq_connection_string, expected.rabbitmq_connection_string); @@ -580,7 +593,8 @@ mod tests { // The order here is important: it is the reverse of the validation order in `load_config` validate_missing!(bitcoind_rpc_password, missing_field_msg("bitcoind_rpc_password")); validate_missing!(bitcoind_rpc_user, missing_field_msg("bitcoind_rpc_user")); - validate_missing!(bitcoind_rpc_address, missing_field_msg("bitcoind_rpc_address")); + validate_missing!(bitcoind_rpc_port, missing_field_msg("bitcoind_rpc_port")); + validate_missing!(bitcoind_rpc_host, missing_field_msg("bitcoind_rpc_host")); validate_missing!(storage_dir_path, missing_field_msg("storage_dir_path")); validate_missing!(node_rest_service_address, missing_field_msg("rest_service_address")); validate_missing!(node_listening_address, missing_field_msg("node_listening_address")); @@ -610,10 +624,8 @@ mod tests { .unwrap(), alias: Some(parse_alias(args_config.node_alias.as_deref().unwrap())), storage_dir_path: args_config.storage_dir_path.unwrap(), - bitcoind_rpc_addr: SocketAddr::from_str( - args_config.bitcoind_rpc_address.as_deref().unwrap(), - ) - .unwrap(), + bitcoind_rpc_host: args_config.bitcoind_rpc_host.unwrap(), + bitcoind_rpc_port: args_config.bitcoind_rpc_port.unwrap(), bitcoind_rpc_user: args_config.bitcoind_rpc_user.unwrap(), bitcoind_rpc_password: args_config.bitcoind_rpc_password.unwrap(), rabbitmq_connection_string: "rabbitmq_connection_string".to_string(), @@ -635,7 +647,8 @@ mod tests { assert_eq!(config.network, expected.network); assert_eq!(config.rest_service_addr, expected.rest_service_addr); assert_eq!(config.storage_dir_path, expected.storage_dir_path); - assert_eq!(config.bitcoind_rpc_addr, expected.bitcoind_rpc_addr); + assert_eq!(config.bitcoind_rpc_host, expected.bitcoind_rpc_host); + assert_eq!(config.bitcoind_rpc_port, expected.bitcoind_rpc_port); assert_eq!(config.bitcoind_rpc_user, expected.bitcoind_rpc_user); assert_eq!(config.bitcoind_rpc_password, expected.bitcoind_rpc_password); assert_eq!(config.rabbitmq_connection_string, expected.rabbitmq_connection_string);