Skip to main content

halfin/
lib.rs

1//! # halfin
2//!
3//! A bitcoin node and indexer running utility for integration testing.
4//!
5//! > A {regtest} bitcoin node runner 🏃‍♂️
6//!
7//! This crate makes it simple to run regtest [`bitcoind`], [`utreexod`],
8//! [`electrs`], and [`electrumx`] instances from Rust code,
9//! useful in integration test contexts.
10//!
11//! ## Supported Implementations
12//!
13//! | Kind    | Implementation | Version   | Feature Flag | Default Feature |
14//! |---------|----------------|-----------|--------------|-----------------|
15//! | Node    | `bitcoind`     | `v31.0`   | `bitcoind`   | Yes             |
16//! | Node    | `utreexod`     | `v0.6.0`  | `utreexod`   | Yes             |
17//! |         |                |                          |                 |
18//! | Indexer | `electrs`      | `v0.11.1` | `electrs`    | No              |
19//!
20//! ## Example
21//!
22//! ```rust,ignore
23//! use halfin::bitcoind::BitcoinD;
24//! use halfin::connect;
25//! use halfin::utreexod::UtreexoD;
26//!
27//! let bitcoind = BitcoinD::new().unwrap();
28//! bitcoind.generate(10).unwrap();
29//! assert_eq!(bitcoind.get_chain_tip().unwrap(), 10);
30//!
31//! let utreexod = UtreexoD::new().unwrap();
32//! utreexod.generate(10).unwrap();
33//! assert_eq!(utreexod.get_chain_tip().unwrap(), 10);
34//!
35//! connect(&bitcoind, &utreexod).unwrap();
36//! ```
37//!
38//! [`bitcoind`]: <https://github.com/bitcoin/bitcoin>
39//! [`utreexod`]: <https://github.com/utreexo/utreexod>
40//! [`electrs`]: <https://github.com/romanz/electrs>
41//! [`electrumx`]: <https://github.com/spesmilo/electrumx>
42
43use core::net::Ipv4Addr;
44
45#[cfg(any(
46    feature = "bitcoind",
47    feature = "utreexod",
48    feature = "electrs",
49    feature = "electrumx"
50))]
51use std::io::{BufRead, BufReader, Read};
52use std::net::TcpListener;
53use std::path::PathBuf;
54use std::time::Duration;
55
56pub use serde_json;
57use tempfile::TempDir;
58#[cfg(any(
59    feature = "bitcoind",
60    feature = "utreexod",
61    feature = "electrs",
62    feature = "electrumx"
63))]
64use tracing::info;
65
66#[allow(unused)]
67#[cfg(feature = "bitcoind")]
68pub(crate) use bitcoind::BitcoinD;
69#[allow(unused)]
70#[cfg(feature = "electrs")]
71pub(crate) use electrsd::ElectrsD;
72#[allow(unused)]
73#[cfg(feature = "electrumx")]
74pub(crate) use electrumxd::ElectrumxD;
75#[allow(unused)]
76#[cfg(feature = "utreexod")]
77pub(crate) use utreexod::UtreexoD;
78
79pub use crate::error::Error;
80
81#[cfg(feature = "bitcoind")]
82pub mod bitcoind;
83#[cfg(feature = "electrs")]
84pub mod electrsd;
85#[cfg(feature = "electrumx")]
86pub mod electrumxd;
87pub mod error;
88pub mod node;
89#[cfg(feature = "utreexod")]
90pub mod utreexod;
91
92/// IPv4 localhost address.
93const IPV4_LOCALHOST: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1);
94
95/// Maximum number of attempts at spawning a process.
96pub const SPAWN_ATTEMPTS: u8 = 5;
97
98/// Period between attempts at spawning a process.
99pub const SPAWN_INTERVAL: Duration = Duration::from_millis(500);
100
101/// Period between polls for [`connect`](crate::node::connect) and [`wait_for_height`](crate::node::wait_for_height).
102pub const POLL_INTERVAL: Duration = Duration::from_millis(100);
103
104/// Timeout for [`connect`](crate::node::connect) and [`wait_for_height`](crate::node::wait_for_height).
105pub const WAIT_TIMEOUT: Duration = Duration::from_secs(10);
106
107/// Period between successive attempts of [`Node`](crate::node::Node) connection.
108pub const CONNECTION_INTERVAL: Duration = Duration::from_millis(150);
109
110/// Timeout for [`Node`](crate::node::Node) connection.
111pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
112
113/// Ask the OS for an available port, immediately unbind and return it.
114///
115/// # Panics
116///
117/// Panics if the OS cannot bind a localhost ephemeral port or report the local socket address.
118#[inline]
119pub fn get_available_port() -> u16 {
120    TcpListener::bind((IPV4_LOCALHOST, 0))
121        .unwrap()
122        .local_addr()
123        .unwrap()
124        .port()
125}
126
127/// Spawn a background thread that reads `reader` line by line and re-emits
128/// each line as an [`info!`] event, prefixed with `source`.
129///
130/// Used to pipe a child process' `stdout`/`stderr`
131/// into [`tracing`]. The thread exits on EOF, which happens when the process
132/// dies and its pipe is closed.
133#[cfg(any(
134    feature = "bitcoind",
135    feature = "utreexod",
136    feature = "electrs",
137    feature = "electrumx"
138))]
139pub(crate) fn pipe_to_tracing<R: Read + Send + 'static>(reader: R, source: &'static str) {
140    std::thread::spawn(move || {
141        let mut lines = BufReader::new(reader).lines();
142        while let Some(Ok(line)) = lines.next() {
143            // Skip blank lines so the log stream mirrors the node's output.
144            if !line.trim().is_empty() {
145                info!("{source}: {line}");
146            }
147        }
148    });
149}
150
151/// Owns a node's working directory, either as a temporary or a persistent path.
152///
153/// * [`DataDir::Temporary`]: backed by a [`TempDir`]; the directory is
154///   deleted automatically when this value is dropped.
155/// * [`DataDir::Persistent`]: backed by a plain [`PathBuf`]; the directory
156///   survives the process and is never cleaned up automatically.
157#[derive(Debug)]
158pub enum DataDir {
159    /// A persistent directory that is **not** cleaned up on drop.
160    Persistent(PathBuf),
161    /// A temporary directory that is deleted when this value is dropped.
162    Temporary(TempDir),
163}
164
165impl DataDir {
166    /// Return the underlying filesystem path regardless of variant.
167    pub fn path(&self) -> PathBuf {
168        match self {
169            Self::Persistent(path) => path.to_owned(),
170            Self::Temporary(tmp_dir) => tmp_dir.path().to_path_buf(),
171        }
172    }
173}