Skip to content

Commit 695a812

Browse files
benthecarmanclaude
andcommitted
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) <noreply@anthropic.com>
1 parent 7c6b112 commit 695a812

3 files changed

Lines changed: 206 additions & 44 deletions

File tree

e2e-tests/Cargo.lock

Lines changed: 45 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

e2e-tests/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@ lapin = { version = "2.4.0", features = ["rustls"], default-features = false }
1515
prost = { version = "0.11.6", default-features = false, features = ["std"] }
1616
futures-util = "0.3"
1717
ldk-node = { git = "https://github.com/lightningdevkit/ldk-node", rev = "d1bbf978c8b7abe87ae2e40793556c1fe4e7ea49" }
18+
electrsd = { version = "0.36", features = ["esplora_a33e97e1", "corepc-node_29_0"] }
19+
rand = "0.9"

e2e-tests/src/lib.rs

Lines changed: 159 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -84,35 +84,54 @@ impl TestBitcoind {
8484
}
8585
}
8686

87-
/// Handle to a running ldk-server child process.
88-
pub struct LdkServerHandle {
89-
child: Option<Child>,
87+
/// Wrapper around an electrsd process providing both Electrum and Esplora endpoints.
88+
pub struct TestElectrs {
89+
pub electrsd: electrsd::ElectrsD,
90+
}
91+
92+
impl TestElectrs {
93+
/// Start an electrs instance connected to the given bitcoind with Esplora HTTP enabled.
94+
pub fn new(bitcoind: &TestBitcoind) -> Self {
95+
let mut conf = electrsd::Conf::default();
96+
conf.http_enabled = true;
97+
let electrsd =
98+
electrsd::ElectrsD::with_conf(electrsd::exe_path().unwrap(), &bitcoind.bitcoind, &conf)
99+
.unwrap();
100+
Self { electrsd }
101+
}
102+
103+
pub fn electrum_url(&self) -> String {
104+
// electrsd binds to 0.0.0.0 but that's not a connectable address for clients
105+
self.electrsd.electrum_url.replace("0.0.0.0", "127.0.0.1")
106+
}
107+
108+
pub fn esplora_url(&self) -> String {
109+
let url = self.electrsd.esplora_url.as_ref().expect("esplora not enabled");
110+
// electrsd binds to 0.0.0.0 but that's not a connectable address for clients
111+
format!("http://{}", url.replace("0.0.0.0", "127.0.0.1"))
112+
}
113+
114+
/// Trigger electrs to sync with bitcoind.
115+
pub fn trigger(&self) {
116+
self.electrsd.trigger().unwrap();
117+
}
118+
}
119+
120+
/// Dynamic parameters available when building test configs.
121+
pub struct TestServerParams {
90122
pub rest_port: u16,
91123
pub p2p_port: u16,
92124
pub storage_dir: PathBuf,
93-
pub api_key: String,
94-
pub tls_cert_path: PathBuf,
95-
pub node_id: String,
125+
pub rpc_address: String,
126+
pub rpc_user: String,
127+
pub rpc_password: String,
96128
pub exchange_name: String,
97-
client: LdkServerClient,
98129
}
99130

100-
impl LdkServerHandle {
101-
/// Starts a new ldk-server instance against the given bitcoind.
102-
/// Waits until the server is ready to accept requests.
103-
pub async fn start(bitcoind: &TestBitcoind) -> Self {
104-
#[allow(deprecated)]
105-
let storage_dir = tempfile::tempdir().unwrap().into_path();
106-
let rest_port = find_available_port();
107-
let p2p_port = find_available_port();
108-
109-
let (rpc_host, rpc_port_num, rpc_user, rpc_password) = bitcoind.rpc_details();
110-
let rpc_address = format!("{rpc_host}:{rpc_port_num}");
111-
112-
let exchange_name = format!("e2e_test_exchange_{rest_port}");
113-
114-
let config_content = format!(
115-
r#"[node]
131+
/// Generate a test config TOML with a custom chain source section.
132+
pub fn test_config_with_chain_source(params: &TestServerParams, chain_source_toml: &str) -> String {
133+
format!(
134+
r#"[node]
116135
network = "regtest"
117136
listening_addresses = ["127.0.0.1:{p2p_port}"]
118137
rest_service_address = "127.0.0.1:{rest_port}"
@@ -121,10 +140,7 @@ alias = "e2e-test-node"
121140
[storage.disk]
122141
dir_path = "{storage_dir}"
123142
124-
[bitcoind]
125-
rpc_address = "{rpc_address}"
126-
rpc_user = "{rpc_user}"
127-
rpc_password = "{rpc_password}"
143+
{chain_source}
128144
129145
[rabbitmq]
130146
connection_string = "amqp://guest:guest@localhost:5672/%2f"
@@ -141,21 +157,81 @@ min_payment_size_msat = 0
141157
max_payment_size_msat = 1000000000
142158
client_trusts_lsp = true
143159
"#,
144-
storage_dir = storage_dir.display(),
145-
);
160+
p2p_port = params.p2p_port,
161+
rest_port = params.rest_port,
162+
storage_dir = params.storage_dir.display(),
163+
chain_source = chain_source_toml,
164+
exchange_name = params.exchange_name,
165+
)
166+
}
167+
168+
/// Generate the default test config TOML with bitcoind RPC chain source.
169+
pub fn default_test_config(params: &TestServerParams) -> String {
170+
let chain_source = format!(
171+
"[bitcoind]\nrpc_address = \"{}\"\nrpc_user = \"{}\"\nrpc_password = \"{}\"",
172+
params.rpc_address, params.rpc_user, params.rpc_password
173+
);
174+
test_config_with_chain_source(params, &chain_source)
175+
}
146176

147-
let config_path = storage_dir.join("config.toml");
148-
std::fs::write(&config_path, &config_content).unwrap();
177+
/// Handle to a running ldk-server child process.
178+
pub struct LdkServerHandle {
179+
child: Option<Child>,
180+
pub rest_port: u16,
181+
pub p2p_port: u16,
182+
pub storage_dir: PathBuf,
183+
pub api_key: String,
184+
pub tls_cert_path: PathBuf,
185+
pub node_id: String,
186+
pub exchange_name: String,
187+
client: LdkServerClient,
188+
// Kept alive so the electrs process doesn't get dropped
189+
_electrs: Option<TestElectrs>,
190+
}
191+
192+
impl LdkServerHandle {
193+
/// Starts a new ldk-server instance against the given bitcoind.
194+
/// Randomly picks between bitcoind RPC, electrum, and esplora as the chain source.
195+
pub async fn start(bitcoind: &TestBitcoind) -> Self {
196+
match rand::random::<u8>() % 3 {
197+
0 => Self::start_with_config(bitcoind, default_test_config).await,
198+
1 => {
199+
let electrs = TestElectrs::new(bitcoind);
200+
let url = electrs.electrum_url();
201+
let mut handle = Self::start_with_config(bitcoind, move |params| {
202+
test_config_with_chain_source(
203+
params,
204+
&format!("[electrum]\nserver_url = \"{}\"\nonchain_wallet_sync_interval_secs = 10\nlightning_wallet_sync_interval_secs = 10", url),
205+
)
206+
})
207+
.await;
208+
handle._electrs = Some(electrs);
209+
handle
210+
},
211+
2 => {
212+
let electrs = TestElectrs::new(bitcoind);
213+
let url = electrs.esplora_url();
214+
let mut handle = Self::start_with_config(bitcoind, move |params| {
215+
test_config_with_chain_source(
216+
params,
217+
&format!("[esplora]\nserver_url = \"{}\"\nonchain_wallet_sync_interval_secs = 10\nlightning_wallet_sync_interval_secs = 10", url),
218+
)
219+
})
220+
.await;
221+
handle._electrs = Some(electrs);
222+
handle
223+
},
224+
_ => unreachable!(),
225+
}
226+
}
149227

150-
let server_binary = server_binary_path();
151-
let mut child = Command::new(&server_binary)
152-
.arg(config_path.to_str().unwrap())
153-
.stdout(Stdio::piped())
154-
.stderr(Stdio::piped())
155-
.spawn()
156-
.unwrap_or_else(|e| {
157-
panic!("Failed to start ldk-server binary at {:?}: {}", server_binary, e)
158-
});
228+
/// Starts a new ldk-server instance with a custom config.
229+
/// The `config_fn` receives dynamic test parameters and returns the full TOML config string.
230+
pub async fn start_with_config(
231+
bitcoind: &TestBitcoind, config_fn: impl FnOnce(&TestServerParams) -> String,
232+
) -> Self {
233+
let (mut child, params) = spawn_server(bitcoind, config_fn);
234+
let TestServerParams { rest_port, p2p_port, storage_dir, exchange_name, .. } = params;
159235

160236
// Spawn threads to forward stdout and stderr for debugging
161237
let stdout = child.stdout.take().unwrap();
@@ -204,6 +280,7 @@ client_trusts_lsp = true
204280
node_id: String::new(),
205281
exchange_name,
206282
client,
283+
_electrs: None,
207284
};
208285

209286
// Wait for server to be ready and get node info
@@ -235,6 +312,48 @@ impl Drop for LdkServerHandle {
235312
}
236313
}
237314

315+
/// Prepare test server params and spawn the ldk-server process.
316+
fn spawn_server(
317+
bitcoind: &TestBitcoind, config_fn: impl FnOnce(&TestServerParams) -> String,
318+
) -> (Child, TestServerParams) {
319+
#[allow(deprecated)]
320+
let storage_dir = tempfile::tempdir().unwrap().into_path();
321+
let rest_port = find_available_port();
322+
let p2p_port = find_available_port();
323+
324+
let (rpc_host, rpc_port_num, rpc_user, rpc_password) = bitcoind.rpc_details();
325+
let rpc_address = format!("{rpc_host}:{rpc_port_num}");
326+
327+
let exchange_name = format!("e2e_test_exchange_{rest_port}");
328+
329+
let params = TestServerParams {
330+
rest_port,
331+
p2p_port,
332+
storage_dir,
333+
rpc_address,
334+
rpc_user,
335+
rpc_password,
336+
exchange_name,
337+
};
338+
339+
let config_content = config_fn(&params);
340+
341+
let config_path = params.storage_dir.join("config.toml");
342+
std::fs::write(&config_path, &config_content).unwrap();
343+
344+
let server_binary = server_binary_path();
345+
let child = Command::new(&server_binary)
346+
.arg(config_path.to_str().unwrap())
347+
.stdout(Stdio::piped())
348+
.stderr(Stdio::piped())
349+
.spawn()
350+
.unwrap_or_else(|e| {
351+
panic!("Failed to start ldk-server binary at {:?}: {}", server_binary, e)
352+
});
353+
354+
(child, params)
355+
}
356+
238357
/// Find an available TCP port by binding to port 0.
239358
pub fn find_available_port() -> u16 {
240359
let listener = TcpListener::bind("127.0.0.1:0").unwrap();

0 commit comments

Comments
 (0)