bitcoin_harness/
bitcoind_rpc.rs1use ::bitcoin::{hashes::hex::FromHex, Address, Amount, Network, Transaction, Txid};
4use bitcoincore_rpc_json::{FinalizePsbtResult, GetAddressInfoResult};
5use jsonrpc_client::{JsonRpcError, ResponsePayload, SendRequest};
6use reqwest::Url;
7use serde::de::DeserializeOwned;
8use serde::Deserialize;
9use std::collections::HashMap;
10
11pub use crate::bitcoind_rpc_api::*;
12pub use jsonrpc_client;
13
14pub type Result<T> = std::result::Result<T, Error>;
15
16pub const JSONRPC_VERSION: &str = "1.0";
17
18#[jsonrpc_client::implement(BitcoindRpcApi)]
19#[derive(Debug, Clone)]
20pub struct Client {
21 inner: reqwest::Client,
22 base_url: reqwest::Url,
23}
24
25impl Client {
26 pub fn new(url: Url) -> Self {
27 Client {
28 inner: reqwest::Client::new(),
29 base_url: url,
30 }
31 }
32
33 pub fn with_wallet(&self, wallet_name: &str) -> Result<Self> {
34 Ok(Self {
35 base_url: self
36 .base_url
37 .join(format!("/wallet/{}", wallet_name).as_str())?,
38 ..self.clone()
39 })
40 }
41
42 pub async fn network(&self) -> Result<Network> {
43 let blockchain_info = self.getblockchaininfo().await?;
44
45 let network = match blockchain_info.chain.as_str() {
46 "main" => Network::Bitcoin,
47 "test" => Network::Testnet,
48 "regtest" => Network::Regtest,
49 _ => return Err(Error::UnexpectedResponse),
50 };
51
52 Ok(network)
53 }
54
55 pub async fn median_time(&self) -> Result<u64> {
56 let blockchain_info = self.getblockchaininfo().await?;
57
58 Ok(blockchain_info.median_time)
59 }
60
61 pub async fn set_hd_seed(
62 &self,
63 wallet_name: &str,
64 new_key_pool: Option<bool>,
65 wif_private_key: Option<String>,
66 ) -> Result<()> {
67 self.with_wallet(wallet_name)?
68 .sethdseed(new_key_pool, wif_private_key)
69 .await?;
70
71 Ok(())
72 }
73
74 pub async fn send_to_address(
75 &self,
76 wallet_name: &str,
77 address: Address,
78 amount: Amount,
79 ) -> Result<Txid> {
80 let txid = self
81 .with_wallet(wallet_name)?
82 .sendtoaddress(address, amount.to_btc())
83 .await?;
84 let txid = Txid::from_hex(&txid)?;
85
86 Ok(txid)
87 }
88
89 pub async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
90 let hex: String = self.get_raw_transaction_rpc(txid, false).await?;
91 let bytes: Vec<u8> = FromHex::from_hex(&hex)?;
92 let transaction = bitcoin::consensus::encode::deserialize(&bytes)?;
93
94 Ok(transaction)
95 }
96
97 pub async fn get_raw_transaction_verbose(
98 &self,
99 txid: Txid,
100 ) -> Result<bitcoincore_rpc_json::GetRawTransactionResult> {
101 let res = self.get_raw_transaction_rpc(txid, true).await?;
102
103 Ok(res)
104 }
105
106 async fn get_raw_transaction_rpc<R>(&self, txid: Txid, verbose: bool) -> Result<R>
107 where
108 R: std::fmt::Debug + DeserializeOwned,
109 {
110 let body = jsonrpc_client::Request::new_v2("getrawtransaction")
111 .with_argument("txid".into(), txid)?
112 .with_argument("verbose".into(), verbose)?
113 .serialize()?;
114
115 let payload: ResponsePayload<R> = self
116 .inner
117 .send_request::<R>(self.base_url.clone(), body)
118 .await
119 .map_err(::jsonrpc_client::Error::Client)?
120 .payload;
121 let response: std::result::Result<R, JsonRpcError> = payload.into();
122
123 Ok(response.map_err(::jsonrpc_client::Error::JsonRpc)?)
124 }
125
126 pub async fn fund_psbt(
127 &self,
128 wallet_name: &str,
129 inputs: &[bitcoincore_rpc_json::CreateRawTransactionInput],
130 address: Address,
131 amount: Amount,
132 ) -> Result<String> {
133 let mut outputs_converted = HashMap::new();
134 outputs_converted.insert(address.to_string(), amount.to_btc());
135 let psbt = self
136 .with_wallet(wallet_name)?
137 .walletcreatefundedpsbt(inputs, outputs_converted)
138 .await?;
139 Ok(psbt.psbt)
140 }
141
142 pub async fn join_psbts(&self, wallet_name: &str, psbts: &[String]) -> Result<PsbtBase64> {
143 let psbt = self.with_wallet(wallet_name)?.joinpsbts(psbts).await?;
144 Ok(psbt)
145 }
146 pub async fn wallet_process_psbt(
147 &self,
148 wallet_name: &str,
149 psbt: PsbtBase64,
150 ) -> Result<WalletProcessPsbtResponse> {
151 let psbt = self
152 .with_wallet(wallet_name)?
153 .walletprocesspsbt(psbt)
154 .await?;
155 Ok(psbt)
156 }
157
158 pub async fn finalize_psbt(
159 &self,
160 wallet_name: &str,
161 psbt: PsbtBase64,
162 ) -> Result<FinalizePsbtResult> {
163 let psbt = self.with_wallet(wallet_name)?.finalizepsbt(psbt).await?;
164 Ok(psbt)
165 }
166
167 pub async fn address_info(
168 &self,
169 wallet_name: &str,
170 address: &Address,
171 ) -> Result<GetAddressInfoResult> {
172 let address_info = self
173 .with_wallet(wallet_name)?
174 .getaddressinfo(address)
175 .await?;
176 Ok(address_info)
177 }
178}
179
180#[derive(Debug, thiserror::Error)]
181pub enum Error {
182 #[error("JSON Rpc Client: ")]
183 JsonRpcClient(#[from] jsonrpc_client::Error<reqwest::Error>),
184 #[error("Serde JSON: ")]
185 SerdeJson(#[from] serde_json::Error),
186 #[error("Parse amount: ")]
187 ParseAmount(#[from] bitcoin::util::amount::ParseAmountError),
188 #[error("Hex decode: ")]
189 Hex(#[from] bitcoin::hashes::hex::Error),
190 #[error("Bitcoin decode: ")]
191 BitcoinDecode(#[from] bitcoin::consensus::encode::Error),
192 #[error("Unexpected response: ")]
194 UnexpectedResponse,
195 #[error("Parse url: ")]
196 ParseUrl(#[from] url::ParseError),
197}
198
199#[derive(Clone, Copy, Debug, Deserialize)]
206pub struct GetRawTransactionVerboseResponse {
207 #[serde(rename = "blockhash")]
208 pub block_hash: Option<bitcoin::BlockHash>,
209}
210
211#[derive(Copy, Clone, Debug, Deserialize)]
217pub struct GetBlockResponse {
218 pub height: u32,
219}
220
221#[cfg(all(test, feature = "test-docker"))]
222mod test {
223 use super::*;
224 use crate::Bitcoind;
225 use std::time::Duration;
226 use testcontainers::clients;
227 use tokio::time::sleep;
228
229 #[tokio::test]
230 async fn get_network_info() {
231 let tc_client = clients::Cli::default();
232 let (client, _container) = {
233 let blockchain = Bitcoind::new(&tc_client).unwrap();
234
235 (Client::new(blockchain.node_url.clone()), blockchain)
236 };
237
238 let network = client.network().await.unwrap();
239
240 assert_eq!(network, Network::Regtest)
241 }
242
243 #[tokio::test]
244 async fn get_median_time() {
245 let tc_client = clients::Cli::default();
246 let (client, _container) = {
247 let blockchain = Bitcoind::new(&tc_client).unwrap();
248
249 (Client::new(blockchain.node_url.clone()), blockchain)
250 };
251
252 let _mediant_time = client.median_time().await.unwrap();
253 }
254
255 #[tokio::test]
256 async fn blockcount() {
257 let tc_client = testcontainers::clients::Cli::default();
258 let bitcoind = Bitcoind::new(&tc_client).unwrap();
259 bitcoind.init(5).await.unwrap();
260
261 let client = Client::new(bitcoind.node_url.clone());
262
263 let height_0 = client.getblockcount().await.unwrap();
264 sleep(Duration::from_secs(2)).await;
265
266 let height_1 = client.getblockcount().await.unwrap();
267
268 assert!(height_1 > height_0)
269 }
270}