bitcoind_json_rpc_regtest/
lib.rs

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