@@ -16,11 +16,11 @@ use std::time::Duration;
1616use corepc_node:: Node ;
1717use hex_conservative:: DisplayHex ;
1818use ldk_server_client:: client:: LdkServerClient ;
19- use serde_json:: Value ;
2019use ldk_server_client:: ldk_server_grpc:: api:: { GetNodeInfoRequest , GetNodeInfoResponse } ;
2120use ldk_server_grpc:: api:: {
2221 GetBalancesRequest , ListChannelsRequest , OnchainReceiveRequest , OpenChannelRequest ,
2322} ;
23+ use serde_json:: Value ;
2424
2525/// Wrapper around a managed bitcoind process for regtest.
2626pub struct TestBitcoind {
@@ -103,42 +103,159 @@ pub struct LdkServerConfig {
103103 pub metrics_auth : Option < ( String , String ) > ,
104104}
105105
106- impl LdkServerHandle {
107- /// Starts a new ldk-server instance against the given bitcoind.
108- /// Waits until the server is ready to accept requests.
109- pub async fn start ( bitcoind : & TestBitcoind ) -> Self {
110- Self :: start_with_config ( bitcoind, LdkServerConfig :: default ( ) ) . await
106+ /// Dynamic parameters available when building test configs.
107+ pub struct TestServerParams {
108+ pub grpc_port : u16 ,
109+ pub p2p_port : u16 ,
110+ pub storage_dir : PathBuf ,
111+ pub rpc_address : String ,
112+ pub rpc_user : String ,
113+ pub rpc_password : String ,
114+ }
115+
116+ /// A chain source for the test config, mirroring the server's supported backends.
117+ pub enum ChainSource {
118+ Bitcoind { rpc_address : String , rpc_user : String , rpc_password : String } ,
119+ Electrum { server_url : String } ,
120+ Esplora { server_url : String } ,
121+ }
122+
123+ impl ChainSource {
124+ /// Render the chain source as its TOML section.
125+ fn to_toml ( & self ) -> String {
126+ match self {
127+ ChainSource :: Bitcoind { rpc_address, rpc_user, rpc_password } => format ! (
128+ "[bitcoind]\n rpc_address = \" {}\" \n rpc_user = \" {}\" \n rpc_password = \" {}\" " ,
129+ rpc_address, rpc_user, rpc_password
130+ ) ,
131+ ChainSource :: Electrum { server_url } => {
132+ format ! ( "[electrum]\n server_url = \" {}\" " , server_url)
133+ } ,
134+ ChainSource :: Esplora { server_url } => {
135+ format ! ( "[esplora]\n server_url = \" {}\" " , server_url)
136+ } ,
137+ }
138+ }
139+ }
140+
141+ /// Builder for the ldk-server config TOML used in tests.
142+ ///
143+ /// Tests tweak named, typed knobs and call [`TestConfigBuilder::build`] once to
144+ /// produce the TOML. This keeps tests from doing string surgery on rendered output.
145+ pub struct TestConfigBuilder {
146+ listening_addresses : Vec < String > ,
147+ announcement_addresses : Vec < String > ,
148+ grpc_service_address : String ,
149+ alias : Option < String > ,
150+ storage_dir : PathBuf ,
151+ chain_source : ChainSource ,
152+ metrics_auth : Option < ( String , String ) > ,
153+ log : Option < ( Option < String > , String ) > ,
154+ tls_hosts : Option < Vec < String > > ,
155+ }
156+
157+ impl TestConfigBuilder {
158+ /// Start from the default test config: a single localhost listening address, the
159+ /// `e2e-test-node` alias, and a bitcoind RPC chain source derived from `params`.
160+ pub fn new ( params : & TestServerParams ) -> Self {
161+ Self {
162+ listening_addresses : vec ! [ format!( "127.0.0.1:{}" , params. p2p_port) ] ,
163+ announcement_addresses : Vec :: new ( ) ,
164+ grpc_service_address : format ! ( "127.0.0.1:{}" , params. grpc_port) ,
165+ alias : Some ( "e2e-test-node" . to_string ( ) ) ,
166+ storage_dir : params. storage_dir . clone ( ) ,
167+ chain_source : ChainSource :: Bitcoind {
168+ rpc_address : params. rpc_address . clone ( ) ,
169+ rpc_user : params. rpc_user . clone ( ) ,
170+ rpc_password : params. rpc_password . clone ( ) ,
171+ } ,
172+ metrics_auth : None ,
173+ log : None ,
174+ tls_hosts : None ,
175+ }
176+ }
177+
178+ /// Set the node alias, or `None` to omit it entirely.
179+ pub fn alias ( mut self , alias : Option < & str > ) -> Self {
180+ self . alias = alias. map ( str:: to_string) ;
181+ self
182+ }
183+
184+ /// Set the listening addresses. An empty vec omits the key entirely.
185+ pub fn listening_addresses ( mut self , addresses : Vec < String > ) -> Self {
186+ self . listening_addresses = addresses;
187+ self
188+ }
189+
190+ /// Set the announcement addresses. An empty vec (the default) omits the key.
191+ pub fn announcement_addresses ( mut self , addresses : Vec < String > ) -> Self {
192+ self . announcement_addresses = addresses;
193+ self
194+ }
195+
196+ /// Replace the chain source backend.
197+ pub fn chain_source ( mut self , chain_source : ChainSource ) -> Self {
198+ self . chain_source = chain_source;
199+ self
200+ }
201+
202+ /// Add HTTP basic auth credentials to the `[metrics]` section.
203+ pub fn metrics_auth ( mut self , username : & str , password : & str ) -> Self {
204+ self . metrics_auth = Some ( ( username. to_string ( ) , password. to_string ( ) ) ) ;
205+ self
206+ }
207+
208+ /// Add a `[log]` section with the given file path and optional level.
209+ pub fn log ( mut self , level : Option < & str > , file : & str ) -> Self {
210+ self . log = Some ( ( level. map ( str:: to_string) , file. to_string ( ) ) ) ;
211+ self
111212 }
112213
113- pub async fn start_with_config ( bitcoind : & TestBitcoind , config : LdkServerConfig ) -> Self {
114- #[ allow( deprecated) ]
115- let storage_dir = tempfile:: tempdir ( ) . unwrap ( ) . into_path ( ) ;
116- let grpc_port = find_available_port ( ) ;
117- let p2p_port = find_available_port ( ) ;
214+ /// Add a `[tls]` section advertising the given hosts.
215+ pub fn tls_hosts ( mut self , hosts : Vec < String > ) -> Self {
216+ self . tls_hosts = Some ( hosts) ;
217+ self
218+ }
219+
220+ /// Build the config into a TOML string.
221+ pub fn build ( & self ) -> String {
222+ fn toml_string_array ( values : & [ String ] ) -> String {
223+ let quoted: Vec < String > = values. iter ( ) . map ( |v| format ! ( "\" {}\" " , v) ) . collect ( ) ;
224+ format ! ( "[{}]" , quoted. join( ", " ) )
225+ }
118226
119- let ( rpc_host, rpc_port_num, rpc_user, rpc_password) = bitcoind. rpc_details ( ) ;
120- let rpc_address = format ! ( "{rpc_host}:{rpc_port_num}" ) ;
227+ let mut node = vec ! [ "[node]" . to_string( ) , "network = \" regtest\" " . to_string( ) ] ;
228+ if !self . listening_addresses . is_empty ( ) {
229+ node. push ( format ! (
230+ "listening_addresses = {}" ,
231+ toml_string_array( & self . listening_addresses)
232+ ) ) ;
233+ }
234+ node. push ( format ! ( "grpc_service_address = \" {}\" " , self . grpc_service_address) ) ;
235+ if let Some ( alias) = & self . alias {
236+ node. push ( format ! ( "alias = \" {}\" " , alias) ) ;
237+ }
238+ if !self . announcement_addresses . is_empty ( ) {
239+ node. push ( format ! (
240+ "announcement_addresses = {}" ,
241+ toml_string_array( & self . announcement_addresses)
242+ ) ) ;
243+ }
121244
122- let metrics_auth_config = if let Some ( ( user, pass) ) = config. metrics_auth {
123- format ! ( "username = \" {}\" \n password = \" {}\" " , user, pass)
124- } else {
125- String :: new ( )
245+ let metrics_auth = match & self . metrics_auth {
246+ Some ( ( user, pass) ) => {
247+ format ! ( "\n username = \" {}\" \n password = \" {}\" " , user, pass)
248+ } ,
249+ None => String :: new ( ) ,
126250 } ;
127251
128- let config_content = format ! (
129- r#"[node]
130- network = "regtest"
131- listening_addresses = ["127.0.0.1:{p2p_port}"]
132- grpc_service_address = "127.0.0.1:{grpc_port}"
133- alias = "e2e-test-node"
252+ let mut config = format ! (
253+ r#"{node}
134254
135255[storage.disk]
136256dir_path = "{storage_dir}"
137257
138- [bitcoind]
139- rpc_address = "{rpc_address}"
140- rpc_user = "{rpc_user}"
141- rpc_password = "{rpc_password}"
258+ {chain_source}
142259
143260[liquidity.lsps2_service]
144261advertise_service = false
@@ -154,24 +271,53 @@ disable_client_reserve = false
154271
155272[metrics]
156273enabled = true
157- poll_metrics_interval = 1
158- {metrics_auth_config}
274+ poll_metrics_interval = 1{metrics_auth}
159275"# ,
160- storage_dir = storage_dir. display( ) ,
276+ node = node. join( "\n " ) ,
277+ storage_dir = self . storage_dir. display( ) ,
278+ chain_source = self . chain_source. to_toml( ) ,
279+ metrics_auth = metrics_auth,
161280 ) ;
162281
163- let config_path = storage_dir. join ( "config.toml" ) ;
164- std:: fs:: write ( & config_path, & config_content) . unwrap ( ) ;
282+ if let Some ( ( level, file) ) = & self . log {
283+ config. push_str ( "\n [log]\n " ) ;
284+ if let Some ( level) = level {
285+ config. push_str ( & format ! ( "level = \" {}\" \n " , level) ) ;
286+ }
287+ config. push_str ( & format ! ( "file = \" {}\" \n " , file) ) ;
288+ }
165289
166- let server_binary = server_binary_path ( ) ;
167- let mut child = Command :: new ( & server_binary)
168- . arg ( config_path. to_str ( ) . unwrap ( ) )
169- . stdout ( Stdio :: piped ( ) )
170- . stderr ( Stdio :: piped ( ) )
171- . spawn ( )
172- . unwrap_or_else ( |e| {
173- panic ! ( "Failed to start ldk-server binary at {:?}: {}" , server_binary, e)
174- } ) ;
290+ if let Some ( hosts) = & self . tls_hosts {
291+ config. push_str ( & format ! ( "\n [tls]\n hosts = {}\n " , toml_string_array( hosts) ) ) ;
292+ }
293+
294+ config
295+ }
296+ }
297+
298+ impl LdkServerHandle {
299+ /// Starts a new ldk-server instance against the given bitcoind.
300+ /// Waits until the server is ready to accept requests.
301+ pub async fn start ( bitcoind : & TestBitcoind ) -> Self {
302+ Self :: start_with_options ( bitcoind, LdkServerConfig :: default ( ) ) . await
303+ }
304+
305+ pub async fn start_with_options ( bitcoind : & TestBitcoind , config : LdkServerConfig ) -> Self {
306+ Self :: start_with_config ( bitcoind, |params| {
307+ let mut builder = TestConfigBuilder :: new ( params) ;
308+ if let Some ( ( user, pass) ) = & config. metrics_auth {
309+ builder = builder. metrics_auth ( user, pass) ;
310+ }
311+ builder. build ( )
312+ } )
313+ . await
314+ }
315+
316+ pub async fn start_with_config (
317+ config_bitcoind : & TestBitcoind , config : impl FnOnce ( & TestServerParams ) -> String ,
318+ ) -> Self {
319+ let ( mut child, params, config_path) = spawn_server ( config_bitcoind, config) ;
320+ let TestServerParams { grpc_port, p2p_port, storage_dir, .. } = params;
175321
176322 // Spawn threads to forward stdout and stderr for debugging
177323 let stdout = child. stdout . take ( ) . unwrap ( ) ;
@@ -251,6 +397,78 @@ impl Drop for LdkServerHandle {
251397 }
252398}
253399
400+ /// Prepare test server params and spawn the ldk-server process.
401+ fn spawn_server (
402+ bitcoind : & TestBitcoind , config_fn : impl FnOnce ( & TestServerParams ) -> String ,
403+ ) -> ( Child , TestServerParams , PathBuf ) {
404+ #[ allow( deprecated) ]
405+ let storage_dir = tempfile:: tempdir ( ) . unwrap ( ) . into_path ( ) ;
406+ let grpc_port = find_available_port ( ) ;
407+ let p2p_port = find_available_port ( ) ;
408+
409+ let ( rpc_host, rpc_port_num, rpc_user, rpc_password) = bitcoind. rpc_details ( ) ;
410+ let rpc_address = format ! ( "{rpc_host}:{rpc_port_num}" ) ;
411+
412+ let params =
413+ TestServerParams { grpc_port, p2p_port, storage_dir, rpc_address, rpc_user, rpc_password } ;
414+
415+ let config_content = config_fn ( & params) ;
416+
417+ let config_path = params. storage_dir . join ( "config.toml" ) ;
418+ std:: fs:: write ( & config_path, & config_content) . unwrap ( ) ;
419+
420+ let server_binary = server_binary_path ( ) ;
421+ let child = Command :: new ( & server_binary)
422+ . arg ( config_path. to_str ( ) . unwrap ( ) )
423+ . stdout ( Stdio :: piped ( ) )
424+ . stderr ( Stdio :: piped ( ) )
425+ . spawn ( )
426+ . unwrap_or_else ( |e| {
427+ panic ! ( "Failed to start ldk-server binary at {:?}: {}" , server_binary, e)
428+ } ) ;
429+
430+ ( child, params, config_path)
431+ }
432+
433+ /// Start ldk-server with the given config and expect it to fail (exit non-zero).
434+ /// Returns the stderr output for assertion in tests.
435+ pub fn start_expect_failure (
436+ bitcoind : & TestBitcoind , config_fn : impl FnOnce ( & TestServerParams ) -> String ,
437+ ) -> String {
438+ let ( mut child, ..) = spawn_server ( bitcoind, config_fn) ;
439+
440+ let timeout = Duration :: from_secs ( 30 ) ;
441+ let start = std:: time:: Instant :: now ( ) ;
442+ loop {
443+ match child. try_wait ( ) {
444+ Ok ( Some ( _) ) => break ,
445+ Ok ( None ) => {
446+ if start. elapsed ( ) > timeout {
447+ let _ = child. kill ( ) ;
448+ panic ! (
449+ "Server did not exit within {:?} — it may have started successfully \
450+ instead of failing",
451+ timeout
452+ ) ;
453+ }
454+ std:: thread:: sleep ( Duration :: from_millis ( 100 ) ) ;
455+ } ,
456+ Err ( e) => panic ! ( "Failed to wait for ldk-server process: {}" , e) ,
457+ }
458+ }
459+
460+ let output = child
461+ . wait_with_output ( )
462+ . unwrap_or_else ( |e| panic ! ( "Failed to read ldk-server output: {}" , e) ) ;
463+
464+ assert ! (
465+ !output. status. success( ) ,
466+ "Expected server to fail but it exited with status: {}" ,
467+ output. status
468+ ) ;
469+
470+ String :: from_utf8_lossy ( & output. stderr ) . to_string ( )
471+ }
254472/// Find an available TCP port by binding to port 0.
255473pub fn find_available_port ( ) -> u16 {
256474 let listener = TcpListener :: bind ( "127.0.0.1:0" ) . unwrap ( ) ;
0 commit comments