// Bitcoin Dev Kit // Written in 2021 by Riccardo Casatta // // Copyright (c) 2020-2021 Bitcoin Dev Kit Developers // // This file is licensed under the Apache License, Version 2.0 or the MIT license // , at your option. // You may not use this file except in accordance with one or both of these // licenses. //! Rpc Blockchain //! //! Backend that gets blockchain data from Bitcoin Core RPC //! //! This is an **EXPERIMENTAL** feature, API and other major changes are expected. //! //! ## Example //! //! ```no_run //! # use bdk::blockchain::{RpcConfig, RpcBlockchain, ConfigurableBlockchain, rpc::Auth}; //! let config = RpcConfig { //! url: "127.0.0.1:18332".to_string(), //! auth: Auth::Cookie { //! file: "/home/user/.bitcoin/.cookie".into(), //! }, //! network: bdk::bitcoin::Network::Testnet, //! wallet_name: "wallet_name".to_string(), //! sync_params: None, //! }; //! let blockchain = RpcBlockchain::from_config(&config); //! ``` use crate::bitcoin::hashes::hex::ToHex; use crate::bitcoin::{Network, OutPoint, Transaction, TxOut, Txid}; use crate::blockchain::*; use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils}; use crate::descriptor::calc_checksum; use crate::error::MissingCachedScripts; use crate::{BlockTime, Error, FeeRate, KeychainKind, LocalUtxo, TransactionDetails}; use bitcoin::Script; use bitcoincore_rpc::json::{ GetTransactionResultDetailCategory, ImportMultiOptions, ImportMultiRequest, ImportMultiRequestScriptPubkey, ImportMultiRescanSince, ListTransactionResult, ListUnspentResultEntry, ScanningDetails, }; use bitcoincore_rpc::jsonrpc::serde_json::{json, Value}; use bitcoincore_rpc::Auth as RpcAuth; use bitcoincore_rpc::{Client, RpcApi}; use log::{debug, info}; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use std::ops::Deref; use std::path::PathBuf; use std::thread; use std::time::Duration; /// The main struct for RPC backend implementing the [crate::blockchain::Blockchain] trait #[derive(Debug)] pub struct RpcBlockchain { /// Rpc client to the node, includes the wallet name client: Client, /// Whether the wallet is a "descriptor" or "legacy" wallet in Core is_descriptors: bool, /// Blockchain capabilities, cached here at startup capabilities: HashSet, /// Sync parameters. sync_params: RpcSyncParams, } impl Deref for RpcBlockchain { type Target = Client; fn deref(&self) -> &Self::Target { &self.client } } /// RpcBlockchain configuration options #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct RpcConfig { /// The bitcoin node url pub url: String, /// The bitcoin node authentication mechanism pub auth: Auth, /// The network we are using (it will be checked the bitcoin node network matches this) pub network: Network, /// The wallet name in the bitcoin node, consider using [crate::wallet::wallet_name_from_descriptor] for this pub wallet_name: String, /// Sync parameters pub sync_params: Option, } /// Sync parameters for Bitcoin Core RPC. /// /// In general, BDK tries to sync `scriptPubKey`s cached in [`crate::database::Database`] with /// `scriptPubKey`s imported in the Bitcoin Core Wallet. These parameters are used for determining /// how the `importdescriptors` RPC calls are to be made. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct RpcSyncParams { /// The minimum number of scripts to scan for on initial sync. pub start_script_count: usize, /// Time in unix seconds in which initial sync will start scanning from (0 to start from genesis). pub start_time: u64, /// Forces every sync to use `start_time` as import timestamp. pub force_start_time: bool, /// RPC poll rate (in seconds) to get state updates. pub poll_rate_sec: u64, } impl Default for RpcSyncParams { fn default() -> Self { Self { start_script_count: 100, start_time: 0, force_start_time: false, poll_rate_sec: 3, } } } /// This struct is equivalent to [bitcoincore_rpc::Auth] but it implements [serde::Serialize] /// To be removed once upstream equivalent is implementing Serialize (json serialization format /// should be the same), see [rust-bitcoincore-rpc/pull/181](https://github.com/rust-bitcoin/rust-bitcoincore-rpc/pull/181) #[derive(Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] #[serde(untagged)] pub enum Auth { /// None authentication None, /// Authentication with username and password, usually [Auth::Cookie] should be preferred UserPass { /// Username username: String, /// Password password: String, }, /// Authentication with a cookie file Cookie { /// Cookie file file: PathBuf, }, } impl From for RpcAuth { fn from(auth: Auth) -> Self { match auth { Auth::None => RpcAuth::None, Auth::UserPass { username, password } => RpcAuth::UserPass(username, password), Auth::Cookie { file } => RpcAuth::CookieFile(file), } } } impl Blockchain for RpcBlockchain { fn get_capabilities(&self) -> HashSet { self.capabilities.clone() } fn broadcast(&self, tx: &Transaction) -> Result<(), Error> { Ok(self.client.send_raw_transaction(tx).map(|_| ())?) } fn estimate_fee(&self, target: usize) -> Result { let sat_per_kb = self .client .estimate_smart_fee(target as u16, None)? .fee_rate .ok_or(Error::FeeRateUnavailable)? .to_sat() as f64; Ok(FeeRate::from_sat_per_vb((sat_per_kb / 1000f64) as f32)) } } impl GetTx for RpcBlockchain { fn get_tx(&self, txid: &Txid) -> Result, Error> { Ok(Some(self.client.get_raw_transaction(txid, None)?)) } } impl GetHeight for RpcBlockchain { fn get_height(&self) -> Result { Ok(self.client.get_blockchain_info().map(|i| i.blocks as u32)?) } } impl GetBlockHash for RpcBlockchain { fn get_block_hash(&self, height: u64) -> Result { Ok(self.client.get_block_hash(height)?) } } impl WalletSync for RpcBlockchain { fn wallet_setup(&self, db: &mut D, prog: Box) -> Result<(), Error> where D: BatchDatabase, { let batch = DbState::new(db, &self.sync_params, &*prog)? .sync_with_core(&self.client, self.is_descriptors)? .as_db_batch()?; db.commit_batch(batch) } } impl ConfigurableBlockchain for RpcBlockchain { type Config = RpcConfig; /// Returns RpcBlockchain backend creating an RPC client to a specific wallet named as the descriptor's checksum /// if it's the first time it creates the wallet in the node and upon return is granted the wallet is loaded fn from_config(config: &Self::Config) -> Result { let wallet_url = format!("{}/wallet/{}", config.url, &config.wallet_name); let client = Client::new(wallet_url.as_str(), config.auth.clone().into())?; let rpc_version = client.version()?; info!("connected to '{}' with auth: {:?}", wallet_url, config.auth); if client.list_wallets()?.contains(&config.wallet_name) { info!("wallet already loaded: {}", config.wallet_name); } else if list_wallet_dir(&client)?.contains(&config.wallet_name) { client.load_wallet(&config.wallet_name)?; info!("wallet loaded: {}", config.wallet_name); } else { // pre-0.21 use legacy wallets if rpc_version < 210_000 { client.create_wallet(&config.wallet_name, Some(true), None, None, None)?; } else { // TODO: move back to api call when https://github.com/rust-bitcoin/rust-bitcoincore-rpc/issues/225 is closed let args = [ Value::String(config.wallet_name.clone()), Value::Bool(true), Value::Bool(false), Value::Null, Value::Bool(false), Value::Bool(true), ]; let _: Value = client.call("createwallet", &args)?; } info!("wallet created: {}", config.wallet_name); } let is_descriptors = is_wallet_descriptor(&client)?; let blockchain_info = client.get_blockchain_info()?; let network = match blockchain_info.chain.as_str() { "main" => Network::Bitcoin, "test" => Network::Testnet, "regtest" => Network::Regtest, "signet" => Network::Signet, _ => return Err(Error::Generic("Invalid network".to_string())), }; if network != config.network { return Err(Error::InvalidNetwork { requested: config.network, found: network, }); } let mut capabilities: HashSet<_> = vec![Capability::FullHistory].into_iter().collect(); if rpc_version >= 210_000 { let info: HashMap = client.call("getindexinfo", &[]).unwrap(); if info.contains_key("txindex") { capabilities.insert(Capability::GetAnyTx); capabilities.insert(Capability::AccurateFees); } } Ok(RpcBlockchain { client, capabilities, is_descriptors, sync_params: config.sync_params.clone().unwrap_or_default(), }) } } /// return the wallets available in default wallet directory //TODO use bitcoincore_rpc method when PR #179 lands fn list_wallet_dir(client: &Client) -> Result, Error> { #[derive(Deserialize)] struct Name { name: String, } #[derive(Deserialize)] struct CallResult { wallets: Vec, } let result: CallResult = client.call("listwalletdir", &[])?; Ok(result.wallets.into_iter().map(|n| n.name).collect()) } /// Represents the state of the [`crate::database::Database`]. struct DbState<'a, D> { db: &'a D, params: &'a RpcSyncParams, prog: &'a dyn Progress, ext_spks: Vec