clementine_tx_sender/
test_utils.rs1use std::net::TcpListener;
4
5use bitcoin::secp256k1::SecretKey;
6use clementine_config::TxSenderLimits;
7use clementine_extended_rpc::ExtendedBitcoinRpc;
8use clementine_utils::tracing::initialize_logger;
9use secrecy::ExposeSecret;
10
11use crate::config::{TxSenderBitcoinRpcConfig, TxSenderConfig, TxSenderPostgresConfig};
12use crate::{MempoolConfig, TxSenderDb};
13
14pub async fn create_test_environment(
16 setup_db: bool,
17 setup_rpc: bool,
18) -> (
19 TxSenderConfig,
20 Option<TxSenderDb>,
21 Option<WithProcessCleanup>,
22) {
23 initialize_logger(Some(::tracing::level_filters::LevelFilter::DEBUG))
24 .expect("Failed to initialize logger");
25
26 let mut config = TxSenderConfig {
27 network: bitcoin::Network::Regtest,
28 secret_key: SecretKey::new(&mut bitcoin::secp256k1::rand::thread_rng()),
29 private_da_key: Some(SecretKey::new(&mut bitcoin::secp256k1::rand::thread_rng())),
30 postgres: TxSenderPostgresConfig {
31 host: "127.0.0.1".to_string(),
32 port: 5432,
33 user: "clementine".to_string().into(),
34 password: "clementine".to_string().into(),
35 dbname: format!("clementine_tx_sender_{}", get_current_test_name()),
36 },
37 bitcoin_rpc: TxSenderBitcoinRpcConfig {
38 url: "http://127.0.0.1:18443".to_string(),
39 user: "admin".to_string().into(),
40 password: "admin".to_string().into(),
41 },
42 finality_depth: 1,
43 poll_delay_ms: 500,
44 include_unsafe: true,
45 jsonrpc: None,
46 mempool: MempoolConfig {
47 host: None,
48 endpoint: None,
49 },
50 limits: TxSenderLimits::default(),
51 };
52
53 tracing::info!("Test txsender db name: {}", config.postgres.dbname);
54
55 let rpc = if setup_rpc {
56 Some(create_regtest_rpc(&mut config).await)
57 } else {
58 None
59 };
60 let db = if setup_db {
61 Some(setup_txsender_test_db(&config).await)
62 } else {
63 None
64 };
65
66 (config, db, rpc)
67}
68
69pub async fn setup_txsender_test_db(config: &TxSenderConfig) -> TxSenderDb {
82 let db_name = config.postgres.dbname.clone();
83
84 let admin_config = TxSenderPostgresConfig {
86 dbname: "postgres".to_string(),
87 ..config.postgres.clone()
88 };
89
90 let admin_db = TxSenderDb::connect(&admin_config)
92 .await
93 .expect("Failed to connect to postgres database");
94
95 let _ = sqlx::query(&format!("DROP DATABASE IF EXISTS {db_name}"))
97 .execute(admin_db.pool())
98 .await;
99
100 let _ = sqlx::query(&format!(
101 "CREATE DATABASE {} WITH OWNER {}",
102 db_name,
103 config.postgres.user.expose_secret()
104 ))
105 .execute(admin_db.pool())
106 .await;
107
108 admin_db.pool().close().await;
109
110 let db = TxSenderDb::connect(&config.postgres)
112 .await
113 .expect("Failed to connect to test database");
114 db.run_migrations().await.expect("Failed to run migrations");
115 db
116}
117
118pub struct WithProcessCleanup(
119 pub Option<std::process::Child>,
121 pub ExtendedBitcoinRpc,
123 pub std::path::PathBuf,
125 pub bool,
127);
128impl WithProcessCleanup {
129 pub fn rpc(&self) -> &ExtendedBitcoinRpc {
130 &self.1
131 }
132}
133
134pub async fn create_regtest_rpc(config: &mut TxSenderConfig) -> WithProcessCleanup {
152 use bitcoincore_rpc::RpcApi;
153 use tempfile::TempDir;
154
155 let data_dir = TempDir::new()
157 .expect("Failed to create temporary directory")
158 .keep();
159 let bitcoin_rpc_debug = std::env::var("BITCOIN_RPC_DEBUG").map(|d| !d.is_empty()) == Ok(true);
160
161 let rpc_port = if bitcoin_rpc_debug {
163 18443
164 } else {
165 get_available_port()
166 };
167
168 config.bitcoin_rpc.url = format!("http://127.0.0.1:{rpc_port}");
169
170 if bitcoin_rpc_debug && TcpListener::bind(format!("127.0.0.1:{rpc_port}")).is_err() {
171 return WithProcessCleanup(
173 None,
174 ExtendedBitcoinRpc::connect(
175 "http://127.0.0.1:18443".into(),
176 config.bitcoin_rpc.user.clone(),
177 config.bitcoin_rpc.password.clone(),
178 None,
179 )
180 .await
181 .unwrap(),
182 data_dir.join("debug.log"),
183 false, );
185 }
186 let args = vec![
189 "-regtest".to_string(),
190 format!("-datadir={}", data_dir.display()),
191 "-listen=0".to_string(),
192 format!("-rpcport={}", rpc_port),
193 format!("-rpcuser={}", config.bitcoin_rpc.user.expose_secret()),
194 format!(
195 "-rpcpassword={}",
196 config.bitcoin_rpc.password.expose_secret()
197 ),
198 "-wallet=admin".to_string(),
199 "-txindex=1".to_string(),
200 "-fallbackfee=0.00001".to_string(),
201 "-rpcallowip=0.0.0.0/0".to_string(),
202 "-maxtxfee=5".to_string(),
203 ];
204
205 let log_file = data_dir.join("debug.log");
207 let log_file_path = log_file
208 .to_str()
209 .expect("Failed to convert log file path to string");
210
211 let process = std::process::Command::new("bitcoind")
213 .args(&args)
214 .arg(format!("-debuglogfile={log_file_path}"))
215 .stdout(std::process::Stdio::null())
216 .stderr(std::process::Stdio::null())
217 .spawn()
218 .expect("Failed to start bitcoind");
219
220 if bitcoin_rpc_debug {
221 tracing::warn!("Bitcoind logs are available at {}", log_file_path);
222 }
223
224 let rpc_url = format!("http://127.0.0.1:{rpc_port}");
226
227 let mut attempts = 0;
229 let retry_count = 30;
230 let client = loop {
231 match ExtendedBitcoinRpc::connect(
232 rpc_url.clone(),
233 config.bitcoin_rpc.user.clone(),
234 config.bitcoin_rpc.password.clone(),
235 None,
236 )
237 .await
238 {
239 Ok(client) => break client,
240 Err(_) => {
241 attempts += 1;
242 if attempts >= retry_count {
243 panic!("Bitcoin node failed to start in {retry_count} seconds");
244 }
245 tokio::time::sleep(std::time::Duration::from_secs(1)).await;
246 }
247 }
248 };
249
250 let network_info = client
252 .get_network_info()
253 .await
254 .expect("Failed to get network info");
255 tracing::info!("Using bitcoind version: {}", network_info.version);
256
257 client
259 .create_wallet("admin", None, None, None, None)
260 .await
261 .expect("Failed to create wallet");
262
263 let address = client
265 .get_new_address(None, None)
266 .await
267 .expect("Failed to get new address");
268
269 client
271 .generate_to_address(201, address.assume_checked_ref())
272 .await
273 .expect("Failed to generate blocks");
274
275 WithProcessCleanup(Some(process), client.clone(), log_file, bitcoin_rpc_debug)
276}
277
278pub fn get_available_port() -> u16 {
280 use std::net::TcpListener;
281 TcpListener::bind("127.0.0.1:0")
282 .expect("Could not bind to an available port")
283 .local_addr()
284 .expect("Could not get local address")
285 .port()
286}
287
288pub fn get_current_test_name() -> String {
289 let test_name = std::thread::current()
291 .name()
292 .unwrap_or("main")
293 .split(':')
294 .next_back()
295 .unwrap_or("main")
296 .to_string();
297
298 if test_name == "main" {
301 let pid = std::process::id();
303 format!("{pid}")
304 } else {
305 test_name
306 }
307}