1mod bitcoin;
2pub mod commands;
3pub mod config;
4#[cfg(all(unix, feature = "daemon"))]
5mod daemonize;
6mod database;
7pub mod descriptors;
8#[cfg(feature = "daemon")]
9mod jsonrpc;
10pub mod random;
11pub mod signer;
12pub mod spend;
13#[cfg(test)]
14mod testutils;
15
16pub use bip39;
17pub use miniscript;
18
19pub use crate::bitcoin::d::{BitcoinD, BitcoindError, WalletError};
20#[cfg(feature = "daemon")]
21use crate::jsonrpc::server::{rpcserver_loop, rpcserver_setup};
22use crate::{
23 bitcoin::{poller, BitcoinInterface},
24 config::Config,
25 database::{
26 sqlite::{FreshDbOptions, SqliteDb, SqliteDbError},
27 DatabaseInterface,
28 },
29};
30
31use std::{
32 error, fmt, fs, io, path,
33 sync::{self, mpsc},
34 thread,
35};
36
37use miniscript::bitcoin::secp256k1;
38
39#[cfg(not(test))]
40use std::panic;
41#[cfg(not(test))]
43fn setup_panic_hook() {
44 panic::set_hook(Box::new(move |panic_info| {
45 let file = panic_info
46 .location()
47 .map(|l| l.file())
48 .unwrap_or_else(|| "'unknown'");
49 let line = panic_info
50 .location()
51 .map(|l| l.line().to_string())
52 .unwrap_or_else(|| "'unknown'".to_string());
53
54 let bt = backtrace::Backtrace::new();
55 let info = panic_info
56 .payload()
57 .downcast_ref::<&str>()
58 .map(|s| s.to_string())
59 .or_else(|| panic_info.payload().downcast_ref::<String>().cloned());
60 log::error!(
61 "panic occurred at line {} of file {}: {:?}\n{:?}",
62 line,
63 file,
64 info,
65 bt
66 );
67 }));
68}
69
70#[derive(Debug, Clone)]
71pub struct Version {
72 pub major: u32,
73 pub minor: u32,
74 pub patch: u32,
75}
76
77impl fmt::Display for Version {
78 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
79 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
80 }
81}
82
83pub const VERSION: Version = Version {
84 major: 5,
85 minor: 0,
86 patch: 0,
87};
88
89#[derive(Debug)]
90pub enum StartupError {
91 Io(io::Error),
92 DefaultDataDirNotFound,
93 DatadirCreation(path::PathBuf, io::Error),
94 MissingBitcoindConfig,
95 Database(SqliteDbError),
96 Bitcoind(BitcoindError),
97 #[cfg(unix)]
98 Daemonization(&'static str),
99 #[cfg(windows)]
100 NoWatchonlyInDatadir,
101}
102
103impl fmt::Display for StartupError {
104 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
105 match self {
106 Self::Io(e) => write!(f, "{}", e),
107 Self::DefaultDataDirNotFound => write!(
108 f,
109 "Not data directory was specified and a default path could not be determined for this platform."
110 ),
111 Self::DatadirCreation(dir_path, e) => write!(
112 f,
113 "Could not create data directory at '{}': '{}'", dir_path.display(), e
114 ),
115 Self::MissingBitcoindConfig => write!(
116 f,
117 "Our Bitcoin interface is bitcoind but we have no 'bitcoind_config' entry in the configuration."
118 ),
119 Self::Database(e) => write!(f, "Error initializing database: '{}'.", e),
120 Self::Bitcoind(e) => write!(f, "Error setting up bitcoind interface: '{}'.", e),
121 #[cfg(unix)]
122 Self::Daemonization(e) => write!(f, "Error when daemonizing: '{}'.", e),
123 #[cfg(windows)]
124 Self::NoWatchonlyInDatadir => {
125 write!(
126 f,
127 "A data directory exists with no watchonly wallet. Really old versions of Liana used to not \
128 store the bitcoind watchonly wallet under their own datadir on Windows. A migration will be \
129 necessary to be able to use such an old datadir with recent versions of Liana. The migration \
130 is automatically performed by Liana version 4 and older. If you want to salvage this datadir \
131 first run Liana v4 before running more recent Liana versions."
132 )
133 }
134 }
135 }
136}
137
138impl error::Error for StartupError {}
139
140impl From<io::Error> for StartupError {
141 fn from(e: io::Error) -> Self {
142 Self::Io(e)
143 }
144}
145
146impl From<SqliteDbError> for StartupError {
147 fn from(e: SqliteDbError) -> Self {
148 Self::Database(e)
149 }
150}
151
152impl From<BitcoindError> for StartupError {
153 fn from(e: BitcoindError) -> Self {
154 Self::Bitcoind(e)
155 }
156}
157
158fn create_datadir(datadir_path: &path::Path) -> Result<(), StartupError> {
159 #[cfg(unix)]
160 return {
161 use fs::DirBuilder;
162 use std::os::unix::fs::DirBuilderExt;
163
164 let mut builder = DirBuilder::new();
165 builder
166 .mode(0o700)
167 .recursive(true)
168 .create(datadir_path)
169 .map_err(|e| StartupError::DatadirCreation(datadir_path.to_path_buf(), e))
170 };
171
172 #[cfg(not(unix))]
174 return {
175 fs::create_dir_all(datadir_path)
176 .map_err(|e| StartupError::DatadirCreation(datadir_path.to_path_buf(), e))
177 };
178}
179
180fn setup_sqlite(
183 config: &Config,
184 data_dir: &path::Path,
185 fresh_data_dir: bool,
186 secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
187) -> Result<SqliteDb, StartupError> {
188 let db_path: path::PathBuf = [data_dir, path::Path::new("lianad.sqlite3")]
189 .iter()
190 .collect();
191 let options = if fresh_data_dir {
192 Some(FreshDbOptions::new(
193 config.bitcoin_config.network,
194 config.main_descriptor.clone(),
195 ))
196 } else {
197 None
198 };
199 let sqlite = SqliteDb::new(db_path, options, secp)?;
200 sqlite.sanity_check(config.bitcoin_config.network, &config.main_descriptor)?;
201 log::info!("Database initialized and checked.");
202
203 Ok(sqlite)
204}
205
206fn setup_bitcoind(
209 config: &Config,
210 data_dir: &path::Path,
211 fresh_data_dir: bool,
212) -> Result<BitcoinD, StartupError> {
213 let wo_path: path::PathBuf = [data_dir, path::Path::new("lianad_watchonly_wallet")]
214 .iter()
215 .collect();
216 let wo_path_str = wo_path.to_str().expect("Must be valid unicode").to_string();
217 #[cfg(target_os = "windows")]
227 let wo_path_str = wo_path_str.replace("\\\\?\\", "").replace("\\\\?", "");
228
229 let bitcoind_config = config
230 .bitcoind_config
231 .as_ref()
232 .ok_or(StartupError::MissingBitcoindConfig)?;
233 let bitcoind = BitcoinD::new(bitcoind_config, wo_path_str)?;
234 bitcoind.node_sanity_checks(
235 config.bitcoin_config.network,
236 config.main_descriptor.is_taproot(),
237 )?;
238 if fresh_data_dir {
239 log::info!("Creating a new watchonly wallet on bitcoind.");
240 bitcoind.create_watchonly_wallet(&config.main_descriptor)?;
241 log::info!("Watchonly wallet created.");
242 } else {
243 #[cfg(windows)]
244 if !cfg!(test) && !wo_path.exists() {
245 return Err(StartupError::NoWatchonlyInDatadir);
246 }
247 }
248 log::info!("Loading our watchonly wallet on bitcoind.");
249 bitcoind.maybe_load_watchonly_wallet()?;
250 bitcoind.wallet_sanity_checks(&config.main_descriptor)?;
251 log::info!("Watchonly wallet loaded on bitcoind and sanity checked.");
252
253 Ok(bitcoind)
254}
255
256#[derive(Clone)]
257pub struct DaemonControl {
258 config: Config,
259 bitcoin: sync::Arc<sync::Mutex<dyn BitcoinInterface>>,
260 poller_sender: mpsc::SyncSender<poller::PollerMessage>,
261 db: sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
263 secp: secp256k1::Secp256k1<secp256k1::VerifyOnly>,
264}
265
266impl DaemonControl {
267 pub(crate) fn new(
268 config: Config,
269 bitcoin: sync::Arc<sync::Mutex<dyn BitcoinInterface>>,
270 poller_sender: mpsc::SyncSender<poller::PollerMessage>,
271 db: sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
272 secp: secp256k1::Secp256k1<secp256k1::VerifyOnly>,
273 ) -> DaemonControl {
274 DaemonControl {
275 config,
276 bitcoin,
277 poller_sender,
278 db,
279 secp,
280 }
281 }
282
283 #[cfg(test)]
285 pub fn db(&self) -> sync::Arc<sync::Mutex<dyn DatabaseInterface>> {
286 self.db.clone()
287 }
288}
289
290pub enum DaemonHandle {
293 Controller {
294 poller_sender: mpsc::SyncSender<poller::PollerMessage>,
295 poller_handle: thread::JoinHandle<()>,
296 control: DaemonControl,
297 },
298 #[cfg(feature = "daemon")]
299 Server {
300 poller_sender: mpsc::SyncSender<poller::PollerMessage>,
301 poller_handle: thread::JoinHandle<()>,
302 rpcserver_shutdown: sync::Arc<sync::atomic::AtomicBool>,
303 rpcserver_handle: thread::JoinHandle<Result<(), io::Error>>,
304 },
305}
306
307impl DaemonHandle {
308 pub fn start(
319 config: Config,
320 bitcoin: Option<impl BitcoinInterface + 'static>,
321 db: Option<impl DatabaseInterface + 'static>,
322 #[cfg(feature = "daemon")] with_rpc_server: bool,
323 ) -> Result<Self, StartupError> {
324 #[cfg(not(test))]
325 setup_panic_hook();
326
327 let secp = secp256k1::Secp256k1::verification_only();
328
329 let mut data_dir = config
331 .data_dir()
332 .ok_or(StartupError::DefaultDataDirNotFound)?;
333 data_dir.push(config.bitcoin_config.network.to_string());
334 let fresh_data_dir = !data_dir.as_path().exists();
335 if fresh_data_dir {
336 create_datadir(&data_dir)?;
337 log::info!("Created a new data directory at '{}'", data_dir.display());
338 }
339
340 let db = match db {
342 Some(db) => sync::Arc::from(sync::Mutex::from(db)),
343 None => sync::Arc::from(sync::Mutex::from(setup_sqlite(
344 &config,
345 &data_dir,
346 fresh_data_dir,
347 &secp,
348 )?)) as sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
349 };
350
351 let bit = match bitcoin {
353 Some(bit) => sync::Arc::from(sync::Mutex::from(bit)),
354 None => sync::Arc::from(sync::Mutex::from(setup_bitcoind(
355 &config,
356 &data_dir,
357 fresh_data_dir,
358 )?)) as sync::Arc<sync::Mutex<dyn BitcoinInterface>>,
359 };
360
361 #[cfg(all(unix, feature = "daemon"))]
365 if config.daemon {
366 log::info!("Daemonizing");
367 let log_file = data_dir.as_path().join("log");
368 let pid_file = data_dir.as_path().join("lianad.pid");
369 unsafe {
370 daemonize::daemonize(&data_dir, &log_file, &pid_file)
371 .map_err(StartupError::Daemonization)?;
372 }
373 }
374
375 let bitcoin_poller =
378 poller::Poller::new(bit.clone(), db.clone(), config.main_descriptor.clone());
379 let (poller_sender, poller_receiver) = mpsc::sync_channel(0);
380 let poller_handle = thread::Builder::new()
381 .name("Bitcoin Network poller".to_string())
382 .spawn({
383 let poll_interval = config.bitcoin_config.poll_interval_secs;
384 move || {
385 log::info!("Bitcoin poller started.");
386 bitcoin_poller.poll_forever(poll_interval, poller_receiver);
387 log::info!("Bitcoin poller stopped.");
388 }
389 })
390 .expect("Spawning the poller thread must never fail.");
391
392 let control = DaemonControl::new(config, bit, poller_sender.clone(), db, secp);
395
396 #[cfg(feature = "daemon")]
397 if with_rpc_server {
398 let rpcserver_shutdown = sync::Arc::from(sync::atomic::AtomicBool::from(false));
399 let rpcserver_handle = thread::Builder::new()
400 .name("Bitcoin Network poller".to_string())
401 .spawn({
402 let shutdown = rpcserver_shutdown.clone();
403 move || {
404 let mut rpc_socket = data_dir;
405 rpc_socket.push("lianad_rpc");
406 let listener = rpcserver_setup(&rpc_socket)?;
407 log::info!("JSONRPC server started.");
408
409 rpcserver_loop(listener, control, shutdown)?;
410 log::info!("JSONRPC server stopped.");
411 Ok(())
412 }
413 })
414 .expect("Spawning the RPC server thread should never fail.");
415
416 return Ok(DaemonHandle::Server {
417 poller_sender,
418 poller_handle,
419 rpcserver_shutdown,
420 rpcserver_handle,
421 });
422 }
423
424 Ok(DaemonHandle::Controller {
425 poller_sender,
426 poller_handle,
427 control,
428 })
429 }
430
431 pub fn start_default(
434 config: Config,
435 #[cfg(feature = "daemon")] with_rpc_server: bool,
436 ) -> Result<DaemonHandle, StartupError> {
437 Self::start(
438 config,
439 Option::<BitcoinD>::None,
440 Option::<SqliteDb>::None,
441 #[cfg(feature = "daemon")]
442 with_rpc_server,
443 )
444 }
445
446 pub fn is_alive(&self) -> bool {
450 match self {
451 Self::Controller {
452 ref poller_handle, ..
453 } => !poller_handle.is_finished(),
454 #[cfg(feature = "daemon")]
455 Self::Server {
456 ref poller_handle,
457 ref rpcserver_handle,
458 ..
459 } => !poller_handle.is_finished() && !rpcserver_handle.is_finished(),
460 }
461 }
462
463 pub fn stop(self) -> Result<(), Box<dyn error::Error>> {
465 match self {
466 Self::Controller {
467 poller_sender,
468 poller_handle,
469 ..
470 } => {
471 poller_sender
472 .send(poller::PollerMessage::Shutdown)
473 .expect("The other end should never have hung up before this.");
474 poller_handle.join().expect("Poller thread must not panic");
475 Ok(())
476 }
477 #[cfg(feature = "daemon")]
478 Self::Server {
479 poller_sender,
480 poller_handle,
481 rpcserver_shutdown,
482 rpcserver_handle,
483 } => {
484 poller_sender
485 .send(poller::PollerMessage::Shutdown)
486 .expect("The other end should never have hung up before this.");
487 rpcserver_shutdown.store(true, sync::atomic::Ordering::Relaxed);
488 rpcserver_handle
489 .join()
490 .expect("Poller thread must not panic")?;
491 poller_handle.join().expect("Poller thread must not panic");
492 Ok(())
493 }
494 }
495 }
496}
497
498#[cfg(all(test, unix))]
499mod tests {
500 use super::*;
501 use crate::{
502 config::{BitcoinConfig, BitcoindConfig, BitcoindRpcAuth},
503 descriptors::LianaDescriptor,
504 testutils::*,
505 };
506
507 use miniscript::bitcoin;
508 use std::{
509 fs,
510 io::{BufRead, BufReader, Write},
511 net, path,
512 str::FromStr,
513 thread, time,
514 };
515
516 fn read_til_json_end(stream: &mut net::TcpStream) {
518 stream
519 .set_read_timeout(Some(time::Duration::from_secs(5)))
520 .unwrap();
521 let mut reader = BufReader::new(stream);
522 loop {
523 let mut line = String::new();
524 reader.read_line(&mut line).unwrap();
525
526 if line.starts_with("Authorization") {
527 let mut buf = vec![0; 256];
528 reader.read_until(b'}', &mut buf).unwrap();
529 return;
530 }
531 }
532 }
533
534 fn complete_sanity_check(server: &net::TcpListener) {
536 let echo_resp =
537 "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[]}\n".as_bytes();
538
539 {
541 let (mut stream, _) = server.accept().unwrap();
542 read_til_json_end(&mut stream);
543 stream.write_all(echo_resp).unwrap();
544 stream.flush().unwrap();
545 }
546
547 let (mut stream, _) = server.accept().unwrap();
549 read_til_json_end(&mut stream);
550 stream.write_all(echo_resp).unwrap();
551 stream.flush().unwrap();
552 }
553
554 fn complete_version_check(server: &net::TcpListener) {
556 let net_resp =
557 "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"version\":240000}}\n"
558 .as_bytes();
559 let (mut stream, _) = server.accept().unwrap();
560 read_til_json_end(&mut stream);
561 stream.write_all(net_resp).unwrap();
562 stream.flush().unwrap();
563 }
564
565 fn complete_network_check(server: &net::TcpListener) {
567 let net_resp =
568 "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"chain\":\"main\"}}\n"
569 .as_bytes();
570 let (mut stream, _) = server.accept().unwrap();
571 read_til_json_end(&mut stream);
572 stream.write_all(net_resp).unwrap();
573 stream.flush().unwrap();
574 }
575
576 fn complete_wallet_creation(server: &net::TcpListener) {
578 {
579 let net_resp =
580 ["HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[]}\n".as_bytes()]
581 .concat();
582 let (mut stream, _) = server.accept().unwrap();
583 read_til_json_end(&mut stream);
584 stream.write_all(&net_resp).unwrap();
585 stream.flush().unwrap();
586 }
587
588 {
589 let net_resp = [
590 "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"name\":\"dummy\"}}\n"
591 .as_bytes(),
592 ]
593 .concat();
594 let (mut stream, _) = server.accept().unwrap();
595 read_til_json_end(&mut stream);
596 stream.write_all(&net_resp).unwrap();
597 stream.flush().unwrap();
598 }
599
600 let net_resp = [
601 "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[{\"success\":true}]}\n"
602 .as_bytes(),
603 ]
604 .concat();
605 let (mut stream, _) = server.accept().unwrap();
606 read_til_json_end(&mut stream);
607 stream.write_all(&net_resp).unwrap();
608 stream.flush().unwrap();
609 }
610
611 fn complete_wallet_loading(server: &net::TcpListener) {
613 {
614 let listwallets_resp =
615 "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[]}\n".as_bytes();
616 let (mut stream, _) = server.accept().unwrap();
617 read_til_json_end(&mut stream);
618 stream.write_all(listwallets_resp).unwrap();
619 stream.flush().unwrap();
620 }
621
622 let loadwallet_resp =
623 "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"name\":\"dummy\"}}\n"
624 .as_bytes();
625 let (mut stream, _) = server.accept().unwrap();
626 read_til_json_end(&mut stream);
627 stream.write_all(loadwallet_resp).unwrap();
628 stream.flush().unwrap();
629 }
630
631 fn complete_wallet_check(server: &net::TcpListener, watchonly_wallet_path: &str) {
633 let net_resp = [
634 "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":[\"".as_bytes(),
635 watchonly_wallet_path.as_bytes(),
636 "\"]}\n".as_bytes(),
637 ]
638 .concat();
639 let (mut stream, _) = server.accept().unwrap();
640 read_til_json_end(&mut stream);
641 stream.write_all(&net_resp).unwrap();
642 stream.flush().unwrap();
643 }
644
645 fn complete_desc_check(server: &net::TcpListener, receive_desc: &str, change_desc: &str) {
647 let net_resp = [
648 "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"descriptors\":[{\"desc\":\"".as_bytes(),
649 receive_desc.as_bytes(),
650 "\",\"timestamp\":0},".as_bytes(),
651 "{\"desc\":\"".as_bytes(),
652 change_desc.as_bytes(),
653 "\",\"timestamp\":1}]}}\n".as_bytes(),
654 ]
655 .concat();
656 let (mut stream, _) = server.accept().unwrap();
657 read_til_json_end(&mut stream);
658 stream.write_all(&net_resp).unwrap();
659 stream.flush().unwrap();
660 }
661
662 fn complete_tip_init(server: &net::TcpListener) {
664 let net_resp = [
665 "HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f\"}\n".as_bytes(),
666 ]
667 .concat();
668 let (mut stream, _) = server.accept().unwrap();
669 read_til_json_end(&mut stream);
670 stream.write_all(&net_resp).unwrap();
671 stream.flush().unwrap();
672 }
673
674 #[test]
679 fn daemon_startup() {
680 let tmp_dir = tmp_dir();
681 fs::create_dir_all(&tmp_dir).unwrap();
682 let data_dir: path::PathBuf = [tmp_dir.as_path(), path::Path::new("datadir")]
683 .iter()
684 .collect();
685 let wo_path: path::PathBuf = [
686 data_dir.as_path(),
687 path::Path::new("bitcoin"),
688 path::Path::new("lianad_watchonly_wallet"),
689 ]
690 .iter()
691 .collect();
692 let wo_path = wo_path.to_str().unwrap().to_string();
693
694 let network = bitcoin::Network::Bitcoin;
696 let cookie: path::PathBuf = [
697 tmp_dir.as_path(),
698 path::Path::new(&format!(
699 "dummy_bitcoind_{:?}.cookie",
700 thread::current().id()
701 )),
702 ]
703 .iter()
704 .collect();
705 fs::write(&cookie, [0; 32]).unwrap(); let addr: net::SocketAddr =
707 net::SocketAddrV4::new(net::Ipv4Addr::new(127, 0, 0, 1), 0).into();
708 let server = net::TcpListener::bind(addr).unwrap();
709 let addr = server.local_addr().unwrap();
710 let bitcoin_config = BitcoinConfig {
711 network,
712 poll_interval_secs: time::Duration::from_secs(2),
713 };
714 let bitcoind_config = BitcoindConfig {
715 addr,
716 rpc_auth: BitcoindRpcAuth::CookieFile(cookie),
717 };
718
719 let desc_str = "wsh(andor(pk([aabbccdd]xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/<0;1>/*),older(10000),pk([aabbccdd]xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/<0;1>/*)))#3xh8xmhn";
721 let desc = LianaDescriptor::from_str(desc_str).unwrap();
722 let receive_desc = desc.receive_descriptor().clone();
723 let change_desc = desc.change_descriptor().clone();
724 let config = Config {
725 bitcoin_config,
726 bitcoind_config: Some(bitcoind_config),
727 data_dir: Some(data_dir),
728 #[cfg(unix)]
729 daemon: false,
730 log_level: log::LevelFilter::Debug,
731 main_descriptor: desc,
732 };
733
734 let t = thread::spawn({
736 let config = config.clone();
737 move || {
738 let handle = DaemonHandle::start_default(
739 config,
740 #[cfg(feature = "daemon")]
741 false,
742 )
743 .unwrap();
744 handle.stop().unwrap();
745 }
746 });
747 complete_sanity_check(&server);
748 complete_version_check(&server);
749 complete_network_check(&server);
750 complete_wallet_creation(&server);
751 complete_wallet_loading(&server);
752 complete_wallet_check(&server, &wo_path);
753 complete_desc_check(&server, &receive_desc.to_string(), &change_desc.to_string());
754 complete_tip_init(&server);
755 t.join().unwrap();
758
759 let t = thread::spawn({
761 let config = config.clone();
762 move || {
763 let handle = DaemonHandle::start_default(
764 config,
765 #[cfg(feature = "daemon")]
766 false,
767 )
768 .unwrap();
769 handle.stop().unwrap();
770 }
771 });
772 complete_sanity_check(&server);
773 complete_version_check(&server);
774 complete_network_check(&server);
775 complete_wallet_loading(&server);
776 complete_wallet_check(&server, &wo_path);
777 complete_desc_check(&server, &receive_desc.to_string(), &change_desc.to_string());
778 t.join().unwrap();
781
782 fs::remove_dir_all(&tmp_dir).unwrap();
783 }
784}