bitcoin_rpc_midas/node/
mod.rs1use std::sync::Arc;
6use std::time::{Duration, Instant};
7
8use anyhow::Result;
9use async_trait::async_trait;
10use tempfile::TempDir;
11use tokio::io::AsyncBufReadExt;
12use tokio::process::{Child, Command};
13use tokio::sync::{Mutex, RwLock};
14use tracing::{debug, error, info};
15use std::process::Stdio;
16
17use crate::test_config::TestConfig;
18
19#[derive(Debug, Default, Clone)]
21pub struct NodeState {
22 pub is_running: bool,
23}
24
25#[derive(Debug, Clone)]
27pub enum PortSelection {
28 Fixed(u16),
30 Dynamic,
32 Zero,
34}
35
36#[async_trait]
38pub trait NodeManager: Send + Sync + std::any::Any + std::fmt::Debug {
39 async fn start(&self) -> Result<()>;
40 async fn stop(&mut self) -> Result<()>;
41 async fn get_state(&self) -> Result<NodeState>;
42 fn rpc_port(&self) -> u16;
44}
45
46#[derive(Debug)]
48pub struct BitcoinNodeManager {
49 state: Arc<RwLock<NodeState>>,
50 child: Arc<Mutex<Option<Child>>>,
51 pub rpc_port: u16,
52 config: TestConfig,
53 _datadir: Option<TempDir>,
54}
55
56impl BitcoinNodeManager {
57 pub fn new() -> Result<Self> { Self::new_with_config(&TestConfig::default()) }
58
59 pub fn new_with_config(config: &TestConfig) -> Result<Self> {
60 let datadir = TempDir::new()?;
61
62 let rpc_port = if config.rpc_port == 0 {
69 let listener = std::net::TcpListener::bind(("127.0.0.1", 0))?;
71 let port = listener.local_addr()?.port();
72 drop(listener);
73 port
74 } else {
75 config.rpc_port
76 };
77
78 Ok(Self {
79 state: Arc::new(RwLock::new(NodeState::default())),
80 child: Arc::new(Mutex::new(None)),
81 rpc_port,
82 config: config.clone(),
83 _datadir: Some(datadir),
84 })
85 }
86
87 pub fn rpc_port(&self) -> u16 { self.rpc_port }
88}
89
90#[async_trait]
91impl NodeManager for BitcoinNodeManager {
92 async fn start(&self) -> Result<()> {
93 let mut state = self.state.write().await;
94 if state.is_running {
95 return Ok(());
96 }
97
98 let datadir = self._datadir.as_ref().unwrap().path();
99 let mut cmd = Command::new("bitcoind");
100
101 let chain = format!("-chain={}", self.config.as_chain_str());
102 let data_dir = format!("-datadir={}", datadir.display());
103 let rpc_port = format!("-rpcport={}", self.rpc_port);
104 let rpc_bind = format!("-rpcbind=127.0.0.1:{}", self.rpc_port);
105 let rpc_user = format!("-rpcuser={}", self.config.rpc_username);
106 let rpc_password = format!("-rpcpassword={}", self.config.rpc_password);
107
108 let mut args = vec![
109 &chain,
110 "-listen=0",
111 &data_dir,
112 &rpc_port,
113 &rpc_bind,
114 "-rpcallowip=127.0.0.1",
115 "-fallbackfee=0.0002",
116 "-server=1",
117 "-prune=1",
118 &rpc_user,
119 &rpc_password,
120 ];
121
122 for arg in &self.config.extra_args {
123 args.push(arg);
124 }
125
126 cmd.args(&args);
127
128 cmd.stderr(Stdio::piped());
130 cmd.stdout(Stdio::piped());
131
132 let mut child = cmd.spawn()?;
133
134 let stderr = child.stderr.take().unwrap();
136 let stderr_reader = tokio::io::BufReader::new(stderr);
137 tokio::spawn(async move {
138 let mut lines = stderr_reader.lines();
139 while let Ok(Some(line)) = lines.next_line().await {
140 error!("bitcoind stderr: {}", line);
141 }
142 });
143
144 let stdout = child.stdout.take().unwrap();
146 let stdout_reader = tokio::io::BufReader::new(stdout);
147 tokio::spawn(async move {
148 let mut lines = stdout_reader.lines();
149 while let Ok(Some(line)) = lines.next_line().await {
150 info!("bitcoind stdout: {}", line);
151 }
152 });
153
154 let mut child_guard = self.child.lock().await;
156 *child_guard = Some(child);
157
158 info!("Waiting for Bitcoin node to initialize...");
159 tokio::time::sleep(Duration::from_millis(150)).await;
160
161 let deadline = Instant::now() + Duration::from_secs(10);
163 let mut attempts = 0;
164 while Instant::now() < deadline {
165 if let Some(child) = child_guard.as_mut() {
166 if let Ok(Some(status)) = child.try_wait() {
167 let error = format!("Bitcoin node exited early with status: {}", status);
168 error!("{}", error);
169 anyhow::bail!(error);
170 }
171 }
172
173 let client = reqwest::Client::new();
175 match client
176 .post(format!("http://127.0.0.1:{}/", self.rpc_port))
177 .basic_auth(&self.config.rpc_username, Some(&self.config.rpc_password))
178 .json(&serde_json::json!({
179 "jsonrpc": "2.0",
180 "method": "getnetworkinfo",
181 "params": [],
182 "id": 1
183 }))
184 .send()
185 .await
186 {
187 Ok(response) =>
188 if response.status().is_success() {
189 state.is_running = true;
190 info!("Bitcoin node started successfully on port {}", self.rpc_port);
191 return Ok(());
192 } else {
193 debug!(
194 "RPC request failed with status {} (attempt {})",
195 response.status(),
196 attempts
197 );
198 },
199 Err(e) => {
200 debug!("Failed to connect to RPC (attempt {}): {}", attempts, e);
201 }
202 }
203
204 attempts += 1;
205 tokio::time::sleep(Duration::from_millis(200)).await;
206 }
207
208 let error = format!(
209 "Timed out waiting for Bitcoin node to start on port {} after {} attempts",
210 self.rpc_port, attempts
211 );
212 error!("{}", error);
213 anyhow::bail!(error);
214 }
215
216 async fn stop(&mut self) -> Result<()> {
217 let mut state = self.state.write().await;
218 if !state.is_running {
219 return Ok(());
220 }
221
222 let child = self.child.lock().await.take();
223 if let Some(mut child) = child {
224 std::mem::drop(child.kill());
225 std::mem::drop(child.wait());
226 }
227
228 state.is_running = false;
229 Ok(())
230 }
231
232 async fn get_state(&self) -> Result<NodeState> { Ok(self.state.read().await.clone()) }
233
234 fn rpc_port(&self) -> u16 { self.rpc_port }
235}
236
237impl Drop for BitcoinNodeManager {
238 fn drop(&mut self) {
239 if let Some(mut child) = self.child.try_lock().ok().and_then(|mut guard| guard.take()) {
240 std::mem::drop(child.kill());
241 std::mem::drop(child.wait());
242 }
243 }
244}
245
246impl Default for BitcoinNodeManager {
247 fn default() -> Self {
248 Self::new_with_config(&TestConfig::default())
249 .expect("Failed to create default BitcoinNodeManager")
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn test_extra_args() {
259 let config = crate::test_config::TestConfig {
260 extra_args: vec!["-debug=1".to_string()],
261 ..crate::test_config::TestConfig::default()
262 };
263
264 let node_manager = BitcoinNodeManager::new_with_config(&config)
265 .expect("Failed to create node manager with extra args");
266
267 assert_eq!(node_manager.config.extra_args[0], "-debug=1");
268 }
269}