Skip to main content

ethos_bitcoind/
test_config.rs

1//! Test configuration for Bitcoin RPC testing
2//!
3//! This module provides configuration utilities for running Bitcoin nodes in test environments.
4
5use std::path::PathBuf;
6use std::{env, fmt};
7
8use bitcoin::Network;
9
10use crate::config::Config;
11
12const DEFAULT_EXTRA_ARGS: [&str; 2] = ["-prune=0", "-txindex"];
13
14/// Error returned when the configured network is not supported for node startup.
15#[derive(Debug)]
16pub struct UnsupportedNetwork;
17
18impl std::fmt::Display for UnsupportedNetwork {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        write!(f, "unsupported network")
21    }
22}
23
24impl std::error::Error for UnsupportedNetwork {}
25
26/// TestConfig represents the configuration needed to run a Bitcoin node in a test environment.
27/// This struct encapsulates test‑node settings: network, RPC port, username, password, and extra args.
28/// Defaults are:
29/// - `network = Network::Regtest`
30/// - `rpc_port = 0` (auto‑select a free port)
31/// - `rpc_username = "rpcuser"`
32/// - `rpc_password = "rpcpassword"`
33/// - `bitcoind_path = None` (use executable from PATH)
34/// - `extra_args = ["-prune=0", "-txindex"]` (for full blockchain history and transaction lookup)
35///
36/// To override any of these, simply modify fields on `TestConfig::default()`
37/// (or assign directly in code). If you prefer not to recompile for every change,
38/// consider using `TestConfig::from_env()` to read overrides from environment variables.
39///
40/// # Examples
41///
42/// ```rust,ignore
43/// let mut cfg = TestConfig::default();
44/// cfg.network = Network::Testnet;
45/// cfg.rpc_port = 18545;
46/// cfg.rpc_username = "alice".into();
47/// ```
48///
49/// # Environment Overrides
50///
51/// Reads `RPC_NETWORK`, `RPC_PORT`, `RPC_USER`, and `RPC_PASS`, and `BITCOIND_PATH` (path to bitcoind executable) to override defaults.
52#[derive(Clone)]
53pub struct TestConfig {
54    /// Which Bitcoin network to run against.
55    pub network: Network,
56    /// The port number for RPC communication with the Bitcoin node.
57    /// A value of 0 indicates that an available port should be automatically selected.
58    pub rpc_port: u16,
59    /// The username for RPC authentication.
60    /// Can be customized to match your `bitcoin.conf` `rpcuser` setting.
61    pub rpc_username: String,
62    /// The password for RPC authentication.
63    /// Can be customized to match your `bitcoin.conf` `rpcpassword` setting.
64    pub rpc_password: String,
65    /// Path to the bitcoind executable. If None, the default executable name is used (e.g. from PATH).
66    pub bitcoind_path: Option<PathBuf>,
67    /// Extra command-line arguments to pass to bitcoind
68    pub extra_args: Vec<String>,
69}
70
71impl fmt::Debug for TestConfig {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        f.debug_struct("TestConfig")
74            .field("network", &self.network)
75            .field("rpc_port", &self.rpc_port)
76            .field("rpc_username", &"[redacted]")
77            .field("rpc_password", &"[redacted]")
78            .field("bitcoind_path", &self.bitcoind_path)
79            .field("extra_args", &self.extra_args)
80            .finish()
81    }
82}
83
84impl TestConfig {
85    /// Return the value used with `-chain=<value>` for the configured network.
86    /// Returns `Err(UnsupportedNetwork)` if the network variant is not supported for node startup.
87    pub fn as_chain_str(&self) -> Result<&'static str, UnsupportedNetwork> {
88        #[allow(unreachable_patterns)]
89        match self.network {
90            Network::Bitcoin => Ok("main"),
91            Network::Regtest => Ok("regtest"),
92            Network::Signet => Ok("signet"),
93            Network::Testnet => Ok("testnet"),
94            Network::Testnet4 => Ok("testnet4"),
95            _ => Err(UnsupportedNetwork),
96        }
97    }
98
99    /// Parse network from common strings (case-insensitive). Accepts: regtest, testnet|test,
100    /// signet, mainnet|main|bitcoin, testnet4.
101    pub fn network_from_str(s: &str) -> Option<Network> {
102        match s.to_ascii_lowercase().as_str() {
103            "regtest" => Some(Network::Regtest),
104            "testnet" | "test" => Some(Network::Testnet),
105            "signet" => Some(Network::Signet),
106            "mainnet" | "main" | "bitcoin" => Some(Network::Bitcoin),
107            "testnet4" => Some(Network::Testnet4),
108            _ => None,
109        }
110    }
111
112    /// Create a `TestConfig`, overriding defaults with environment variables:
113    /// - `RPC_NETWORK`: overrides `network`; one of `regtest`, `testnet|test`, `signet`, `mainnet|main|bitcoin`, `testnet4`
114    /// - `RPC_PORT`: overrides `rpc_port`
115    /// - `RPC_USER`: overrides `rpc_username`
116    /// - `RPC_PASS`: overrides `rpc_password`
117    /// - `BITCOIND_PATH`: overrides `bitcoind_path` (path to the bitcoind executable)
118    #[allow(clippy::field_reassign_with_default)]
119    pub fn from_env() -> Self {
120        let mut cfg = Self::default();
121        if let Ok(net) = env::var("RPC_NETWORK") {
122            if let Some(n) = Self::network_from_str(&net) {
123                cfg.network = n;
124            }
125        }
126        if let Ok(port_str) = env::var("RPC_PORT") {
127            if let Ok(port) = port_str.parse() {
128                cfg.rpc_port = port;
129            }
130        }
131        if let Ok(user) = env::var("RPC_USER") {
132            cfg.rpc_username = user;
133        }
134        if let Ok(pass) = env::var("RPC_PASS") {
135            cfg.rpc_password = pass;
136        }
137        if let Ok(path) = env::var("BITCOIND_PATH") {
138            cfg.bitcoind_path = Some(PathBuf::from(path));
139        }
140        cfg
141    }
142
143    /// Convert this test configuration into a full Config instance
144    pub fn into_config(self) -> Config {
145        Config {
146            rpc_url: format!("http://127.0.0.1:{}", self.rpc_port),
147            rpc_user: self.rpc_username,
148            rpc_password: self.rpc_password,
149        }
150    }
151
152    /// Create a TestConfig from a full Config instance
153    pub fn from_config(config: &Config) -> Self {
154        // Extract port from URL, defaulting to 0 if parsing fails
155        let rpc_port =
156            config.rpc_url.split(':').next_back().and_then(|s| s.parse().ok()).unwrap_or(0);
157
158        Self {
159            network: Network::Regtest, // Default to regtest for test environments
160            rpc_port,
161            rpc_username: config.rpc_user.clone(),
162            rpc_password: config.rpc_password.clone(),
163            bitcoind_path: None,
164            extra_args: DEFAULT_EXTRA_ARGS.map(String::from).to_vec(),
165        }
166    }
167}
168
169impl Default for TestConfig {
170    fn default() -> Self {
171        Self {
172            network: Network::Regtest,
173            rpc_port: 0,
174            rpc_username: "rpcuser".to_string(),
175            rpc_password: "rpcpassword".to_string(),
176            bitcoind_path: None,
177            extra_args: DEFAULT_EXTRA_ARGS.map(String::from).to_vec(),
178        }
179    }
180}