Skip to main content

bitcoin_sdk/
lib.rs

1mod crypto;
2mod serialization;
3mod types;
4
5use base64::{Engine, prelude::BASE64_STANDARD};
6pub use crypto::*;
7pub use types::*;
8
9use anyhow::{Result, anyhow};
10use reqwest::{Client, Response};
11use serde::{Deserialize, Serialize};
12use serde_json::{Value, json};
13use std::collections::HashMap;
14
15#[derive(Debug, Clone)]
16pub struct BitcoinClient {
17    client: Client,
18    url: String,
19    auth: String,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23struct BitcoinNetWorkRequest {
24    jsonrpc: String,
25    id: u64,
26    method: String,
27    params: Value,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31struct BitcoinNetWorkResponse<T> {
32    jsonrpc: String,
33    id: u64,
34    result: Option<T>,
35    error: Option<RpcError>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
39struct RpcError {
40    code: i32,
41    message: String,
42}
43
44impl BitcoinClient {
45    pub fn new(url: &str, username: &str, password: &str) -> Self {
46        let client = Client::builder()
47            .timeout(std::time::Duration::from_secs(30))
48            .build()
49            .unwrap();
50        let auth = format!("{}:{}", username, password);
51        let auth = format!("Basic {}", BASE64_STANDARD.encode(&auth));
52        BitcoinClient {
53            client,
54            url: url.to_string(),
55            auth,
56        }
57    }
58
59    pub fn new_local(network: BitcoinClientType) -> Self {
60        let (url, port) = match network {
61            BitcoinClientType::Mainnet => ("http://127.0.0.1", 8332),
62            BitcoinClientType::Testnet => ("http://127.0.0.1", 18332),
63            BitcoinClientType::Signet => ("http://127.0.0.1", 38332),
64            BitcoinClientType::Regtest => ("http://127.0.0.1", 18443),
65        };
66        Self::new(&format!("{}:{}", url, port), "bitcoin", "password")
67    }
68
69    async fn call<T: for<'de> Deserialize<'de>>(&self, method: &str, params: Value) -> Result<T> {
70        let request = BitcoinNetWorkRequest {
71            jsonrpc: "2.0".to_string(),
72            id: 1,
73            method: method.to_string(),
74            params,
75        };
76        let response: Response = self
77            .client
78            .post(&self.url)
79            .header("Authorization", &self.auth)
80            .header("Content-Type", "application/json")
81            .json(&request)
82            .send()
83            .await?;
84        if !response.status().is_success() {
85            let status = response.status();
86            let text = response.text().await.unwrap_or_default();
87            return Err(anyhow!("HTTP error {}: {}", status, text));
88        }
89        let rpc_response: BitcoinNetWorkResponse<T> = response.json().await?;
90        if let Some(error) = rpc_response.error {
91            return Err(anyhow!("RPC error {}: {}", error.code, error.message));
92        }
93        rpc_response
94            .result
95            .ok_or_else(|| anyhow!("No result in response"))
96    }
97
98    pub async fn get_blockchain_info(&self) -> Result<BlockchainInfo> {
99        self.call("getblockchaininfo", Value::Null).await
100    }
101
102    pub async fn get_block_count(&self) -> Result<u64> {
103        self.call("getblockcount", Value::Null).await
104    }
105
106    pub async fn get_best_block_hash(&self) -> Result<String> {
107        self.call("getbestblockhash", Value::Null).await
108    }
109
110    pub async fn get_block(&self, block_hash: &str, verbosity: u8) -> Result<Block> {
111        self.call("getblock", json!([block_hash, verbosity])).await
112    }
113
114    pub async fn get_block_hash(&self, height: u64) -> Result<String> {
115        self.call("getblockhash", json!([height])).await
116    }
117
118    pub async fn get_block_header(&self, block_hash: &str, verbose: bool) -> Result<BlockHeader> {
119        self.call("getblockheader", json!([block_hash, verbose]))
120            .await
121    }
122
123    pub async fn get_chain_tips(&self) -> Result<Vec<ChainTip>> {
124        self.call("getchaintips", Value::Null).await
125    }
126
127    pub async fn get_difficulty(&self) -> Result<f64> {
128        self.call("getdifficulty", Value::Null).await
129    }
130
131    pub async fn get_raw_transaction(&self, txid: &str, verbose: bool) -> Result<Transaction> {
132        self.call("getrawtransaction", json!([txid, verbose])).await
133    }
134
135    pub async fn decode_raw_transaction(&self, tx_hex: &str) -> Result<DecodedTransaction> {
136        self.call("decoderawtransaction", json!([tx_hex])).await
137    }
138
139    pub async fn send_raw_transaction(&self, tx_hex: &str) -> Result<String> {
140        self.call("sendrawtransaction", json!([tx_hex])).await
141    }
142
143    pub async fn get_tx_out(
144        &self,
145        txid: &str,
146        vout: u32,
147        include_mempool: bool,
148    ) -> Result<Option<TxOut>> {
149        self.call("gettxout", json!([txid, vout, include_mempool]))
150            .await
151    }
152
153    pub async fn get_tx_out_set_info(&self) -> Result<TxOutSetInfo> {
154        self.call("gettxoutsetinfo", Value::Null).await
155    }
156
157    pub async fn get_wallet_info(&self) -> Result<WalletInfo> {
158        self.call("getwalletinfo", Value::Null).await
159    }
160
161    pub async fn get_balance(
162        &self,
163        dummy: &str,
164        min_conf: i32,
165        include_watchonly: bool,
166    ) -> Result<f64> {
167        self.call("getbalance", json!([dummy, min_conf, include_watchonly]))
168            .await
169    }
170
171    pub async fn get_new_address(
172        &self,
173        label: Option<&str>,
174        address_type: Option<&str>,
175    ) -> Result<String> {
176        let params = match (label, address_type) {
177            (Some(l), Some(t)) => json!([l, t]),
178            (Some(l), None) => json!([l]),
179            _ => Value::Null,
180        };
181        self.call("getnewaddress", params).await
182    }
183
184    pub async fn get_addresses_by_label(
185        &self,
186        label: &str,
187    ) -> Result<HashMap<String, AddressInfo>> {
188        self.call("getaddressesbylabel", json!([label])).await
189    }
190
191    pub async fn validate_address(&self, address: &str) -> Result<ValidateAddress> {
192        self.call("validateaddress", json!([address])).await
193    }
194
195    pub async fn send_to_address(&self, address: &str, amount: f64) -> Result<String> {
196        self.call("sendtoaddress", json!([address, amount])).await
197    }
198
199    pub async fn list_unspent(
200        &self,
201        min_conf: i32,
202        max_conf: i32,
203        addresses: Option<Vec<&str>>,
204    ) -> Result<Vec<Utxo>> {
205        let params = match addresses {
206            Some(addrs) => json!([min_conf, max_conf, addrs]),
207            None => json!([min_conf, max_conf]),
208        };
209        self.call("listunspent", params).await
210    }
211
212    pub async fn get_network_info(&self) -> Result<NetworkInfo> {
213        self.call("getnetworkinfo", Value::Null).await
214    }
215
216    pub async fn get_peer_info(&self) -> Result<Vec<PeerInfo>> {
217        self.call("getpeerinfo", Value::Null).await
218    }
219
220    pub async fn get_network_hash_ps(
221        &self,
222        nblocks: Option<i32>,
223        height: Option<i32>,
224    ) -> Result<f64> {
225        let params = match (nblocks, height) {
226            (Some(n), Some(h)) => json!([n, h]),
227            (Some(n), None) => json!([n]),
228            _ => Value::Null,
229        };
230        self.call("getnetworkhashps", params).await
231    }
232
233    pub async fn get_mempool_info(&self) -> Result<MempoolInfo> {
234        self.call("getmempoolinfo", Value::Null).await
235    }
236
237    pub async fn get_raw_mempool(&self, verbose: bool) -> Result<Vec<String>> {
238        self.call("getrawmempool", json!([verbose])).await
239    }
240
241    pub async fn estimate_smart_fee(&self, conf_target: i32) -> Result<FeeEstimate> {
242        self.call("estimatesmartfee", json!([conf_target])).await
243    }
244
245    pub async fn get_mining_info(&self) -> Result<MiningInfo> {
246        self.call("getmininginfo", Value::Null).await
247    }
248
249    pub async fn generate_to_address(&self, nblocks: u32, address: &str) -> Result<Vec<String>> {
250        self.call("generatetoaddress", json!([nblocks, address]))
251            .await
252    }
253
254    pub async fn create_raw_transaction(
255        &self,
256        inputs: Vec<CreateTxInput>,
257        outputs: HashMap<String, f64>,
258    ) -> Result<String> {
259        self.call("createrawtransaction", json!([inputs, outputs]))
260            .await
261    }
262
263    pub async fn sign_raw_transaction_with_wallet(
264        &self,
265        tx_hex: &str,
266    ) -> Result<SignedTransaction> {
267        self.call("signrawtransactionwithwallet", json!([tx_hex]))
268            .await
269    }
270
271    pub async fn get_block_stats(&self, height: u64) -> Result<BlockStats> {
272        self.call("getblockstats", json!([height])).await
273    }
274
275    pub async fn batch_call(&self, requests: Vec<(String, Value)>) -> Result<Vec<Value>> {
276        let batch_requests: Vec<BitcoinNetWorkRequest> = requests
277            .into_iter()
278            .enumerate()
279            .map(|(i, (method, params))| BitcoinNetWorkRequest {
280                jsonrpc: "2.0".to_string(),
281                id: i as u64,
282                method,
283                params,
284            })
285            .collect();
286        let response: Response = self
287            .client
288            .post(&self.url)
289            .header("Authorization", &self.auth)
290            .header("Content-Type", "application/json")
291            .json(&batch_requests)
292            .send()
293            .await?;
294
295        if !response.status().is_success() {
296            return Err(anyhow!("HTTP error: {}", response.status()));
297        }
298        let responses: Vec<BitcoinNetWorkResponse<Value>> = response.json().await?;
299        let mut results = Vec::new();
300        for response in responses {
301            if let Some(error) = response.error {
302                return Err(anyhow!("RPC error {}: {}", error.code, error.message));
303            }
304            results.push(response.result.unwrap_or(Value::Null));
305        }
306        Ok(results)
307    }
308}