bitcoin_harness/
lib.rs

1#![warn(
2    unused_extern_crates,
3    missing_debug_implementations,
4    missing_copy_implementations,
5    rust_2018_idioms,
6    clippy::cast_possible_truncation,
7    clippy::cast_sign_loss,
8    clippy::fallible_impl_from,
9    clippy::cast_precision_loss,
10    clippy::cast_possible_wrap,
11    clippy::dbg_macro
12)]
13#![forbid(unsafe_code)]
14
15//! # bitcoin-harness
16//! A simple lib to start a bitcoind container, generate blocks and funds addresses.
17//! Note: It uses tokio.
18//!
19//! # Examples
20//!
21//! ## Just connect to bitcoind and get the network
22//!
23//! ```rust
24//! use bitcoin_harness::{Bitcoind, bitcoind_rpc, Client};
25//!
26//! # #[tokio::main]
27//! # async fn main() {
28//! let tc_client = testcontainers::clients::Cli::default();
29//! let bitcoind = Bitcoind::new(&tc_client).unwrap();
30//! let client = Client::new(bitcoind.node_url);
31//! let network = client.network().await.unwrap();
32//!
33//! assert_eq!(network, bitcoin::Network::Regtest)
34//! # }
35//! ```
36//!
37//! ## Create a wallet, fund it and get a UTXO
38//!
39//! ```rust
40//! use bitcoin_harness::{Bitcoind, bitcoind_rpc, Client, Wallet};
41//!
42//! # #[tokio::main]
43//! # async fn main() {
44//! let tc_client = testcontainers::clients::Cli::default();
45//! let bitcoind = Bitcoind::new(&tc_client).unwrap();
46//! let client = Client::new(bitcoind.node_url.clone());
47//!
48//! bitcoind.init(5).await.unwrap();
49//!
50//! let wallet = Wallet::new("my_wallet", bitcoind.node_url.clone()).await.unwrap();
51//! let address = wallet.new_address().await.unwrap();
52//! let amount = bitcoin::Amount::from_btc(3.0).unwrap();
53//!
54//! bitcoind.mint(address, amount).await.unwrap();
55//!
56//! let balance = wallet.balance().await.unwrap();
57//!
58//! assert_eq!(balance, amount);
59//!
60//! let utxos = wallet.list_unspent().await.unwrap();
61//!
62//! assert_eq!(utxos.get(0).unwrap().amount, amount);
63//! # }
64//! ```
65
66pub mod bitcoind_rpc;
67pub mod bitcoind_rpc_api;
68pub mod wallet;
69
70use reqwest::Url;
71use std::time::Duration;
72use testcontainers::{clients, images::coblox_bitcoincore::BitcoinCore, Container};
73
74pub use crate::bitcoind_rpc::Client;
75pub use crate::bitcoind_rpc_api::BitcoindRpcApi;
76pub use crate::wallet::Wallet;
77
78pub type Result<T> = std::result::Result<T, Error>;
79
80const BITCOIND_RPC_PORT: u16 = 18443;
81
82#[derive(Debug)]
83pub struct Bitcoind<'c> {
84    pub container: Container<'c, BitcoinCore>,
85    pub node_url: Url,
86    pub wallet_name: String,
87}
88
89impl<'c> Bitcoind<'c> {
90    /// Starts a new regtest bitcoind container
91    pub fn new(client: &'c clients::Cli) -> Result<Self> {
92        let container = client.run(BitcoinCore::default());
93        let port = container.get_host_port_ipv4(BITCOIND_RPC_PORT);
94
95        let auth = &container.image_args().rpc_auth;
96        let url = format!(
97            "http://{}:{}@localhost:{}",
98            auth.username(),
99            auth.password(),
100            port
101        );
102        let url = Url::parse(&url)?;
103
104        let wallet_name = String::from("testwallet");
105
106        Ok(Self {
107            container,
108            node_url: url,
109            wallet_name,
110        })
111    }
112
113    /// Create a test wallet, generate enough block to fund it and activate segwit.
114    /// Generate enough blocks to make the passed `spendable_quantity` spendable.
115    /// Spawn a tokio thread to mine a new block every second.
116    pub async fn init(&self, spendable_quantity: u32) -> Result<()> {
117        let bitcoind_client = Client::new(self.node_url.clone());
118
119        bitcoind_client
120            .createwallet(&self.wallet_name, None, None, None, None)
121            .await?;
122
123        let reward_address = bitcoind_client
124            .with_wallet(&self.wallet_name)?
125            .getnewaddress(None, None)
126            .await?;
127
128        bitcoind_client
129            .generatetoaddress(101 + spendable_quantity, reward_address.clone())
130            .await?;
131        let _ = tokio::spawn(mine(bitcoind_client, reward_address));
132
133        Ok(())
134    }
135
136    /// Send Bitcoin to the specified address, limited to the spendable bitcoin quantity.
137    pub async fn mint(&self, address: bitcoin::Address, amount: bitcoin::Amount) -> Result<()> {
138        let bitcoind_client = Client::new(self.node_url.clone());
139
140        bitcoind_client
141            .send_to_address(&self.wallet_name, address.clone(), amount)
142            .await?;
143
144        // Confirm the transaction
145        let reward_address = bitcoind_client
146            .with_wallet(&self.wallet_name)?
147            .getnewaddress(None, None)
148            .await?;
149        bitcoind_client.generatetoaddress(1, reward_address).await?;
150
151        Ok(())
152    }
153
154    pub fn container_id(&self) -> &str {
155        self.container.id()
156    }
157}
158
159async fn mine(bitcoind_client: Client, reward_address: bitcoin::Address) -> Result<()> {
160    loop {
161        tokio::time::sleep(Duration::from_secs(1)).await;
162        bitcoind_client
163            .generatetoaddress(1, reward_address.clone())
164            .await?;
165    }
166}
167
168#[derive(Debug, thiserror::Error)]
169pub enum Error {
170    #[error("Bitcoin Rpc: ")]
171    BitcoindRpc(#[from] bitcoind_rpc::Error),
172    #[error("Json Rpc: ")]
173    JsonRpc(#[from] jsonrpc_client::Error<reqwest::Error>),
174    #[error("Url Parsing: ")]
175    UrlParseError(#[from] url::ParseError),
176    #[error("Docker port not exposed: ")]
177    PortNotExposed(u16),
178}