bitcoin-sdk 0.2.0

bitcoin rust sdk
Documentation
mod crypto;
mod serialization;
mod types;

use base64::{Engine, prelude::BASE64_STANDARD};
pub use crypto::*;
pub use types::*;

use anyhow::{Result, anyhow};
use reqwest::{Client, Response};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::collections::HashMap;

#[derive(Debug, Clone)]
pub struct BitcoinClient {
    client: Client,
    url: String,
    auth: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct BitcoinNetWorkRequest {
    jsonrpc: String,
    id: u64,
    method: String,
    params: Value,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct BitcoinNetWorkResponse<T> {
    jsonrpc: String,
    id: u64,
    result: Option<T>,
    error: Option<RpcError>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct RpcError {
    code: i32,
    message: String,
}

impl BitcoinClient {
    pub fn new(url: &str, username: &str, password: &str) -> Self {
        let client = Client::builder()
            .timeout(std::time::Duration::from_secs(30))
            .build()
            .unwrap();
        let auth = format!("{}:{}", username, password);
        let auth = format!("Basic {}", BASE64_STANDARD.encode(&auth));
        BitcoinClient {
            client,
            url: url.to_string(),
            auth,
        }
    }

    pub fn new_local(network: BitcoinClientType) -> Self {
        let (url, port) = match network {
            BitcoinClientType::Mainnet => ("http://127.0.0.1", 8332),
            BitcoinClientType::Testnet => ("http://127.0.0.1", 18332),
            BitcoinClientType::Signet => ("http://127.0.0.1", 38332),
            BitcoinClientType::Regtest => ("http://127.0.0.1", 18443),
        };
        Self::new(&format!("{}:{}", url, port), "bitcoin", "password")
    }

    async fn call<T: for<'de> Deserialize<'de>>(&self, method: &str, params: Value) -> Result<T> {
        let request = BitcoinNetWorkRequest {
            jsonrpc: "2.0".to_string(),
            id: 1,
            method: method.to_string(),
            params,
        };
        let response: Response = self
            .client
            .post(&self.url)
            .header("Authorization", &self.auth)
            .header("Content-Type", "application/json")
            .json(&request)
            .send()
            .await?;
        if !response.status().is_success() {
            let status = response.status();
            let text = response.text().await.unwrap_or_default();
            return Err(anyhow!("HTTP error {}: {}", status, text));
        }
        let rpc_response: BitcoinNetWorkResponse<T> = response.json().await?;
        if let Some(error) = rpc_response.error {
            return Err(anyhow!("RPC error {}: {}", error.code, error.message));
        }
        rpc_response
            .result
            .ok_or_else(|| anyhow!("No result in response"))
    }

    pub async fn get_blockchain_info(&self) -> Result<BlockchainInfo> {
        self.call("getblockchaininfo", Value::Null).await
    }

    pub async fn get_block_count(&self) -> Result<u64> {
        self.call("getblockcount", Value::Null).await
    }

    pub async fn get_best_block_hash(&self) -> Result<String> {
        self.call("getbestblockhash", Value::Null).await
    }

    pub async fn get_block(&self, block_hash: &str, verbosity: u8) -> Result<Block> {
        self.call("getblock", json!([block_hash, verbosity])).await
    }

    pub async fn get_block_hash(&self, height: u64) -> Result<String> {
        self.call("getblockhash", json!([height])).await
    }

    pub async fn get_block_header(&self, block_hash: &str, verbose: bool) -> Result<BlockHeader> {
        self.call("getblockheader", json!([block_hash, verbose]))
            .await
    }

    pub async fn get_chain_tips(&self) -> Result<Vec<ChainTip>> {
        self.call("getchaintips", Value::Null).await
    }

    pub async fn get_difficulty(&self) -> Result<f64> {
        self.call("getdifficulty", Value::Null).await
    }

    pub async fn get_raw_transaction(&self, txid: &str, verbose: bool) -> Result<Transaction> {
        self.call("getrawtransaction", json!([txid, verbose])).await
    }

    pub async fn decode_raw_transaction(&self, tx_hex: &str) -> Result<DecodedTransaction> {
        self.call("decoderawtransaction", json!([tx_hex])).await
    }

    pub async fn send_raw_transaction(&self, tx_hex: &str) -> Result<String> {
        self.call("sendrawtransaction", json!([tx_hex])).await
    }

    pub async fn get_tx_out(
        &self,
        txid: &str,
        vout: u32,
        include_mempool: bool,
    ) -> Result<Option<TxOut>> {
        self.call("gettxout", json!([txid, vout, include_mempool]))
            .await
    }

    pub async fn get_tx_out_set_info(&self) -> Result<TxOutSetInfo> {
        self.call("gettxoutsetinfo", Value::Null).await
    }

    pub async fn get_wallet_info(&self) -> Result<WalletInfo> {
        self.call("getwalletinfo", Value::Null).await
    }

    pub async fn get_balance(
        &self,
        dummy: &str,
        min_conf: i32,
        include_watchonly: bool,
    ) -> Result<f64> {
        self.call("getbalance", json!([dummy, min_conf, include_watchonly]))
            .await
    }

    pub async fn get_new_address(
        &self,
        label: Option<&str>,
        address_type: Option<&str>,
    ) -> Result<String> {
        let params = match (label, address_type) {
            (Some(l), Some(t)) => json!([l, t]),
            (Some(l), None) => json!([l]),
            _ => Value::Null,
        };
        self.call("getnewaddress", params).await
    }

    pub async fn get_addresses_by_label(
        &self,
        label: &str,
    ) -> Result<HashMap<String, AddressInfo>> {
        self.call("getaddressesbylabel", json!([label])).await
    }

    pub async fn validate_address(&self, address: &str) -> Result<ValidateAddress> {
        self.call("validateaddress", json!([address])).await
    }

    pub async fn send_to_address(&self, address: &str, amount: f64) -> Result<String> {
        self.call("sendtoaddress", json!([address, amount])).await
    }

    pub async fn list_unspent(
        &self,
        min_conf: i32,
        max_conf: i32,
        addresses: Option<Vec<&str>>,
    ) -> Result<Vec<Utxo>> {
        let params = match addresses {
            Some(addrs) => json!([min_conf, max_conf, addrs]),
            None => json!([min_conf, max_conf]),
        };
        self.call("listunspent", params).await
    }

    pub async fn get_network_info(&self) -> Result<NetworkInfo> {
        self.call("getnetworkinfo", Value::Null).await
    }

    pub async fn get_peer_info(&self) -> Result<Vec<PeerInfo>> {
        self.call("getpeerinfo", Value::Null).await
    }

    pub async fn get_network_hash_ps(
        &self,
        nblocks: Option<i32>,
        height: Option<i32>,
    ) -> Result<f64> {
        let params = match (nblocks, height) {
            (Some(n), Some(h)) => json!([n, h]),
            (Some(n), None) => json!([n]),
            _ => Value::Null,
        };
        self.call("getnetworkhashps", params).await
    }

    pub async fn get_mempool_info(&self) -> Result<MempoolInfo> {
        self.call("getmempoolinfo", Value::Null).await
    }

    pub async fn get_raw_mempool(&self, verbose: bool) -> Result<Vec<String>> {
        self.call("getrawmempool", json!([verbose])).await
    }

    pub async fn estimate_smart_fee(&self, conf_target: i32) -> Result<FeeEstimate> {
        self.call("estimatesmartfee", json!([conf_target])).await
    }

    pub async fn get_mining_info(&self) -> Result<MiningInfo> {
        self.call("getmininginfo", Value::Null).await
    }

    pub async fn generate_to_address(&self, nblocks: u32, address: &str) -> Result<Vec<String>> {
        self.call("generatetoaddress", json!([nblocks, address]))
            .await
    }

    pub async fn create_raw_transaction(
        &self,
        inputs: Vec<CreateTxInput>,
        outputs: HashMap<String, f64>,
    ) -> Result<String> {
        self.call("createrawtransaction", json!([inputs, outputs]))
            .await
    }

    pub async fn sign_raw_transaction_with_wallet(
        &self,
        tx_hex: &str,
    ) -> Result<SignedTransaction> {
        self.call("signrawtransactionwithwallet", json!([tx_hex]))
            .await
    }

    pub async fn get_block_stats(&self, height: u64) -> Result<BlockStats> {
        self.call("getblockstats", json!([height])).await
    }

    pub async fn batch_call(&self, requests: Vec<(String, Value)>) -> Result<Vec<Value>> {
        let batch_requests: Vec<BitcoinNetWorkRequest> = requests
            .into_iter()
            .enumerate()
            .map(|(i, (method, params))| BitcoinNetWorkRequest {
                jsonrpc: "2.0".to_string(),
                id: i as u64,
                method,
                params,
            })
            .collect();
        let response: Response = self
            .client
            .post(&self.url)
            .header("Authorization", &self.auth)
            .header("Content-Type", "application/json")
            .json(&batch_requests)
            .send()
            .await?;

        if !response.status().is_success() {
            return Err(anyhow!("HTTP error: {}", response.status()));
        }
        let responses: Vec<BitcoinNetWorkResponse<Value>> = response.json().await?;
        let mut results = Vec::new();
        for response in responses {
            if let Some(error) = response.error {
                return Err(anyhow!("RPC error {}: {}", error.code, error.message));
            }
            results.push(response.result.unwrap_or(Value::Null));
        }
        Ok(results)
    }
}