From f840244fd7fea7b34fec613f2276945da3b582de Mon Sep 17 00:00:00 2001 From: benthecarman Date: Wed, 4 Mar 2026 11:02:06 -0600 Subject: [PATCH 1/5] Allow configuring electrum/esplora sync configs --- ldk-server/src/main.rs | 49 +++++++++++++++++++++++++++++---- ldk-server/src/util/config.rs | 51 ++++++++++++++++++++++++++++++----- 2 files changed, 89 insertions(+), 11 deletions(-) diff --git a/ldk-server/src/main.rs b/ldk-server/src/main.rs index e6bce80..d3e6a5d 100644 --- a/ldk-server/src/main.rs +++ b/ldk-server/src/main.rs @@ -23,7 +23,7 @@ use hex::DisplayHex; use hyper::server::conn::http1; use hyper_util::rt::TokioIo; use ldk_node::bitcoin::Network; -use ldk_node::config::Config; +use ldk_node::config::{BackgroundSyncConfig, Config, ElectrumSyncConfig, EsploraSyncConfig}; use ldk_node::entropy::NodeEntropy; use ldk_node::lightning::ln::channelmanager::PaymentId; use ldk_node::{Builder, Event, Node}; @@ -73,6 +73,24 @@ pub fn get_default_data_dir() -> Option { } } +fn build_background_sync_config( + onchain_wallet_sync_interval_secs: Option, + lightning_wallet_sync_interval_secs: Option, +) -> Option { + if onchain_wallet_sync_interval_secs.is_none() && lightning_wallet_sync_interval_secs.is_none() + { + return None; + } + let mut bg = BackgroundSyncConfig::default(); + if let Some(interval) = onchain_wallet_sync_interval_secs { + bg.onchain_wallet_sync_interval_secs = interval; + } + if let Some(interval) = lightning_wallet_sync_interval_secs { + bg.lightning_wallet_sync_interval_secs = interval; + } + Some(bg) +} + fn main() { let args_config = ArgsConfig::parse(); @@ -156,11 +174,32 @@ fn main() { ChainSource::Rpc { rpc_host, rpc_port, rpc_user, rpc_password } => { builder.set_chain_source_bitcoind_rpc(rpc_host, rpc_port, rpc_user, rpc_password); }, - ChainSource::Electrum { server_url } => { - builder.set_chain_source_electrum(server_url, None); + ChainSource::Electrum { + server_url, + onchain_wallet_sync_interval_secs, + lightning_wallet_sync_interval_secs, + } => { + let sync_config = build_background_sync_config( + onchain_wallet_sync_interval_secs, + lightning_wallet_sync_interval_secs, + ) + .map(|bg| ElectrumSyncConfig { + background_sync_config: Some(bg), + ..Default::default() + }); + builder.set_chain_source_electrum(server_url, sync_config); }, - ChainSource::Esplora { server_url } => { - builder.set_chain_source_esplora(server_url, None); + ChainSource::Esplora { + server_url, + onchain_wallet_sync_interval_secs, + lightning_wallet_sync_interval_secs, + } => { + let sync_config = build_background_sync_config( + onchain_wallet_sync_interval_secs, + lightning_wallet_sync_interval_secs, + ) + .map(|bg| EsploraSyncConfig { background_sync_config: Some(bg), ..Default::default() }); + builder.set_chain_source_esplora(server_url, sync_config); }, } diff --git a/ldk-server/src/util/config.rs b/ldk-server/src/util/config.rs index dbc452a..10ce89f 100644 --- a/ldk-server/src/util/config.rs +++ b/ldk-server/src/util/config.rs @@ -64,9 +64,22 @@ pub struct TlsConfig { #[derive(Debug, PartialEq, Eq)] pub enum ChainSource { - Rpc { rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String }, - Electrum { server_url: String }, - Esplora { server_url: String }, + Rpc { + rpc_host: String, + rpc_port: u16, + rpc_user: String, + rpc_password: String, + }, + Electrum { + server_url: String, + onchain_wallet_sync_interval_secs: Option, + lightning_wallet_sync_interval_secs: Option, + }, + Esplora { + server_url: String, + onchain_wallet_sync_interval_secs: Option, + lightning_wallet_sync_interval_secs: Option, + }, } /// A builder for `Config`. @@ -81,6 +94,8 @@ struct ConfigBuilder { storage_dir_path: Option, electrum_url: Option, esplora_url: Option, + onchain_wallet_sync_interval_secs: Option, + lightning_wallet_sync_interval_secs: Option, bitcoind_rpc_address: Option, bitcoind_rpc_user: Option, bitcoind_rpc_password: Option, @@ -123,10 +138,22 @@ impl ConfigBuilder { if let Some(electrum) = toml.electrum { self.electrum_url = Some(electrum.server_url); + self.onchain_wallet_sync_interval_secs = electrum + .onchain_wallet_sync_interval_secs + .or(self.onchain_wallet_sync_interval_secs); + self.lightning_wallet_sync_interval_secs = electrum + .lightning_wallet_sync_interval_secs + .or(self.lightning_wallet_sync_interval_secs); } if let Some(esplora) = toml.esplora { self.esplora_url = Some(esplora.server_url); + self.onchain_wallet_sync_interval_secs = esplora + .onchain_wallet_sync_interval_secs + .or(self.onchain_wallet_sync_interval_secs); + self.lightning_wallet_sync_interval_secs = esplora + .lightning_wallet_sync_interval_secs + .or(self.lightning_wallet_sync_interval_secs); } if let Some(log) = toml.log { @@ -280,9 +307,17 @@ impl ConfigBuilder { ChainSource::Rpc { rpc_host, rpc_port, rpc_user, rpc_password } } else if let Some(url) = self.electrum_url { - ChainSource::Electrum { server_url: url } + ChainSource::Electrum { + server_url: url, + onchain_wallet_sync_interval_secs: self.onchain_wallet_sync_interval_secs, + lightning_wallet_sync_interval_secs: self.lightning_wallet_sync_interval_secs, + } } else if let Some(url) = self.esplora_url { - ChainSource::Esplora { server_url: url } + ChainSource::Esplora { + server_url: url, + onchain_wallet_sync_interval_secs: self.onchain_wallet_sync_interval_secs, + lightning_wallet_sync_interval_secs: self.lightning_wallet_sync_interval_secs, + } } else { return Err(io::Error::new(io::ErrorKind::InvalidInput, "No valid Chain Source configured. Provide Bitcoind RPC, Electrum, or Esplora details.")); }; @@ -408,11 +443,15 @@ struct BitcoindConfig { #[derive(Deserialize, Serialize)] struct ElectrumConfig { server_url: String, + onchain_wallet_sync_interval_secs: Option, + lightning_wallet_sync_interval_secs: Option, } #[derive(Deserialize, Serialize)] struct EsploraConfig { server_url: String, + onchain_wallet_sync_interval_secs: Option, + lightning_wallet_sync_interval_secs: Option, } #[derive(Deserialize, Serialize)] @@ -837,7 +876,7 @@ mod tests { fs::write(storage_path.join(config_file_name), toml_config).unwrap(); let config = load_config(&args_config).unwrap(); - let ChainSource::Electrum { server_url } = config.chain_source else { + let ChainSource::Electrum { server_url, .. } = config.chain_source else { panic!("unexpected chain source"); }; From 28ef47cd0f37052c57b3d609f78b9b5dddcdf82e Mon Sep 17 00:00:00 2001 From: benthecarman Date: Tue, 3 Mar 2026 16:48:58 -0600 Subject: [PATCH 2/5] Add electrsd for esplora/electrum chain source e2e tests Add electrsd and rand dependencies to run real electrs processes in tests. Tests now randomly pick between bitcoind RPC, electrum, and esplora as the chain source for broader coverage. Co-Authored-By: Claude Opus 4.6 (1M context) --- e2e-tests/Cargo.lock | 49 ++++++++++- e2e-tests/Cargo.toml | 2 + e2e-tests/src/lib.rs | 199 ++++++++++++++++++++++++++++++++++--------- 3 files changed, 206 insertions(+), 44 deletions(-) diff --git a/e2e-tests/Cargo.lock b/e2e-tests/Cargo.lock index 13326fd..d9bc829 100644 --- a/e2e-tests/Cargo.lock +++ b/e2e-tests/Cargo.lock @@ -869,6 +869,7 @@ name = "e2e-tests" version = "0.1.0" dependencies = [ "corepc-node", + "electrsd", "futures-util", "hex-conservative", "lapin", @@ -876,6 +877,7 @@ dependencies = [ "ldk-server-client", "ldk-server-protos", "prost", + "rand 0.9.2", "serde_json", "tempfile", "tokio", @@ -887,6 +889,22 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "electrsd" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8926868af723c2819807809e54585992aaea0e26a6f5089ac8c2598eaec8d01" +dependencies = [ + "bitcoin_hashes", + "corepc-client", + "corepc-node", + "electrum-client", + "log", + "minreq", + "nix", + "zip", +] + [[package]] name = "electrum-client" version = "0.24.1" @@ -1526,7 +1544,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.2", "tokio", "tower-service", "tracing", @@ -2072,6 +2090,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -2143,6 +2170,20 @@ dependencies = [ "bitcoin", ] +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + [[package]] name = "nom" version = "7.1.3" @@ -2529,7 +2570,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls 0.23.36", - "socket2 0.5.10", + "socket2 0.6.2", "thiserror", "tokio", "tracing", @@ -2566,9 +2607,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] diff --git a/e2e-tests/Cargo.toml b/e2e-tests/Cargo.toml index 5576b7d..27aaef1 100644 --- a/e2e-tests/Cargo.toml +++ b/e2e-tests/Cargo.toml @@ -15,3 +15,5 @@ lapin = { version = "2.4.0", features = ["rustls"], default-features = false } prost = { version = "0.11.6", default-features = false, features = ["std"] } futures-util = "0.3" ldk-node = { git = "https://github.com/lightningdevkit/ldk-node", rev = "d1bbf978c8b7abe87ae2e40793556c1fe4e7ea49" } +electrsd = { version = "0.36", features = ["esplora_a33e97e1", "corepc-node_29_0"] } +rand = "0.9" diff --git a/e2e-tests/src/lib.rs b/e2e-tests/src/lib.rs index 8b34fd2..c5103ea 100644 --- a/e2e-tests/src/lib.rs +++ b/e2e-tests/src/lib.rs @@ -84,35 +84,54 @@ impl TestBitcoind { } } -/// Handle to a running ldk-server child process. -pub struct LdkServerHandle { - child: Option, +/// Wrapper around an electrsd process providing both Electrum and Esplora endpoints. +pub struct TestElectrs { + pub electrsd: electrsd::ElectrsD, +} + +impl TestElectrs { + /// Start an electrs instance connected to the given bitcoind with Esplora HTTP enabled. + pub fn new(bitcoind: &TestBitcoind) -> Self { + let mut conf = electrsd::Conf::default(); + conf.http_enabled = true; + let electrsd = + electrsd::ElectrsD::with_conf(electrsd::exe_path().unwrap(), &bitcoind.bitcoind, &conf) + .unwrap(); + Self { electrsd } + } + + pub fn electrum_url(&self) -> String { + // electrsd binds to 0.0.0.0 but that's not a connectable address for clients + self.electrsd.electrum_url.replace("0.0.0.0", "127.0.0.1") + } + + pub fn esplora_url(&self) -> String { + let url = self.electrsd.esplora_url.as_ref().expect("esplora not enabled"); + // electrsd binds to 0.0.0.0 but that's not a connectable address for clients + format!("http://{}", url.replace("0.0.0.0", "127.0.0.1")) + } + + /// Trigger electrs to sync with bitcoind. + pub fn trigger(&self) { + self.electrsd.trigger().unwrap(); + } +} + +/// Dynamic parameters available when building test configs. +pub struct TestServerParams { pub rest_port: u16, pub p2p_port: u16, pub storage_dir: PathBuf, - pub api_key: String, - pub tls_cert_path: PathBuf, - pub node_id: String, + pub rpc_address: String, + pub rpc_user: String, + pub rpc_password: String, pub exchange_name: String, - client: LdkServerClient, } -impl LdkServerHandle { - /// Starts a new ldk-server instance against the given bitcoind. - /// Waits until the server is ready to accept requests. - pub async fn start(bitcoind: &TestBitcoind) -> Self { - #[allow(deprecated)] - let storage_dir = tempfile::tempdir().unwrap().into_path(); - let rest_port = find_available_port(); - let p2p_port = find_available_port(); - - let (rpc_host, rpc_port_num, rpc_user, rpc_password) = bitcoind.rpc_details(); - let rpc_address = format!("{rpc_host}:{rpc_port_num}"); - - let exchange_name = format!("e2e_test_exchange_{rest_port}"); - - let config_content = format!( - r#"[node] +/// Generate a test config TOML with a custom chain source section. +pub fn test_config_with_chain_source(params: &TestServerParams, chain_source_toml: &str) -> String { + format!( + r#"[node] network = "regtest" listening_addresses = ["127.0.0.1:{p2p_port}"] rest_service_address = "127.0.0.1:{rest_port}" @@ -121,10 +140,7 @@ alias = "e2e-test-node" [storage.disk] dir_path = "{storage_dir}" -[bitcoind] -rpc_address = "{rpc_address}" -rpc_user = "{rpc_user}" -rpc_password = "{rpc_password}" +{chain_source} [rabbitmq] connection_string = "amqp://guest:guest@localhost:5672/%2f" @@ -141,21 +157,81 @@ min_payment_size_msat = 0 max_payment_size_msat = 1000000000 client_trusts_lsp = true "#, - storage_dir = storage_dir.display(), - ); + p2p_port = params.p2p_port, + rest_port = params.rest_port, + storage_dir = params.storage_dir.display(), + chain_source = chain_source_toml, + exchange_name = params.exchange_name, + ) +} + +/// Generate the default test config TOML with bitcoind RPC chain source. +pub fn default_test_config(params: &TestServerParams) -> String { + let chain_source = format!( + "[bitcoind]\nrpc_address = \"{}\"\nrpc_user = \"{}\"\nrpc_password = \"{}\"", + params.rpc_address, params.rpc_user, params.rpc_password + ); + test_config_with_chain_source(params, &chain_source) +} - let config_path = storage_dir.join("config.toml"); - std::fs::write(&config_path, &config_content).unwrap(); +/// Handle to a running ldk-server child process. +pub struct LdkServerHandle { + child: Option, + pub rest_port: u16, + pub p2p_port: u16, + pub storage_dir: PathBuf, + pub api_key: String, + pub tls_cert_path: PathBuf, + pub node_id: String, + pub exchange_name: String, + client: LdkServerClient, + // Kept alive so the electrs process doesn't get dropped + _electrs: Option, +} + +impl LdkServerHandle { + /// Starts a new ldk-server instance against the given bitcoind. + /// Randomly picks between bitcoind RPC, electrum, and esplora as the chain source. + pub async fn start(bitcoind: &TestBitcoind) -> Self { + match rand::random::() % 3 { + 0 => Self::start_with_config(bitcoind, default_test_config).await, + 1 => { + let electrs = TestElectrs::new(bitcoind); + let url = electrs.electrum_url(); + let mut handle = Self::start_with_config(bitcoind, move |params| { + test_config_with_chain_source( + params, + &format!("[electrum]\nserver_url = \"{}\"\nonchain_wallet_sync_interval_secs = 10\nlightning_wallet_sync_interval_secs = 10", url), + ) + }) + .await; + handle._electrs = Some(electrs); + handle + }, + 2 => { + let electrs = TestElectrs::new(bitcoind); + let url = electrs.esplora_url(); + let mut handle = Self::start_with_config(bitcoind, move |params| { + test_config_with_chain_source( + params, + &format!("[esplora]\nserver_url = \"{}\"\nonchain_wallet_sync_interval_secs = 10\nlightning_wallet_sync_interval_secs = 10", url), + ) + }) + .await; + handle._electrs = Some(electrs); + handle + }, + _ => unreachable!(), + } + } - let server_binary = server_binary_path(); - let mut child = Command::new(&server_binary) - .arg(config_path.to_str().unwrap()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .unwrap_or_else(|e| { - panic!("Failed to start ldk-server binary at {:?}: {}", server_binary, e) - }); + /// Starts a new ldk-server instance with a custom config. + /// The `config_fn` receives dynamic test parameters and returns the full TOML config string. + pub async fn start_with_config( + bitcoind: &TestBitcoind, config_fn: impl FnOnce(&TestServerParams) -> String, + ) -> Self { + let (mut child, params) = spawn_server(bitcoind, config_fn); + let TestServerParams { rest_port, p2p_port, storage_dir, exchange_name, .. } = params; // Spawn threads to forward stdout and stderr for debugging let stdout = child.stdout.take().unwrap(); @@ -204,6 +280,7 @@ client_trusts_lsp = true node_id: String::new(), exchange_name, client, + _electrs: None, }; // Wait for server to be ready and get node info @@ -235,6 +312,48 @@ impl Drop for LdkServerHandle { } } +/// Prepare test server params and spawn the ldk-server process. +fn spawn_server( + bitcoind: &TestBitcoind, config_fn: impl FnOnce(&TestServerParams) -> String, +) -> (Child, TestServerParams) { + #[allow(deprecated)] + let storage_dir = tempfile::tempdir().unwrap().into_path(); + let rest_port = find_available_port(); + let p2p_port = find_available_port(); + + let (rpc_host, rpc_port_num, rpc_user, rpc_password) = bitcoind.rpc_details(); + let rpc_address = format!("{rpc_host}:{rpc_port_num}"); + + let exchange_name = format!("e2e_test_exchange_{rest_port}"); + + let params = TestServerParams { + rest_port, + p2p_port, + storage_dir, + rpc_address, + rpc_user, + rpc_password, + exchange_name, + }; + + let config_content = config_fn(¶ms); + + let config_path = params.storage_dir.join("config.toml"); + std::fs::write(&config_path, &config_content).unwrap(); + + let server_binary = server_binary_path(); + let child = Command::new(&server_binary) + .arg(config_path.to_str().unwrap()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap_or_else(|e| { + panic!("Failed to start ldk-server binary at {:?}: {}", server_binary, e) + }); + + (child, params) +} + /// Find an available TCP port by binding to port 0. pub fn find_available_port() -> u16 { let listener = TcpListener::bind("127.0.0.1:0").unwrap(); From d2f5f8b75dc906d6dbd6d225a4ae0eb93c209e04 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Sat, 14 Mar 2026 22:44:46 -0500 Subject: [PATCH 3/5] Replace wildcard listen addresses with loopback in CLI When the CLI reads a base URL from config, 0.0.0.0 or [::] aren't connectable. Rewrite them to 127.0.0.1 or [::1] so the client can actually reach the server. Co-Authored-By: Claude Opus 4.6 (1M context) --- ldk-server-cli/src/main.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ldk-server-cli/src/main.rs b/ldk-server-cli/src/main.rs index 8cfa087..ef449bd 100644 --- a/ldk-server-cli/src/main.rs +++ b/ldk-server-cli/src/main.rs @@ -463,13 +463,20 @@ async fn main() { }); // Get base URL from argument then from config file - let base_url = + let mut base_url = cli.base_url.or_else(|| config.as_ref().map(|c| c.node.rest_service_address.clone())) .unwrap_or_else(|| { eprintln!("Base URL not provided. Use --base-url or ensure config file exists at ~/.ldk-server/config.toml"); std::process::exit(1); }); + // Replace wildcard listen addresses with loopback addresses for connectivity + if base_url.contains("0.0.0.0") { + base_url = base_url.replacen("0.0.0.0", "127.0.0.1", 1); + } else if base_url.contains("[::]") { + base_url = base_url.replacen("[::]", "[::1]", 1); + } + // Get TLS cert path from argument, then from config tls.cert_path, then from storage dir, // then try default location. let tls_cert_path = cli.tls_cert.map(PathBuf::from).or_else(|| { From 4800390e210772c1e6268267712ea15be94e0526 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Mon, 16 Mar 2026 15:30:45 -0500 Subject: [PATCH 4/5] Make rabbitmq and LSPS2 config optional when features are enabled Allow building with features enabled but unconfigured so we can distribute single binaries and Docker images with all features compiled in, letting users opt-in via config at runtime. Co-Authored-By: Claude Opus 4.6 (1M context) --- ldk-server/src/io/events/event_publisher.rs | 2 - ldk-server/src/main.rs | 24 ++- ldk-server/src/util/config.rs | 191 ++++++-------------- 3 files changed, 68 insertions(+), 149 deletions(-) diff --git a/ldk-server/src/io/events/event_publisher.rs b/ldk-server/src/io/events/event_publisher.rs index 308bde2..60860f1 100644 --- a/ldk-server/src/io/events/event_publisher.rs +++ b/ldk-server/src/io/events/event_publisher.rs @@ -52,11 +52,9 @@ pub trait EventPublisher: Send + Sync { } /// A no-op implementation of the [`EventPublisher`] trait. -#[cfg(not(feature = "events-rabbitmq"))] pub(crate) struct NoopEventPublisher; #[async_trait] -#[cfg(not(feature = "events-rabbitmq"))] impl EventPublisher for NoopEventPublisher { /// Publishes an event to a no-op sink, effectively discarding it. /// diff --git a/ldk-server/src/main.rs b/ldk-server/src/main.rs index d3e6a5d..bfd8da0 100644 --- a/ldk-server/src/main.rs +++ b/ldk-server/src/main.rs @@ -213,9 +213,9 @@ fn main() { // LSPS2 support is highly experimental and for testing purposes only. #[cfg(feature = "experimental-lsps2-support")] - builder.set_liquidity_provider_lsps2( - config_file.lsps2_service_config.expect("Missing liquidity.lsps2_server config"), - ); + if let Some(lsps2_config) = config_file.lsps2_service_config { + builder.set_liquidity_provider_lsps2(lsps2_config); + } let runtime = match tokio::runtime::Builder::new_multi_thread().enable_all().build() { Ok(runtime) => Arc::new(runtime), @@ -255,15 +255,21 @@ fn main() { #[cfg(not(feature = "events-rabbitmq"))] let event_publisher: Arc = - Arc::new(crate::io::events::event_publisher::NoopEventPublisher); + Arc::new(io::events::event_publisher::NoopEventPublisher); #[cfg(feature = "events-rabbitmq")] let event_publisher: Arc = { - let rabbitmq_config = RabbitMqConfig { - connection_string: config_file.rabbitmq_connection_string, - exchange_name: config_file.rabbitmq_exchange_name, - }; - Arc::new(RabbitMqEventPublisher::new(rabbitmq_config)) + match (config_file.rabbitmq_connection_string, config_file.rabbitmq_exchange_name) { + (Some(connection_string), Some(exchange_name)) => { + let rabbitmq_config = RabbitMqConfig { connection_string, exchange_name }; + Arc::new(RabbitMqEventPublisher::new(rabbitmq_config)) + }, + (None, None) => Arc::new(io::events::event_publisher::NoopEventPublisher), + _ => { + error!("Invalid RabbitMQ connection string or exchange name"); + std::process::exit(-1); + }, + } }; info!("Starting up..."); diff --git a/ldk-server/src/util/config.rs b/ldk-server/src/util/config.rs index 10ce89f..9955018 100644 --- a/ldk-server/src/util/config.rs +++ b/ldk-server/src/util/config.rs @@ -47,8 +47,8 @@ pub struct Config { pub storage_dir_path: Option, pub chain_source: ChainSource, pub rgs_server_url: Option, - pub rabbitmq_connection_string: String, - pub rabbitmq_exchange_name: String, + pub rabbitmq_connection_string: Option, + pub rabbitmq_exchange_name: Option, pub lsps2_service_config: Option, pub log_level: LevelFilter, pub log_file_path: Option, @@ -162,8 +162,10 @@ impl ConfigBuilder { } if let Some(rabbitmq) = toml.rabbitmq { - self.rabbitmq_connection_string = Some(rabbitmq.connection_string); - self.rabbitmq_exchange_name = Some(rabbitmq.exchange_name); + self.rabbitmq_connection_string = + rabbitmq.connection_string.or(self.rabbitmq_connection_string.clone()); + self.rabbitmq_exchange_name = + rabbitmq.exchange_name.or(self.rabbitmq_exchange_name.clone()); } if let Some(liquidity) = toml.liquidity { @@ -336,45 +338,19 @@ impl ConfigBuilder { .transpose()? .unwrap_or(LevelFilter::Debug); - #[cfg(feature = "events-rabbitmq")] - let (rabbitmq_connection_string, rabbitmq_exchange_name) = { - let connection_string = self.rabbitmq_connection_string.ok_or_else(|| io::Error::new( - io::ErrorKind::InvalidInput, - "Both `rabbitmq.connection_string` and `rabbitmq.exchange_name` must be configured if enabling `events-rabbitmq` feature." - ))?; - let exchange_name = self.rabbitmq_exchange_name.ok_or_else(|| io::Error::new( - io::ErrorKind::InvalidInput, - "Both `rabbitmq.connection_string` and `rabbitmq.exchange_name` must be configured if enabling `events-rabbitmq` feature." - ))?; - - if connection_string.is_empty() || 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." - )); - } - - (connection_string, exchange_name) - }; - - #[cfg(not(feature = "events-rabbitmq"))] - let (rabbitmq_connection_string, rabbitmq_exchange_name) = (String::new(), String::new()); - - #[cfg(feature = "experimental-lsps2-support")] - let lsps2_service_config = { - let liquidity = self.lsps2.ok_or_else(|| io::Error::new( - io::ErrorKind::InvalidInput, - "`liquidity.lsps2_service` must be defined in config if enabling `experimental-lsps2-support` feature." - ))?; - let lsps2_service = liquidity.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." - ))?; - Some(lsps2_service.into()) - }; + let (rabbitmq_connection_string, rabbitmq_exchange_name) = + match (self.rabbitmq_connection_string, self.rabbitmq_exchange_name) { + (Some(conn), Some(exchange)) => (Some(conn), Some(exchange)), + (None, None) => (None, None), + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Both `rabbitmq.connection_string` and `rabbitmq.exchange_name` must be configured together.", + )); + }, + }; - #[cfg(not(feature = "experimental-lsps2-support"))] - let lsps2_service_config = None; + let lsps2_service_config = self.lsps2.and_then(|l| l.lsps2_service).map(|s| s.into()); let pathfinding_scores_source_url = self.pathfinding_scores_source_url; @@ -462,8 +438,8 @@ struct LogConfig { #[derive(Deserialize, Serialize)] struct RabbitmqConfig { - connection_string: String, - exchange_name: String, + connection_string: Option, + exchange_name: Option, } #[derive(Deserialize, Serialize)] @@ -770,13 +746,6 @@ mod tests { let alias = "LDK Server"; - #[cfg(feature = "events-rabbitmq")] - let (expected_rabbit_conn, expected_rabbit_exchange) = - ("rabbitmq_connection_string".to_string(), "rabbitmq_exchange_name".to_string()); - - #[cfg(not(feature = "events-rabbitmq"))] - let (expected_rabbit_conn, expected_rabbit_exchange) = (String::new(), String::new()); - let expected = Config { listening_addrs: Some(vec![SocketAddress::from_str("localhost:3001").unwrap()]), announcement_addrs: Some(vec![SocketAddress::from_str("54.3.7.81:3001").unwrap()]), @@ -796,8 +765,8 @@ mod tests { rpc_password: "bitcoind-testpassword".to_string(), }, rgs_server_url: Some("https://rapidsync.lightningdevkit.org/snapshot/v2/".to_string()), - rabbitmq_connection_string: expected_rabbit_conn, - rabbitmq_exchange_name: expected_rabbit_exchange, + rabbitmq_connection_string: Some("rabbitmq_connection_string".to_string()), + rabbitmq_exchange_name: Some("rabbitmq_exchange_name".to_string()), lsps2_service_config: Some(LSPS2ServiceConfig { require_token: None, advertise_service: false, @@ -825,7 +794,6 @@ mod tests { assert_eq!(config.rgs_server_url, expected.rgs_server_url); 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()); assert_eq!(config.log_level, expected.log_level); assert_eq!(config.log_file_path, expected.log_file_path); @@ -991,46 +959,6 @@ mod tests { assert_eq!(error.to_string(), "Must set a single chain source, multiple were configured"); } - #[test] - fn test_config_optional_values() { - let storage_path = std::env::temp_dir(); - let config_file_name = "test_only_required_config.toml"; - - let mut args_config = empty_args_config(); - args_config.config_file = - Some(storage_path.join(config_file_name).to_string_lossy().to_string()); - - // Test with optional values not specified in the config file - let toml_config = r#" - [node] - network = "regtest" - rest_service_address = "127.0.0.1:3002" - - [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 - client_trusts_lsp = true - "#; - - fs::write(storage_path.join(config_file_name), toml_config).unwrap(); - assert!(load_config(&args_config).is_ok()); - } - #[test] fn test_config_missing_fields_in_file() { let storage_path = std::env::temp_dir(); @@ -1053,22 +981,6 @@ mod tests { }; } - #[cfg(feature = "experimental-lsps2-support")] - { - validate_missing!( - "[liquidity.lsps2_service]", - "`liquidity.lsps2_service` must be defined in config if enabling `experimental-lsps2-support` feature." - ); - } - - #[cfg(feature = "events-rabbitmq")] - { - validate_missing!( - "[rabbitmq]", - "Both `rabbitmq.connection_string` and `rabbitmq.exchange_name` must be configured if enabling `events-rabbitmq` feature." - ); - } - 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")); @@ -1085,8 +997,6 @@ mod tests { } #[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(); @@ -1117,8 +1027,8 @@ mod tests { rpc_password: args_config.bitcoind_rpc_password.unwrap(), }, rgs_server_url: None, - rabbitmq_connection_string: String::new(), - rabbitmq_exchange_name: String::new(), + rabbitmq_connection_string: None, + rabbitmq_exchange_name: None, lsps2_service_config: None, log_level: LevelFilter::Trace, log_file_path: Some("/var/log/ldk-server.log".to_string()), @@ -1139,8 +1049,6 @@ mod tests { } #[test] - #[cfg(not(feature = "experimental-lsps2-support"))] - #[cfg(not(feature = "events-rabbitmq"))] fn test_config_missing_fields_in_args_config() { macro_rules! validate_missing { ($field:ident, $err_msg:expr) => { @@ -1171,13 +1079,6 @@ mod tests { args_config.config_file = Some(storage_path.join(config_file_name).to_string_lossy().to_string()); - #[cfg(feature = "events-rabbitmq")] - let (expected_rabbit_conn, expected_rabbit_exchange) = - ("rabbitmq_connection_string".to_string(), "rabbitmq_exchange_name".to_string()); - - #[cfg(not(feature = "events-rabbitmq"))] - let (expected_rabbit_conn, expected_rabbit_exchange) = (String::new(), String::new()); - let (host, port) = parse_host_port(args_config.bitcoind_rpc_address.clone().unwrap().as_str()).unwrap(); @@ -1210,8 +1111,8 @@ mod tests { rpc_password: args_config.bitcoind_rpc_password.unwrap(), }, rgs_server_url: Some("https://rapidsync.lightningdevkit.org/snapshot/v2/".to_string()), - rabbitmq_connection_string: expected_rabbit_conn, - rabbitmq_exchange_name: expected_rabbit_exchange, + rabbitmq_connection_string: Some("rabbitmq_connection_string".to_string()), + rabbitmq_exchange_name: Some("rabbitmq_exchange_name".to_string()), lsps2_service_config: Some(LSPS2ServiceConfig { require_token: None, advertise_service: false, @@ -1238,28 +1139,42 @@ mod tests { assert_eq!(config.rgs_server_url, expected.rgs_server_url); 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()); assert_eq!(config.pathfinding_scores_source_url, expected.pathfinding_scores_source_url); } #[test] - #[cfg(feature = "events-rabbitmq")] - fn test_error_if_rabbitmq_feature_without_valid_config_file() { - let args_config = empty_args_config(); - let result = load_config(&args_config); - assert!(result.is_err()); - let err = result.unwrap_err(); - assert_eq!(err.kind(), io::ErrorKind::InvalidInput); - } + fn test_error_if_partial_rabbitmq_config() { + let storage_path = std::env::temp_dir(); + let config_file_name = "test_error_if_partial_rabbitmq_config.toml"; - #[test] - #[cfg(feature = "experimental-lsps2-support")] - fn test_error_if_lsps2_feature_without_valid_config_file() { - let args_config = empty_args_config(); + let mut args_config = empty_args_config(); + args_config.config_file = + Some(storage_path.join(config_file_name).to_string_lossy().to_string()); + + // Only connection_string set, missing exchange_name + let toml_config = r#" + [node] + network = "regtest" + rest_service_address = "127.0.0.1:3002" + + [bitcoind] + rpc_address = "127.0.0.1:8332" + rpc_user = "bitcoind-testuser" + rpc_password = "bitcoind-testpassword" + + [rabbitmq] + connection_string = "amqp://localhost" + "#; + + 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(), + "Both `rabbitmq.connection_string` and `rabbitmq.exchange_name` must be configured together." + ); } } From 143c748d041b4d441ba78f40840bad8e1f34bc5e Mon Sep 17 00:00:00 2001 From: benthecarman Date: Tue, 3 Mar 2026 16:49:12 -0600 Subject: [PATCH 5/5] Add e2e config startup tests Test server startup with various config settings: optional fields, log levels, TLS hosts, LSPS2 variations, and bitcoind RPC with localhost hostname. Add negative tests for invalid configs ensuring proper error messages. Dedicated chain source tests verify each backend explicitly. Co-Authored-By: Claude Opus 4.6 (1M context) --- e2e-tests/src/lib.rs | 20 ++ e2e-tests/tests/config.rs | 508 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 528 insertions(+) create mode 100644 e2e-tests/tests/config.rs diff --git a/e2e-tests/src/lib.rs b/e2e-tests/src/lib.rs index c5103ea..a7bc763 100644 --- a/e2e-tests/src/lib.rs +++ b/e2e-tests/src/lib.rs @@ -354,6 +354,26 @@ fn spawn_server( (child, params) } +/// Start ldk-server with the given config and expect it to fail (exit non-zero). +/// Returns the stderr output for assertion in tests. +pub fn start_expect_failure( + bitcoind: &TestBitcoind, config_fn: impl FnOnce(&TestServerParams) -> String, +) -> String { + let (child, ..) = spawn_server(bitcoind, config_fn); + + let output = child + .wait_with_output() + .unwrap_or_else(|e| panic!("Failed to wait for ldk-server process: {}", e)); + + assert!( + !output.status.success(), + "Expected server to fail but it exited with status: {}", + output.status + ); + + String::from_utf8_lossy(&output.stderr).to_string() +} + /// Find an available TCP port by binding to port 0. pub fn find_available_port() -> u16 { let listener = TcpListener::bind("127.0.0.1:0").unwrap(); diff --git a/e2e-tests/tests/config.rs b/e2e-tests/tests/config.rs new file mode 100644 index 0000000..7086b19 --- /dev/null +++ b/e2e-tests/tests/config.rs @@ -0,0 +1,508 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use e2e_tests::{ + default_test_config, start_expect_failure, test_config_with_chain_source, LdkServerHandle, + TestBitcoind, TestElectrs, +}; +use ldk_server_protos::api::GetNodeInfoRequest; + +fn remove_config_line(config: &str, key: &str) -> String { + config.lines().filter(|line| !line.trim_start().starts_with(key)).collect::>().join("\n") +} + +fn replace_config_line(config: &str, key: &str, new_line: &str) -> String { + config + .lines() + .map(|line| if line.trim_start().starts_with(key) { new_line } else { line }) + .collect::>() + .join("\n") +} + +fn remove_config_section(config: &str, section_header: &str) -> String { + let mut result = Vec::new(); + let mut skipping = false; + for line in config.lines() { + let trimmed = line.trim(); + if trimmed == section_header { + skipping = true; + continue; + } + if skipping && trimmed.starts_with('[') { + skipping = false; + } + if !skipping { + result.push(line); + } + } + result.join("\n") +} + +#[tokio::test] +async fn test_config_no_alias() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + remove_config_line(&default_test_config(params), "alias =") + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_no_listening_addresses() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + let config = remove_config_line(&default_test_config(params), "listening_addresses ="); + // Alias requires listening addresses for announcement, so remove it too + remove_config_line(&config, "alias =") + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_multiple_listening_addresses() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + let extra_port = e2e_tests::find_available_port(); + replace_config_line( + &default_test_config(params), + "listening_addresses =", + &format!( + "listening_addresses = [\"127.0.0.1:{}\", \"127.0.0.1:{}\"]", + params.p2p_port, extra_port + ), + ) + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_with_announcement_addresses() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + let mut config = default_test_config(params); + // Insert announcement_addresses after alias line + config = config.replace( + "alias = \"e2e-test-node\"", + &format!( + "alias = \"e2e-test-node\"\nannouncement_addresses = [\"127.0.0.1:{}\"]", + params.p2p_port + ), + ); + config + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_log_level_trace() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + let mut config = default_test_config(params); + config.push_str("\n[log]\nlevel = \"Trace\"\n"); + config + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_log_level_error() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + let mut config = default_test_config(params); + config.push_str("\n[log]\nlevel = \"Error\"\n"); + config + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_log_level_warn() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + let mut config = default_test_config(params); + config.push_str("\n[log]\nlevel = \"Warn\"\n"); + config + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_with_log_file() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + let log_path = format!("{}/ldk-server.log", params.storage_dir.display()); + let mut config = default_test_config(params); + config.push_str(&format!("\n[log]\nlevel = \"Debug\"\nfile = \"{}\"\n", log_path)); + config + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_with_tls_hosts() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + let mut config = default_test_config(params); + config.push_str("\n[tls]\nhosts = [\"example.com\", \"ldk-server.local\"]\n"); + config + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_lsps2_advertise_service() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + replace_config_line( + &default_test_config(params), + "advertise_service =", + "advertise_service = true", + ) + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_lsps2_with_require_token() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + let mut config = default_test_config(params); + config = config.replace( + "client_trusts_lsp = true", + "client_trusts_lsp = true\nrequire_token = \"secret-token-123\"", + ); + config + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_lsps2_high_fees() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + let config = default_test_config(params); + let config = replace_config_line( + &config, + "channel_opening_fee_ppm =", + "channel_opening_fee_ppm = 50000", + ); + let config = replace_config_line( + &config, + "min_channel_opening_fee_msat =", + "min_channel_opening_fee_msat = 10000000", + ); + let config = replace_config_line( + &config, + "channel_over_provisioning_ppm =", + "channel_over_provisioning_ppm = 500000", + ); + config + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_lsps2_restrictive_limits() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + let config = default_test_config(params); + let config = replace_config_line( + &config, + "min_payment_size_msat =", + "min_payment_size_msat = 10000000", + ); + let config = replace_config_line( + &config, + "max_payment_size_msat =", + "max_payment_size_msat = 100000000", + ); + let config = + replace_config_line(&config, "min_channel_lifetime =", "min_channel_lifetime = 4320"); + let config = replace_config_line( + &config, + "max_client_to_self_delay =", + "max_client_to_self_delay = 256", + ); + config + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_lsps2_client_trusts_lsp_false() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + replace_config_line( + &default_test_config(params), + "client_trusts_lsp =", + "client_trusts_lsp = false", + ) + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[test] +fn test_config_fail_missing_network() { + let bitcoind = TestBitcoind::new(); + let stderr = start_expect_failure(&bitcoind, |params| { + remove_config_line(&default_test_config(params), "network =") + }); + assert!(stderr.contains("Missing `network`"), "Unexpected stderr: {stderr}"); +} + +#[test] +fn test_config_fail_missing_rest_service_address() { + let bitcoind = TestBitcoind::new(); + let stderr = start_expect_failure(&bitcoind, |params| { + remove_config_line(&default_test_config(params), "rest_service_address =") + }); + assert!(stderr.contains("Missing `rest_service_address`"), "Unexpected stderr: {stderr}"); +} + +#[test] +fn test_config_fail_missing_rpc_address() { + let bitcoind = TestBitcoind::new(); + let stderr = start_expect_failure(&bitcoind, |params| { + remove_config_line(&default_test_config(params), "rpc_address =") + }); + assert!(stderr.contains("Missing `bitcoind_rpc_address`"), "Unexpected stderr: {stderr}"); +} + +#[test] +fn test_config_fail_missing_rpc_user() { + let bitcoind = TestBitcoind::new(); + let stderr = start_expect_failure(&bitcoind, |params| { + remove_config_line(&default_test_config(params), "rpc_user =") + }); + assert!(stderr.contains("Missing `bitcoind_rpc_user`"), "Unexpected stderr: {stderr}"); +} + +#[test] +fn test_config_fail_missing_rpc_password() { + let bitcoind = TestBitcoind::new(); + let stderr = start_expect_failure(&bitcoind, |params| { + remove_config_line(&default_test_config(params), "rpc_password =") + }); + assert!(stderr.contains("Missing `bitcoind_rpc_password`"), "Unexpected stderr: {stderr}"); +} + +#[test] +fn test_config_fail_multiple_chain_sources() { + let bitcoind = TestBitcoind::new(); + let stderr = start_expect_failure(&bitcoind, |params| { + let mut config = default_test_config(params); + config.push_str("\n[esplora]\nserver_url = \"https://mempool.space/api\"\n"); + config + }); + assert!(stderr.contains("Must set a single chain source"), "Unexpected stderr: {stderr}"); +} + +#[test] +fn test_config_fail_invalid_rest_service_address() { + let bitcoind = TestBitcoind::new(); + let stderr = start_expect_failure(&bitcoind, |params| { + replace_config_line( + &default_test_config(params), + "rest_service_address =", + "rest_service_address = \"not-a-valid-address\"", + ) + }); + assert!(stderr.contains("Invalid configuration"), "Unexpected stderr: {stderr}"); +} + +#[test] +fn test_config_fail_invalid_listening_address() { + let bitcoind = TestBitcoind::new(); + let stderr = start_expect_failure(&bitcoind, |params| { + replace_config_line( + &default_test_config(params), + "listening_addresses =", + "listening_addresses = [\"definitely not an address\"]", + ) + }); + assert!(stderr.contains("Invalid listening addresses"), "Unexpected stderr: {stderr}"); +} + +#[test] +fn test_config_fail_alias_too_long() { + let bitcoind = TestBitcoind::new(); + let long_alias = "a".repeat(33); + let stderr = start_expect_failure(&bitcoind, |params| { + replace_config_line( + &default_test_config(params), + "alias =", + &format!("alias = \"{}\"", long_alias), + ) + }); + assert!(stderr.contains("alias") && stderr.contains("32 bytes"), "Unexpected stderr: {stderr}"); +} + +#[test] +fn test_config_fail_invalid_log_level() { + let bitcoind = TestBitcoind::new(); + let stderr = start_expect_failure(&bitcoind, |params| { + let mut config = default_test_config(params); + config.push_str("\n[log]\nlevel = \"NotALevel\"\n"); + config + }); + assert!( + stderr.contains("Invalid log level") || stderr.contains("Invalid configuration"), + "Unexpected stderr: {stderr}" + ); +} + +#[tokio::test] +async fn test_config_no_rabbitmq() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + remove_config_section(&default_test_config(params), "[rabbitmq]") + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_no_lsps2() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + remove_config_section(&default_test_config(params), "[liquidity.lsps2_service]") + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_no_rabbitmq_and_no_lsps2() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + let config = remove_config_section(&default_test_config(params), "[rabbitmq]"); + remove_config_section(&config, "[liquidity.lsps2_service]") + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[test] +fn test_config_fail_partial_rabbitmq() { + let bitcoind = TestBitcoind::new(); + let stderr = start_expect_failure(&bitcoind, |params| { + remove_config_line(&default_test_config(params), "exchange_name =") + }); + assert!( + stderr.contains("rabbitmq") && stderr.contains("configured together"), + "Unexpected stderr: {stderr}" + ); +} + +#[test] +fn test_config_fail_invalid_toml() { + let bitcoind = TestBitcoind::new(); + let stderr = + start_expect_failure(&bitcoind, |_params| "this is not valid [[ toml {{{{".to_string()); + assert!( + stderr.contains("invalid TOML") || stderr.contains("Invalid configuration"), + "Unexpected stderr: {stderr}" + ); +} + +#[test] +fn test_config_fail_alias_without_listening_addresses() { + let bitcoind = TestBitcoind::new(); + let stderr = start_expect_failure(&bitcoind, |params| { + remove_config_line(&default_test_config(params), "listening_addresses =") + }); + assert!( + stderr.contains("Listening addresses") || stderr.contains("listening addresses"), + "Unexpected stderr: {stderr}" + ); +} + +#[tokio::test] +async fn test_config_chain_source_bitcoind_localhost() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + // Use "localhost:port" instead of "127.0.0.1:port" to test hostname RPC support + let rpc_address = params.rpc_address.replace("127.0.0.1", "localhost"); + test_config_with_chain_source( + params, + &format!( + "[bitcoind]\nrpc_address = \"{}\"\nrpc_user = \"{}\"\nrpc_password = \"{}\"", + rpc_address, params.rpc_user, params.rpc_password + ), + ) + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_chain_source_esplora() { + let bitcoind = TestBitcoind::new(); + let electrs = TestElectrs::new(&bitcoind); + let esplora_url = electrs.esplora_url(); + + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + test_config_with_chain_source( + params, + &format!("[esplora]\nserver_url = \"{}\"", esplora_url), + ) + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_chain_source_electrum() { + let bitcoind = TestBitcoind::new(); + let electrs = TestElectrs::new(&bitcoind); + let electrum_url = electrs.electrum_url(); + + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + test_config_with_chain_source( + params, + &format!("[electrum]\nserver_url = \"{}\"", electrum_url), + ) + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +}