Skip to content

Commit ddc584c

Browse files
committed
initial commit
0 parents  commit ddc584c

File tree

6 files changed

+204
-0
lines changed

6 files changed

+204
-0
lines changed

.github/workflows/test.yml

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Test
2+
3+
on: [push]
4+
5+
env:
6+
CARGO_TERM_COLOR: always
7+
BITCOIN_VER: 0.20.1
8+
9+
jobs:
10+
11+
test:
12+
runs-on: ubuntu-20.04
13+
strategy:
14+
matrix:
15+
rust: [stable, nightly, 1.41.0]
16+
17+
steps:
18+
- uses: actions/checkout@v2
19+
- uses: actions-rs/toolchain@v1
20+
with:
21+
toolchain: ${{ matrix.rust }}
22+
override: true
23+
- run: echo "BITCOIND_EXE=${{ github.workspace }}/bitcoin-${{ env.BITCOIN_VER }}/bin/bitcoind" >> $GITHUB_ENV
24+
- run: curl https://bitcoincore.org/bin/bitcoin-core-$BITCOIN_VER/bitcoin-$BITCOIN_VER-x86_64-linux-gnu.tar.gz | tar -xvz bitcoin-$BITCOIN_VER/bin/bitcoind
25+
- uses: actions-rs/cargo@v1
26+
with:
27+
command: test
28+
args: --verbose --all

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/target
2+
Cargo.lock
3+
.idea/

Cargo.toml

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "bitcoind"
3+
version = "0.1.0"
4+
authors = ["Riccardo Casatta <[email protected]>"]
5+
description = "Utility to run a regtest bitcoind process, useful in integration testing environment"
6+
license = "MIT"
7+
edition = "2018"
8+
9+
[dependencies]
10+
bitcoincore-rpc = "0.13.0"
11+
tempfile = "3.1.0"

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2020 Riccardo Casatta
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

Readme.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Bitcoind
2+
3+
Utility to run a regtest bitcoind process, useful in integration testing environment.

src/lib.rs

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#![warn(missing_docs)]
2+
3+
//!
4+
//! Bitcoind
5+
//!
6+
//! Utility to run a regtest bitcoind process, useful in integration testing environment
7+
//!
8+
//! ```no_run
9+
//! use bitcoincore_rpc::RpcApi;
10+
//! let bitcoind = bitcoind::BitcoinD::new("/usr/local/bin/bitcoind").unwrap();
11+
//! assert_eq!(0, bitcoind.client.get_blockchain_info().unwrap().blocks);
12+
//! ```
13+
14+
use bitcoincore_rpc::{Auth, Client, RpcApi};
15+
use std::ffi::OsStr;
16+
use std::net::TcpListener;
17+
use std::path::PathBuf;
18+
use std::process::{Child, Command, ExitStatus};
19+
use std::thread;
20+
use std::time::Duration;
21+
use tempfile::TempDir;
22+
23+
/// Struct representing the bitcoind process with related information
24+
pub struct BitcoinD {
25+
/// Process child handle, used to terminate the process when this struct is dropped
26+
process: Child,
27+
/// Rpc client linked to this bitcoind process
28+
pub client: Client,
29+
/// Work directory, where the node store blocks and other stuff. It is kept in the struct so that
30+
/// directory is deleted only when this struct is dropped
31+
_work_dir: TempDir,
32+
/// Path to the node cookie file, useful for other client to connect to the node
33+
pub cookie_file: PathBuf,
34+
/// Url of the rpc of the node, useful for other client to connect to the node
35+
pub url: String,
36+
}
37+
38+
/// All the possible error in this crate
39+
#[derive(Debug)]
40+
pub enum Error {
41+
/// No port available on the system
42+
PortUnavailable,
43+
/// Wrapper of io Error
44+
Io(std::io::Error),
45+
/// Wrapper of bitcoincore_rpc Error
46+
Rpc(bitcoincore_rpc::Error),
47+
}
48+
49+
impl BitcoinD {
50+
/// Launch the bitcoind process from the given `exe` executable.
51+
/// Waits for the node to be ready before returning
52+
pub fn new<S: AsRef<OsStr>>(exe: S) -> Result<BitcoinD, Error> {
53+
let _work_dir = TempDir::new()?;
54+
let cookie_file = _work_dir.path().join("regtest").join(".cookie");
55+
let rpc_port = get_available_port().ok_or(Error::PortUnavailable)?;
56+
let url = format!("http://127.0.0.1:{}", rpc_port);
57+
58+
let process = Command::new(exe)
59+
.arg(format!("-datadir={}", _work_dir.path().display()))
60+
.arg(format!("-rpcport={}", rpc_port))
61+
.arg("-regtest")
62+
.arg("-listen=0") // do not connect to p2p
63+
.arg("-fallbackfee=0.0001")
64+
.spawn()?;
65+
66+
let node_url_default = format!("{}/wallet/default", url);
67+
// wait bitcoind is ready, use default wallet
68+
let client = loop {
69+
thread::sleep(Duration::from_millis(500));
70+
assert!(process.stderr.is_none());
71+
let client_result = Client::new(url.clone(), Auth::CookieFile(cookie_file.clone()));
72+
if let Ok(client_base) = client_result {
73+
if client_base.get_blockchain_info().is_ok() {
74+
client_base
75+
.create_wallet("default", None, None, None, None)
76+
.unwrap();
77+
break Client::new(node_url_default, Auth::CookieFile(cookie_file.clone()))
78+
.unwrap();
79+
}
80+
}
81+
};
82+
83+
Ok(BitcoinD {
84+
process,
85+
client,
86+
_work_dir,
87+
cookie_file,
88+
url,
89+
})
90+
}
91+
92+
/// Stop the node, waiting correct process termination
93+
pub fn stop(&mut self) -> Result<ExitStatus, Error> {
94+
self.client.stop()?;
95+
Ok(self.process.wait()?)
96+
}
97+
}
98+
99+
impl Drop for BitcoinD {
100+
fn drop(&mut self) {
101+
let _ = self.process.kill();
102+
}
103+
}
104+
105+
fn get_available_port() -> Option<u16> {
106+
(1025..65535).find(|port| TcpListener::bind(("127.0.0.1", *port)).is_ok())
107+
}
108+
109+
impl From<std::io::Error> for Error {
110+
fn from(e: std::io::Error) -> Self {
111+
Error::Io(e)
112+
}
113+
}
114+
115+
impl From<bitcoincore_rpc::Error> for Error {
116+
fn from(e: bitcoincore_rpc::Error) -> Self {
117+
Error::Rpc(e)
118+
}
119+
}
120+
121+
#[cfg(test)]
122+
mod test {
123+
use crate::BitcoinD;
124+
use bitcoincore_rpc::RpcApi;
125+
use std::env;
126+
127+
#[test]
128+
fn test_bitcoind() {
129+
let exe = env::var("BITCOIND_EXE").expect("BITCOIND_EXE env var must be set");
130+
let bitcoind = BitcoinD::new(exe).unwrap();
131+
let info = bitcoind.client.get_blockchain_info().unwrap();
132+
assert_eq!(0, info.blocks);
133+
let address = bitcoind.client.get_new_address(None, None).unwrap();
134+
let _ = bitcoind.client.generate_to_address(1, &address).unwrap();
135+
let info = bitcoind.client.get_blockchain_info().unwrap();
136+
assert_eq!(1, info.blocks);
137+
}
138+
}

0 commit comments

Comments
 (0)