corepc_node/
lib.rs

1#![cfg_attr(docsrs, feature(doc_auto_cfg))]
2#![cfg_attr(docsrs, cfg_attr(all(), doc = include_str!("../README.md")))]
3
4pub extern crate corepc_client as client;
5
6#[rustfmt::skip]
7mod client_versions;
8mod versions;
9
10use std::ffi::OsStr;
11use std::net::{Ipv4Addr, SocketAddrV4, TcpListener};
12use std::path::PathBuf;
13use std::process::{Child, Command, ExitStatus, Stdio};
14use std::time::Duration;
15use std::{env, fmt, fs, thread};
16
17use anyhow::Context;
18use corepc_client::client_sync::{self, Auth};
19use log::{debug, error, warn};
20use tempfile::TempDir;
21pub use {anyhow, serde_json, tempfile, which};
22
23#[rustfmt::skip]                // Keep pubic re-exports separate.
24#[doc(inline)]
25pub use self::{
26    // Re-export `vtype` (the version specific types) and client defined types.
27    client_versions::*,
28    // Re-export the version string e.g., "28.0".
29    versions::VERSION,
30    // Re-export the model types as `mtype` to differentiate it from `vtype`.
31    client::types::model as mtype, // `types` is the `corepc-types` crate.
32};
33
34#[derive(Debug)]
35/// Struct representing the bitcoind process with related information
36pub struct Node {
37    /// Process child handle, used to terminate the process when this struct is dropped
38    process: Child,
39    /// Rpc client linked to this bitcoind process
40    pub client: Client,
41    /// Work directory, where the node store blocks and other stuff.
42    work_dir: DataDir,
43
44    /// Contains information to connect to this node
45    pub params: ConnectParams,
46}
47
48#[derive(Debug)]
49/// The DataDir struct defining the kind of data directory the node
50/// will contain. Data directory can be either persistent, or temporary.
51pub enum DataDir {
52    /// Persistent Data Directory
53    Persistent(PathBuf),
54    /// Temporary Data Directory
55    Temporary(TempDir),
56}
57
58impl DataDir {
59    /// Return the data directory path
60    fn path(&self) -> PathBuf {
61        match self {
62            Self::Persistent(path) => path.to_owned(),
63            Self::Temporary(tmp_dir) => tmp_dir.path().to_path_buf(),
64        }
65    }
66}
67
68#[derive(Debug, Clone)]
69/// Contains all the information to connect to this node
70pub struct ConnectParams {
71    /// Path to the node cookie file, useful for other client to connect to the node
72    pub cookie_file: PathBuf,
73    /// Url of the rpc of the node, useful for other client to connect to the node
74    pub rpc_socket: SocketAddrV4,
75    /// p2p connection url, is some if the node started with p2p enabled
76    pub p2p_socket: Option<SocketAddrV4>,
77    /// zmq pub raw block connection url
78    pub zmq_pub_raw_block_socket: Option<SocketAddrV4>,
79    /// zmq pub raw tx connection Url
80    pub zmq_pub_raw_tx_socket: Option<SocketAddrV4>,
81}
82
83pub struct CookieValues {
84    pub user: String,
85    pub password: String,
86}
87
88impl ConnectParams {
89    /// Parses the cookie file content
90    fn parse_cookie(content: String) -> Option<CookieValues> {
91        let values: Vec<_> = content.splitn(2, ':').collect();
92        let user = values.first()?.to_string();
93        let password = values.get(1)?.to_string();
94        Some(CookieValues { user, password })
95    }
96
97    /// Return the user and password values from cookie file
98    pub fn get_cookie_values(&self) -> Result<Option<CookieValues>, std::io::Error> {
99        let cookie = std::fs::read_to_string(&self.cookie_file)?;
100        Ok(self::ConnectParams::parse_cookie(cookie))
101    }
102}
103
104/// Enum to specify p2p settings
105#[derive(Debug, PartialEq, Eq, Clone)]
106pub enum P2P {
107    /// the node doesn't open a p2p port and work in standalone mode
108    No,
109    /// the node open a p2p port
110    Yes,
111    /// The node open a p2p port and also connects to the url given as parameter, it's handy to
112    /// initialize this with [Node::p2p_connect] of another node. The `bool` parameter indicates
113    /// if the node can accept connection too.
114    Connect(SocketAddrV4, bool),
115}
116
117/// All the possible error in this crate
118pub enum Error {
119    /// Wrapper of io Error
120    Io(std::io::Error),
121    /// Wrapper of bitcoincore_rpc Error
122    Rpc(client_sync::Error),
123    /// Returned when calling methods requiring a feature to be activated, but it's not
124    NoFeature,
125    /// Returned when calling methods requiring a env var to exist, but it's not
126    NoEnvVar,
127    /// Returned when calling methods requiring the bitcoind executable but none is found
128    /// (no feature, no `BITCOIND_EXE`, no `bitcoind` in `PATH` )
129    NoBitcoindExecutableFound,
130    /// Wrapper of early exit status
131    EarlyExit(ExitStatus),
132    /// Returned when both tmpdir and staticdir is specified in `Conf` options
133    BothDirsSpecified,
134    /// Returned when -rpcuser and/or -rpcpassword is used in `Conf` args
135    /// It will soon be deprecated, please use -rpcauth instead
136    RpcUserAndPasswordUsed,
137    /// Returned when expecting an auto-downloaded executable but `BITCOIND_SKIP_DOWNLOAD` env var is set
138    SkipDownload,
139    /// It appears that bitcoind is not reachable.
140    NoBitcoindInstance,
141}
142
143impl fmt::Debug for Error {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        use Error::*;
146
147        match self {
148            Io(_) => write!(f, "io::Error"), // FIXME: Use bitcoin-internals.
149            Rpc(_) => write!(f, "bitcoin_rpc::Error"),
150            NoFeature => write!(f, "Called a method requiring a feature to be set, but it's not"),
151            NoEnvVar => write!(f, "Called a method requiring env var `BITCOIND_EXE` to be set, but it's not"),
152            NoBitcoindExecutableFound =>  write!(f, "`bitcoind` executable is required, provide it with one of the following: set env var `BITCOIND_EXE` or use a feature like \"22_1\" or have `bitcoind` executable in the `PATH`"),
153            EarlyExit(e) => write!(f, "The bitcoind process terminated early with exit code {}", e),
154            BothDirsSpecified => write!(f, "tempdir and staticdir cannot be enabled at same time in configuration options"),
155            RpcUserAndPasswordUsed => write!(f, "`-rpcuser` and `-rpcpassword` cannot be used, it will be deprecated soon and it's recommended to use `-rpcauth` instead which works alongside with the default cookie authentication"),
156            SkipDownload => write!(f, "expecting an auto-downloaded executable but `BITCOIND_SKIP_DOWNLOAD` env var is set"),
157            NoBitcoindInstance => write!(f, "it appears that bitcoind is not reachable"),
158        }
159    }
160}
161
162impl std::fmt::Display for Error {
163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self) }
164}
165
166impl std::error::Error for Error {
167    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
168        use Error::*;
169
170        match *self {
171            Error::Io(ref e) => Some(e),
172            Error::Rpc(ref e) => Some(e),
173            NoFeature
174            | NoEnvVar
175            | NoBitcoindExecutableFound
176            | EarlyExit(_)
177            | BothDirsSpecified
178            | RpcUserAndPasswordUsed
179            | SkipDownload
180            | NoBitcoindInstance => None,
181        }
182    }
183}
184
185const LOCAL_IP: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1);
186
187const INVALID_ARGS: [&str; 2] = ["-rpcuser", "-rpcpassword"];
188
189/// The node configuration parameters, implements a convenient [Default] for most common use.
190///
191/// `#[non_exhaustive]` allows adding new parameters without breaking downstream users.
192/// Users cannot instantiate the struct directly, they need to create it via the `default()` method
193/// and mutate fields according to their preference.
194///
195/// Default values:
196/// ```
197/// use corepc_node as bitcoind;
198/// let mut conf = bitcoind::Conf::default();
199/// conf.args = vec!["-regtest", "-fallbackfee=0.0001"];
200/// conf.view_stdout = false;
201/// conf.p2p = bitcoind::P2P::No;
202/// conf.network = "regtest";
203/// conf.tmpdir = None;
204/// conf.staticdir = None;
205/// conf.attempts = 3;
206/// assert_eq!(conf, bitcoind::Conf::default());
207/// ```
208///
209#[non_exhaustive]
210#[derive(Debug, PartialEq, Eq, Clone)]
211pub struct Conf<'a> {
212    /// Bitcoind command line arguments containing no spaces like `vec!["-dbcache=300", "-regtest"]`
213    /// note that `port`, `rpcport`, `connect`, `datadir`, `listen`
214    /// cannot be used because they are automatically initialized.
215    pub args: Vec<&'a str>,
216
217    /// if `true` bitcoind log output will not be suppressed
218    pub view_stdout: bool,
219
220    /// Allows to specify options to open p2p port or connect to the another node
221    pub p2p: P2P,
222
223    /// Must match what specified in args without dashes, needed to locate the cookie file
224    /// directory with different/esoteric networks
225    pub network: &'a str,
226
227    /// Temporary directory path.
228    ///
229    /// Optionally specify a temporary or persistent working directory for the node.
230    /// The following two parameters can be configured to simulate desired working directory configuration.
231    ///
232    /// tmpdir is Some() && staticdir is Some() : Error. Cannot be enabled at same time.
233    /// tmpdir is Some(temp_path) && staticdir is None : Create temporary directory at `tmpdir` path.
234    /// tmpdir is None && staticdir is Some(work_path) : Create persistent directory at `staticdir` path.
235    /// tmpdir is None && staticdir is None: Creates a temporary directory in OS default temporary directory (eg /tmp) or `TEMPDIR_ROOT` env variable path.
236    ///
237    /// It may be useful for example to set to a ramdisk via `TEMPDIR_ROOT` env option so that
238    /// bitcoin nodes spawn very fast because their datadirs are in RAM. Should not be enabled with persistent
239    /// mode, as it cause memory overflows.
240    pub tmpdir: Option<PathBuf>,
241
242    /// Persistent directory path
243    pub staticdir: Option<PathBuf>,
244
245    /// Try to spawn the process `attempt` time
246    ///
247    /// The OS is giving available ports to use, however, they aren't booked, so it could rarely
248    /// happen they are used at the time the process is spawn. When retrying other available ports
249    /// are returned reducing the probability of conflicts to negligible.
250    pub attempts: u8,
251
252    /// Enable the ZMQ interface to be accessible.
253    pub enable_zmq: bool,
254
255    /// Load `wallet` after initialization.
256    pub wallet: Option<String>,
257}
258
259impl Default for Conf<'_> {
260    fn default() -> Self {
261        Conf {
262            args: vec!["-regtest", "-fallbackfee=0.0001"],
263            view_stdout: false,
264            p2p: P2P::No,
265            network: "regtest",
266            tmpdir: None,
267            staticdir: None,
268            attempts: 3,
269            enable_zmq: false,
270            wallet: Some("default".to_string()),
271        }
272    }
273}
274
275impl Node {
276    /// Launch the bitcoind process from the given `exe` executable with default args.
277    ///
278    /// Waits for the node to be ready to accept connections before returning
279    pub fn new<S: AsRef<OsStr>>(exe: S) -> anyhow::Result<Node> {
280        Node::with_conf(exe, &Conf::default())
281    }
282
283    /// Launch the bitcoind process from the given `exe` executable with given [Conf] param and create/load the "default" wallet.
284    pub fn with_conf<S: AsRef<OsStr>>(exe: S, conf: &Conf) -> anyhow::Result<Node> {
285        let tmpdir =
286            conf.tmpdir.clone().or_else(|| env::var("TEMPDIR_ROOT").map(PathBuf::from).ok());
287        let work_dir = match (&tmpdir, &conf.staticdir) {
288            (Some(_), Some(_)) => return Err(Error::BothDirsSpecified.into()),
289            (Some(tmpdir), None) => DataDir::Temporary(TempDir::new_in(tmpdir)?),
290            (None, Some(workdir)) => {
291                fs::create_dir_all(workdir)?;
292                DataDir::Persistent(workdir.to_owned())
293            }
294            (None, None) => DataDir::Temporary(TempDir::new()?),
295        };
296
297        let work_dir_path = work_dir.path();
298        if !work_dir_path.exists() {
299            panic!("work dir does not exist");
300        }
301        debug!("work_dir: {:?}", work_dir_path);
302
303        let cookie_file = work_dir_path.join(conf.network).join(".cookie");
304        let rpc_port = get_available_port()?;
305        let rpc_socket = SocketAddrV4::new(LOCAL_IP, rpc_port);
306        let rpc_url = format!("http://{}", rpc_socket);
307        debug!("rpc_url: {}", rpc_url);
308
309        let (p2p_args, p2p_socket) = match conf.p2p {
310            P2P::No => (vec!["-listen=0".to_string()], None),
311            P2P::Yes => {
312                let p2p_port = get_available_port()?;
313                let p2p_socket = SocketAddrV4::new(LOCAL_IP, p2p_port);
314                let bind_arg = format!("-bind={}", p2p_socket);
315                let args = vec![bind_arg];
316                (args, Some(p2p_socket))
317            }
318            P2P::Connect(other_node_url, listen) => {
319                let p2p_port = get_available_port()?;
320                let p2p_socket = SocketAddrV4::new(LOCAL_IP, p2p_port);
321                let bind_arg = format!("-bind={}", p2p_socket);
322                let connect = format!("-connect={}", other_node_url);
323                let mut args = vec![bind_arg, connect];
324                if listen {
325                    args.push("-listen=1".to_string())
326                }
327                (args, Some(p2p_socket))
328            }
329        };
330
331        let (zmq_args, zmq_pub_raw_tx_socket, zmq_pub_raw_block_socket) = match conf.enable_zmq {
332            true => {
333                let zmq_pub_raw_tx_port = get_available_port()?;
334                let zmq_pub_raw_tx_socket = SocketAddrV4::new(LOCAL_IP, zmq_pub_raw_tx_port);
335                let zmq_pub_raw_block_port = get_available_port()?;
336                let zmq_pub_raw_block_socket = SocketAddrV4::new(LOCAL_IP, zmq_pub_raw_block_port);
337                let zmqpubrawblock_arg =
338                    format!("-zmqpubrawblock=tcp://0.0.0.0:{}", zmq_pub_raw_block_port);
339                let zmqpubrawtx_arg = format!("-zmqpubrawtx=tcp://0.0.0.0:{}", zmq_pub_raw_tx_port);
340                (
341                    vec![zmqpubrawtx_arg, zmqpubrawblock_arg],
342                    Some(zmq_pub_raw_tx_socket),
343                    Some(zmq_pub_raw_block_socket),
344                )
345            }
346            false => (vec![], None, None),
347        };
348
349        let stdout = if conf.view_stdout { Stdio::inherit() } else { Stdio::null() };
350
351        let datadir_arg = format!("-datadir={}", work_dir_path.display());
352        let rpc_arg = format!("-rpcport={}", rpc_port);
353        let default_args = [&datadir_arg, &rpc_arg];
354        let conf_args = validate_args(conf.args.clone())?;
355
356        debug!(
357            "launching {:?} with args: {:?} {:?} AND custom args: {:?}",
358            exe.as_ref(),
359            default_args,
360            p2p_args,
361            conf_args
362        );
363
364        let mut process = Command::new(exe.as_ref())
365            .args(default_args)
366            .args(&p2p_args)
367            .args(&conf_args)
368            .args(&zmq_args)
369            .stdout(stdout)
370            .spawn()
371            .with_context(|| format!("Error while executing {:?}", exe.as_ref()))?;
372
373        debug!("cookie file: {}", cookie_file.display());
374
375        if let Some(status) = process.try_wait()? {
376            if conf.attempts > 0 {
377                warn!("early exit with: {:?}. Trying to launch again ({} attempts remaining), maybe some other process used our available port", status, conf.attempts);
378                let mut conf = conf.clone();
379                conf.attempts -= 1;
380                return Self::with_conf(exe, &conf)
381                    .with_context(|| format!("Remaining attempts {}", conf.attempts));
382            } else {
383                error!("early exit with: {:?}", status);
384                return Err(Error::EarlyExit(status).into());
385            }
386        }
387        thread::sleep(Duration::from_millis(1000));
388        assert!(process.stderr.is_none());
389
390        let mut tries = 0;
391        let auth = Auth::CookieFile(cookie_file.clone());
392
393        let client = loop {
394            tries += 1;
395
396            if tries > 10 {
397                error!("failed to get a response from bitcoind");
398                return Err(Error::NoBitcoindInstance.into());
399            }
400
401            let client_base = match Client::new_with_auth(&rpc_url, auth.clone()) {
402                Ok(client) => client,
403                Err(e) => {
404                    error!("failed to create client: {}. Retrying!", e);
405                    thread::sleep(Duration::from_millis(1000));
406                    continue;
407                }
408            };
409
410            // Just use serde value because changes to the GetBlockchainInfo type make debugging hard.
411            let client_result: Result<serde_json::Value, _> =
412                client_base.call("getblockchaininfo", &[]);
413
414            match client_result {
415                Ok(_) => {
416                    let url = match &conf.wallet {
417                        Some(wallet) => {
418                            debug!("trying to create/load wallet: {}", wallet);
419                            // Debugging logic here implicitly tests `into_model` for create/load wallet.
420                            match client_base.create_wallet(wallet) {
421                                Ok(json) => {
422                                    debug!("created wallet: {}", json.name());
423                                }
424                                Err(e) => {
425                                    debug!(
426                                        "initial create_wallet failed, try load instead: {:?}",
427                                        e
428                                    );
429                                    let wallet = client_base.load_wallet(wallet)?.name();
430                                    debug!("loaded wallet: {}", wallet);
431                                }
432                            }
433                            format!("{}/wallet/{}", rpc_url, wallet)
434                        }
435                        None => rpc_url,
436                    };
437                    debug!("creating client with url: {}", url);
438                    break Client::new_with_auth(&url, auth)?;
439                }
440                Err(e) => {
441                    error!("failed to get a response from bitcoind: {}. Retrying!", e);
442                    thread::sleep(Duration::from_millis(1000));
443                    continue;
444                }
445            }
446        };
447
448        Ok(Node {
449            process,
450            client,
451            work_dir,
452            params: ConnectParams {
453                cookie_file,
454                rpc_socket,
455                p2p_socket,
456                zmq_pub_raw_block_socket,
457                zmq_pub_raw_tx_socket,
458            },
459        })
460    }
461
462    /// Returns the rpc URL including the schema eg. http://127.0.0.1:44842
463    pub fn rpc_url(&self) -> String { format!("http://{}", self.params.rpc_socket) }
464
465    #[cfg(any(feature = "0_19_1", not(feature = "download")))]
466    /// Returns the rpc URL including the schema and the given `wallet_name`
467    /// eg. http://127.0.0.1:44842/wallet/my_wallet
468    pub fn rpc_url_with_wallet<T: AsRef<str>>(&self, wallet_name: T) -> String {
469        format!("http://{}/wallet/{}", self.params.rpc_socket, wallet_name.as_ref())
470    }
471
472    /// Return the current workdir path of the running node
473    pub fn workdir(&self) -> PathBuf { self.work_dir.path() }
474
475    /// Returns the [P2P] enum to connect to this node p2p port
476    pub fn p2p_connect(&self, listen: bool) -> Option<P2P> {
477        self.params.p2p_socket.map(|s| P2P::Connect(s, listen))
478    }
479
480    /// Stop the node, waiting correct process termination
481    pub fn stop(&mut self) -> anyhow::Result<ExitStatus> {
482        self.client.stop()?;
483        Ok(self.process.wait()?)
484    }
485
486    #[cfg(any(feature = "0_19_1", not(feature = "download")))]
487    /// Create a new wallet in the running node, and return an RPC client connected to the just
488    /// created wallet
489    pub fn create_wallet<T: AsRef<str>>(&self, wallet: T) -> anyhow::Result<Client> {
490        let _ = self.client.create_wallet(wallet.as_ref())?;
491        Ok(Client::new_with_auth(
492            &self.rpc_url_with_wallet(wallet),
493            Auth::CookieFile(self.params.cookie_file.clone()),
494        )?)
495    }
496}
497
498#[cfg(feature = "download")]
499impl Node {
500    /// create Node struct with the downloaded executable.
501    pub fn from_downloaded() -> anyhow::Result<Node> { Node::new(downloaded_exe_path()?) }
502
503    /// create Node struct with the downloaded executable and given Conf.
504    pub fn from_downloaded_with_conf(conf: &Conf) -> anyhow::Result<Node> {
505        Node::with_conf(downloaded_exe_path()?, conf)
506    }
507}
508
509impl Drop for Node {
510    fn drop(&mut self) {
511        if let DataDir::Persistent(_) = self.work_dir {
512            let _ = self.stop();
513        }
514        let _ = self.process.kill();
515    }
516}
517
518/// Returns a non-used local port if available.
519///
520/// Note there is a race condition during the time the method check availability and the caller
521pub fn get_available_port() -> anyhow::Result<u16> {
522    // using 0 as port let the system assign a port available
523    let t = TcpListener::bind(("127.0.0.1", 0))?; // 0 means the OS choose a free port
524    Ok(t.local_addr().map(|s| s.port())?)
525}
526
527impl From<std::io::Error> for Error {
528    fn from(e: std::io::Error) -> Self { Error::Io(e) }
529}
530
531impl From<client_sync::Error> for Error {
532    fn from(e: client_sync::Error) -> Self { Error::Rpc(e) }
533}
534
535/// Provide the bitcoind executable path if a version feature has been specified
536#[cfg(not(feature = "download"))]
537pub fn downloaded_exe_path() -> anyhow::Result<String> { Err(Error::NoFeature.into()) }
538
539/// Provide the bitcoind executable path if a version feature has been specified
540#[cfg(feature = "download")]
541pub fn downloaded_exe_path() -> anyhow::Result<String> {
542    if std::env::var_os("BITCOIND_SKIP_DOWNLOAD").is_some() {
543        return Err(Error::SkipDownload.into());
544    }
545
546    let mut path: PathBuf = env!("OUT_DIR").into();
547    path.push("bitcoin");
548    path.push(format!("bitcoin-{}", VERSION));
549    path.push("bin");
550
551    if cfg!(target_os = "windows") {
552        path.push("bitcoind.exe");
553    } else {
554        path.push("bitcoind");
555    }
556
557    let path = format!("{}", path.display());
558    debug!("path: {}", path);
559    Ok(path)
560}
561
562/// Returns the daemon `bitcoind` executable with the following precedence:
563///
564/// 1) If it's specified in the `BITCOIND_EXE` env var
565/// 2) If there is no env var but the auto-download feature is enabled, returns the
566///    path of the downloaded executabled
567/// 3) If neither of the precedent are available, the `bitcoind` executable is searched in the `PATH`
568pub fn exe_path() -> anyhow::Result<String> {
569    if let Ok(path) = std::env::var("BITCOIND_EXE") {
570        return Ok(path);
571    }
572    if let Ok(path) = downloaded_exe_path() {
573        return Ok(path);
574    }
575    which::which("bitcoind")
576        .map_err(|_| Error::NoBitcoindExecutableFound.into())
577        .map(|p| p.display().to_string())
578}
579
580/// Validate the specified arg if there is any unavailable or deprecated one
581pub fn validate_args(args: Vec<&str>) -> anyhow::Result<Vec<&str>> {
582    args.iter().try_for_each(|arg| {
583        // other kind of invalid arguments can be added into the list if needed
584        if INVALID_ARGS.iter().any(|x| arg.starts_with(x)) {
585            return Err(Error::RpcUserAndPasswordUsed);
586        }
587        Ok(())
588    })?;
589
590    Ok(args)
591}
592
593#[cfg(test)]
594mod test {
595    use std::net::SocketAddrV4;
596
597    use tempfile::TempDir;
598
599    use super::*;
600    use crate::{exe_path, get_available_port, Conf, Node, LOCAL_IP, P2P};
601
602    #[test]
603    fn test_local_ip() {
604        assert_eq!("127.0.0.1", format!("{}", LOCAL_IP));
605        let port = get_available_port().unwrap();
606        let socket = SocketAddrV4::new(LOCAL_IP, port);
607        assert_eq!(format!("127.0.0.1:{}", port), format!("{}", socket));
608    }
609
610    #[test]
611    fn test_node_get_blockchain_info() {
612        let exe = init();
613        let node = Node::new(exe).unwrap();
614        let info = node.client.get_blockchain_info().unwrap();
615        assert_eq!(0, info.blocks);
616    }
617
618    #[test]
619    fn test_node() {
620        let exe = init();
621        let node = Node::new(exe).unwrap();
622        let info = node.client.get_blockchain_info().unwrap();
623
624        assert_eq!(0, info.blocks);
625        let address = node.client.new_address().unwrap();
626        let _ = node.client.generate_to_address(1, &address).unwrap();
627        let info = node.client.get_blockchain_info().unwrap();
628        assert_eq!(1, info.blocks);
629    }
630
631    #[test]
632    #[cfg(feature = "0_21_2")]
633    fn test_getindexinfo() {
634        let exe = init();
635        let mut conf = Conf::default();
636        conf.args.push("-txindex");
637        let node = Node::with_conf(&exe, &conf).unwrap();
638        assert!(
639            node.client.server_version().unwrap() >= 210_000,
640            "getindexinfo requires bitcoin >0.21"
641        );
642        let info: std::collections::HashMap<String, serde_json::Value> =
643            node.client.call("getindexinfo", &[]).unwrap();
644        assert!(info.contains_key("txindex"));
645        assert!(node.client.server_version().unwrap() >= 210_000);
646    }
647
648    #[test]
649    fn test_p2p() {
650        let exe = init();
651
652        let conf = Conf::<'_> { p2p: P2P::Yes, ..Default::default() };
653        let node = Node::with_conf(&exe, &conf).unwrap();
654        assert_eq!(peers_connected(&node.client), 0);
655
656        let other_conf = Conf::<'_> { p2p: node.p2p_connect(false).unwrap(), ..Default::default() };
657        let other_node = Node::with_conf(&exe, &other_conf).unwrap();
658
659        assert_eq!(peers_connected(&node.client), 1);
660        assert_eq!(peers_connected(&other_node.client), 1);
661    }
662
663    #[cfg(not(target_os = "windows"))] // TODO: investigate why it doesn't work in windows
664    #[test]
665    fn test_data_persistence() {
666        // Create a Conf with staticdir type
667        let mut conf = Conf::default();
668        let datadir = TempDir::new().unwrap();
669        conf.staticdir = Some(datadir.path().to_path_buf());
670
671        // Start Node with persistent db config
672        // Generate 101 blocks
673        // Wallet balance should be 50
674        let node = Node::with_conf(exe_path().unwrap(), &conf).unwrap();
675        let core_addrs = node.client.new_address().unwrap();
676        node.client.generate_to_address(101, &core_addrs).unwrap();
677        let wallet_balance_1 = node.client.get_balance().unwrap();
678        let best_block_1 = node.client.get_best_block_hash().unwrap();
679
680        drop(node);
681
682        // Start a new Node with the same datadir
683        let node = Node::with_conf(exe_path().unwrap(), &conf).unwrap();
684
685        let wallet_balance_2 = node.client.get_balance().unwrap();
686        let best_block_2 = node.client.get_best_block_hash().unwrap();
687
688        // Check node chain data persists
689        assert_eq!(best_block_1, best_block_2);
690
691        // Check the node wallet balance persists
692        assert_eq!(wallet_balance_1, wallet_balance_2);
693    }
694
695    #[test]
696    fn test_multi_p2p() {
697        let exe = init();
698
699        let conf_node1 = Conf::<'_> { p2p: P2P::Yes, ..Default::default() };
700        let node1 = Node::with_conf(&exe, &conf_node1).unwrap();
701        assert_eq!(peers_connected(&node1.client), 0);
702
703        // Create Node 2 connected Node 1
704        let conf_node2 = Conf::<'_> { p2p: node1.p2p_connect(true).unwrap(), ..Default::default() };
705        let node2 = Node::with_conf(&exe, &conf_node2).unwrap();
706
707        // Create Node 3 Connected To Node
708        let conf_node3 =
709            Conf::<'_> { p2p: node2.p2p_connect(false).unwrap(), ..Default::default() };
710        let node3 = Node::with_conf(exe_path().unwrap(), &conf_node3).unwrap();
711
712        // Get each nodes Peers
713        let node1_peers = peers_connected(&node1.client);
714        let node2_peers = peers_connected(&node2.client);
715        let node3_peers = peers_connected(&node3.client);
716
717        // Peers found
718        assert!(node1_peers >= 1);
719        assert!(node2_peers >= 1);
720        assert_eq!(node3_peers, 1, "listen false but more than 1 peer");
721    }
722
723    #[cfg(feature = "0_19_1")]
724    #[test]
725    fn test_multi_wallet() {
726        use corepc_client::bitcoin::Amount;
727
728        let exe = init();
729        let node = Node::new(exe).unwrap();
730        let alice = node.create_wallet("alice").unwrap();
731        let alice_address = alice.new_address().unwrap();
732        let bob = node.create_wallet("bob").unwrap();
733        let bob_address = bob.new_address().unwrap();
734        node.client.generate_to_address(1, &alice_address).unwrap();
735        node.client.generate_to_address(101, &bob_address).unwrap();
736
737        let balances = alice.get_balances().unwrap();
738        let alice_balances: vtype::GetBalances = balances;
739
740        let balances = bob.get_balances().unwrap();
741        let bob_balances: vtype::GetBalances = balances;
742
743        assert_eq!(
744            Amount::from_btc(50.0).unwrap(),
745            Amount::from_btc(alice_balances.mine.trusted).unwrap()
746        );
747        assert_eq!(
748            Amount::from_btc(50.0).unwrap(),
749            Amount::from_btc(bob_balances.mine.trusted).unwrap()
750        );
751        assert_eq!(
752            Amount::from_btc(5000.0).unwrap(),
753            Amount::from_btc(bob_balances.mine.immature).unwrap()
754        );
755        let _txid = alice.send_to_address(&bob_address, Amount::from_btc(1.0).unwrap()).unwrap();
756
757        let balances = alice.get_balances().unwrap();
758        let alice_balances: vtype::GetBalances = balances;
759
760        assert!(
761            Amount::from_btc(alice_balances.mine.trusted).unwrap()
762                < Amount::from_btc(49.0).unwrap()
763                && Amount::from_btc(alice_balances.mine.trusted).unwrap()
764                    > Amount::from_btc(48.9).unwrap()
765        );
766
767        // bob wallet may not be immediately updated
768        for _ in 0..30 {
769            let balances = bob.get_balances().unwrap();
770            let bob_balances: vtype::GetBalances = balances;
771
772            if Amount::from_btc(bob_balances.mine.untrusted_pending).unwrap().to_sat() > 0 {
773                break;
774            }
775            std::thread::sleep(std::time::Duration::from_millis(100));
776        }
777        let balances = bob.get_balances().unwrap();
778        let bob_balances: vtype::GetBalances = balances;
779
780        assert_eq!(
781            Amount::from_btc(1.0).unwrap(),
782            Amount::from_btc(bob_balances.mine.untrusted_pending).unwrap()
783        );
784        assert!(node.create_wallet("bob").is_err(), "wallet already exist");
785    }
786
787    #[test]
788    fn test_node_rpcuser_and_rpcpassword() {
789        let exe = init();
790
791        let mut conf = Conf::default();
792        conf.args.push("-rpcuser=bitcoind");
793        conf.args.push("-rpcpassword=bitcoind");
794
795        let node = Node::with_conf(exe, &conf);
796
797        assert!(node.is_err());
798    }
799
800    #[test]
801    fn test_node_rpcauth() {
802        let exe = init();
803
804        let mut conf = Conf::default();
805        // rpcauth generated with [rpcauth.py](https://github.com/bitcoin/bitcoin/blob/master/share/rpcauth/rpcauth.py)
806        // this could be also added to node, example: [RpcAuth](https://github.com/testcontainers/testcontainers-rs/blob/dev/testcontainers/src/images/coblox_bitcoincore.rs#L39-L91)
807        conf.args.push("-rpcauth=bitcoind:cccd5d7fd36e55c1b8576b8077dc1b83$60b5676a09f8518dcb4574838fb86f37700cd690d99bd2fdc2ea2bf2ab80ead6");
808
809        let node = Node::with_conf(exe, &conf).unwrap();
810
811        let auth = Auth::UserPass("bitcoind".to_string(), "bitcoind".to_string());
812        let client = Client::new_with_auth(
813            format!("{}/wallet/default", node.rpc_url().as_str()).as_str(),
814            auth,
815        )
816        .unwrap();
817        let info = client.get_blockchain_info().unwrap();
818        assert_eq!(0, info.blocks);
819
820        let address = client.new_address().unwrap();
821        let _ = client.generate_to_address(1, &address).unwrap();
822        let info = node.client.get_blockchain_info().unwrap();
823        assert_eq!(1, info.blocks);
824    }
825
826    #[test]
827    fn test_get_cookie_user_and_pass() {
828        let exe = init();
829        let node = Node::new(exe).unwrap();
830
831        let user: &str = "bitcoind_user";
832        let password: &str = "bitcoind_password";
833
834        std::fs::write(&node.params.cookie_file, format!("{}:{}", user, password)).unwrap();
835
836        let result_values = node.params.get_cookie_values().unwrap().unwrap();
837
838        assert_eq!(user, result_values.user);
839        assert_eq!(password, result_values.password);
840    }
841
842    #[test]
843    fn zmq_interface_enabled() {
844        let conf = Conf::<'_> { enable_zmq: true, ..Default::default() };
845        let node = Node::with_conf(exe_path().unwrap(), &conf).unwrap();
846
847        assert!(node.params.zmq_pub_raw_tx_socket.is_some());
848        assert!(node.params.zmq_pub_raw_block_socket.is_some());
849    }
850
851    #[test]
852    fn zmq_interface_disabled() {
853        let exe = init();
854        let node = Node::new(exe).unwrap();
855
856        assert!(node.params.zmq_pub_raw_tx_socket.is_none());
857        assert!(node.params.zmq_pub_raw_block_socket.is_none());
858    }
859
860    fn peers_connected(client: &Client) -> usize {
861        // FIXME: Once client implements get_peer_info use it.
862        // This is kinda cool, it shows we can call any RPC method using the client.
863        let result: Vec<serde_json::Value> = client.call("getpeerinfo", &[]).unwrap();
864        result.len()
865    }
866
867    fn init() -> String {
868        let _ = env_logger::try_init();
869        exe_path().unwrap()
870    }
871}