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
15pub 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 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 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 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 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}