From f1efd91a73646a0bb68efb8d101bd4ce034cc6a4 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 3de2a408..ef0343fd 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 c950987c..86687999 100644 --- a/ldk-server/src/util/config.rs +++ b/ldk-server/src/util/config.rs @@ -76,9 +76,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`. @@ -93,6 +106,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, @@ -135,10 +150,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 { @@ -292,9 +319,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.")); }; @@ -428,11 +463,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)] @@ -905,7 +944,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 3b84dd9e50a0db21e6c0c86134db7800ab9155e4 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 13326fd6..d9bc8294 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 5576b7d2..27aaef10 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 083c08e7..2a0013e7 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 53fe7b07a2cb46e0931a065d5bb4f159078de650 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 4cdf30e4..69680153 100644 --- a/ldk-server-cli/src/main.rs +++ b/ldk-server-cli/src/main.rs @@ -544,13 +544,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 3fc6c7a0f2e2327506dd19494e67723fa65947ef 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 | 194 ++++++-------------- 3 files changed, 68 insertions(+), 152 deletions(-) diff --git a/ldk-server/src/io/events/event_publisher.rs b/ldk-server/src/io/events/event_publisher.rs index 308bde25..60860f12 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 ef0343fd..35f1e6e3 100644 --- a/ldk-server/src/main.rs +++ b/ldk-server/src/main.rs @@ -221,9 +221,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), @@ -263,15 +263,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 86687999..123c3608 100644 --- a/ldk-server/src/util/config.rs +++ b/ldk-server/src/util/config.rs @@ -48,12 +48,9 @@ pub struct Config { pub storage_dir_path: Option, pub chain_source: ChainSource, pub rgs_server_url: Option, - #[cfg_attr(not(feature = "events-rabbitmq"), allow(dead_code))] - pub rabbitmq_connection_string: String, - #[cfg_attr(not(feature = "events-rabbitmq"), allow(dead_code))] - pub rabbitmq_exchange_name: String, + pub rabbitmq_connection_string: Option, + pub rabbitmq_exchange_name: Option, pub lsps2_client_config: Option, - #[cfg_attr(not(feature = "experimental-lsps2-support"), allow(dead_code))] pub lsps2_service_config: Option, pub log_level: LevelFilter, pub log_file_path: Option, @@ -174,8 +171,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 { @@ -348,29 +347,17 @@ 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()); + 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.", + )); + }, + }; let lsps2_client_config = self .lsps2 @@ -379,21 +366,7 @@ impl ConfigBuilder { .map(LSPSClientConfig::try_from) .transpose()?; - #[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()) - }; - - #[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; @@ -482,8 +455,8 @@ struct LogConfig { #[derive(Deserialize, Serialize)] struct RabbitmqConfig { - connection_string: String, - exchange_name: String, + connection_string: Option, + exchange_name: Option, } #[derive(Deserialize, Serialize)] @@ -825,13 +798,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()]), @@ -851,8 +817,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_client_config: Some(LSPSClientConfig { node_id: PublicKey::from_str( "0217890e3aad8d35bc054f43acc00084b25229ecff0ab68debd82883ad65ee8266", @@ -889,7 +855,6 @@ mod tests { assert_eq!(config.rabbitmq_connection_string, expected.rabbitmq_connection_string); assert_eq!(config.rabbitmq_exchange_name, expected.rabbitmq_exchange_name); assert_eq!(config.lsps2_client_config, expected.lsps2_client_config); - #[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); @@ -1067,46 +1032,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(); @@ -1129,22 +1054,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")); @@ -1161,8 +1070,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(); @@ -1193,8 +1100,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_client_config: None, lsps2_service_config: None, log_level: LevelFilter::Trace, @@ -1216,8 +1123,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) => { @@ -1248,13 +1153,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(); @@ -1287,8 +1185,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_client_config: Some(LSPSClientConfig { node_id: PublicKey::from_str( "0217890e3aad8d35bc054f43acc00084b25229ecff0ab68debd82883ad65ee8266", @@ -1324,28 +1222,42 @@ mod tests { assert_eq!(config.rabbitmq_connection_string, expected.rabbitmq_connection_string); assert_eq!(config.rabbitmq_exchange_name, expected.rabbitmq_exchange_name); assert_eq!(config.lsps2_client_config, expected.lsps2_client_config); - #[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 0d33256d431946f8fb40b175aa3b47d94d64ac09 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 2a0013e7..0f067e85 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 00000000..7086b195 --- /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()); +}