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}