bitcoin_harness/
bitcoind_rpc.rs

1//! An incomplete async bitcoind rpc client that supports multi-wallet features
2
3use ::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    // TODO: add more info to error
193    #[error("Unexpected response: ")]
194    UnexpectedResponse,
195    #[error("Parse url: ")]
196    ParseUrl(#[from] url::ParseError),
197}
198
199/// Response to the RPC command `getrawtransaction`, when the second
200/// argument is set to `true`.
201///
202/// It only defines one field, but can be expanded to include all the
203/// fields returned by `bitcoind` (see:
204/// https://bitcoincore.org/en/doc/0.19.0/rpc/rawtransactions/getrawtransaction/)
205#[derive(Clone, Copy, Debug, Deserialize)]
206pub struct GetRawTransactionVerboseResponse {
207    #[serde(rename = "blockhash")]
208    pub block_hash: Option<bitcoin::BlockHash>,
209}
210
211/// Response to the RPC command `getblock`.
212///
213/// It only defines one field, but can be expanded to include all the
214/// fields returned by `bitcoind` (see:
215/// https://bitcoincore.org/en/doc/0.19.0/rpc/blockchain/getblock/)
216#[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}