2021-05-17 17:20:32 +02:00
|
|
|
// Bitcoin Dev Kit
|
|
|
|
// Written in 2021 by Riccardo Casatta <riccardo@casatta.it>
|
|
|
|
//
|
|
|
|
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
|
|
|
//
|
|
|
|
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
|
|
|
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
|
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, 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
|
|
|
|
//!
|
2021-07-02 10:07:44 +02:00
|
|
|
//! This is an **EXPERIMENTAL** feature, API and other major changes are expected.
|
|
|
|
//!
|
2021-05-17 17:20:32 +02:00
|
|
|
//! ## Example
|
|
|
|
//!
|
|
|
|
//! ```no_run
|
2021-06-01 14:09:00 +02:00
|
|
|
//! # use bdk::blockchain::{RpcConfig, RpcBlockchain, ConfigurableBlockchain, rpc::Auth};
|
2021-05-17 17:20:32 +02:00
|
|
|
//! let config = RpcConfig {
|
2021-06-04 15:05:35 +02:00
|
|
|
//! url: "127.0.0.1:18332".to_string(),
|
2021-06-01 14:09:00 +02:00
|
|
|
//! auth: Auth::Cookie {
|
|
|
|
//! file: "/home/user/.bitcoin/.cookie".into(),
|
|
|
|
//! },
|
2021-06-04 15:05:35 +02:00
|
|
|
//! network: bdk::bitcoin::Network::Testnet,
|
|
|
|
//! wallet_name: "wallet_name".to_string(),
|
|
|
|
//! skip_blocks: None,
|
|
|
|
//! };
|
2021-05-17 17:20:32 +02:00
|
|
|
//! let blockchain = RpcBlockchain::from_config(&config);
|
|
|
|
//! ```
|
|
|
|
|
|
|
|
use crate::bitcoin::consensus::deserialize;
|
2022-06-07 13:14:52 +02:00
|
|
|
use crate::bitcoin::hashes::hex::ToHex;
|
2021-05-17 17:20:32 +02:00
|
|
|
use crate::bitcoin::{Address, Network, OutPoint, Transaction, TxOut, Txid};
|
2022-02-23 10:38:35 +11:00
|
|
|
use crate::blockchain::*;
|
2021-05-17 17:20:32 +02:00
|
|
|
use crate::database::{BatchDatabase, DatabaseUtils};
|
2022-06-07 13:14:52 +02:00
|
|
|
use crate::descriptor::get_checksum;
|
2021-11-03 16:05:30 +00:00
|
|
|
use crate::{BlockTime, Error, FeeRate, KeychainKind, LocalUtxo, TransactionDetails};
|
2021-10-27 13:52:18 -07:00
|
|
|
use bitcoincore_rpc::json::{
|
2021-05-17 17:20:32 +02:00
|
|
|
GetAddressInfoResultLabel, ImportMultiOptions, ImportMultiRequest,
|
|
|
|
ImportMultiRequestScriptPubkey, ImportMultiRescanSince,
|
|
|
|
};
|
2022-06-07 13:14:52 +02:00
|
|
|
use bitcoincore_rpc::jsonrpc::serde_json::{json, Value};
|
2021-10-27 13:52:18 -07:00
|
|
|
use bitcoincore_rpc::Auth as RpcAuth;
|
|
|
|
use bitcoincore_rpc::{Client, RpcApi};
|
2021-05-17 17:20:32 +02:00
|
|
|
use log::debug;
|
2021-07-26 15:55:40 +02:00
|
|
|
use serde::{Deserialize, Serialize};
|
2021-05-17 17:20:32 +02:00
|
|
|
use std::collections::{HashMap, HashSet};
|
2021-07-26 15:55:40 +02:00
|
|
|
use std::path::PathBuf;
|
2021-05-17 17:20:32 +02:00
|
|
|
use std::str::FromStr;
|
|
|
|
|
|
|
|
/// 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,
|
2022-06-07 13:14:52 +02:00
|
|
|
/// Whether the wallet is a "descriptor" or "legacy" wallet in Core
|
|
|
|
is_descriptors: bool,
|
2021-05-17 17:20:32 +02:00
|
|
|
/// Blockchain capabilities, cached here at startup
|
|
|
|
capabilities: HashSet<Capability>,
|
|
|
|
/// Skip this many blocks of the blockchain at the first rescan, if None the rescan is done from the genesis block
|
|
|
|
skip_blocks: Option<u32>,
|
|
|
|
|
|
|
|
/// This is a fixed Address used as a hack key to store information on the node
|
2021-06-03 11:06:24 +02:00
|
|
|
_storage_address: Address,
|
2021-05-17 17:20:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/// RpcBlockchain configuration options
|
2021-06-01 14:09:00 +02:00
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
2021-05-17 17:20:32 +02:00
|
|
|
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,
|
2021-11-17 16:26:43 +01:00
|
|
|
/// The wallet name in the bitcoin node, consider using [crate::wallet::wallet_name_from_descriptor] for this
|
2021-05-17 17:20:32 +02:00
|
|
|
pub wallet_name: String,
|
|
|
|
/// Skip this many blocks of the blockchain at the first rescan, if None the rescan is done from the genesis block
|
|
|
|
pub skip_blocks: Option<u32>,
|
|
|
|
}
|
|
|
|
|
2021-10-27 13:52:18 -07:00
|
|
|
/// This struct is equivalent to [bitcoincore_rpc::Auth] but it implements [serde::Serialize]
|
2021-07-26 15:55:40 +02:00
|
|
|
/// To be removed once upstream equivalent is implementing Serialize (json serialization format
|
2021-09-30 16:11:42 -07:00
|
|
|
/// should be the same), see [rust-bitcoincore-rpc/pull/181](https://github.com/rust-bitcoin/rust-bitcoincore-rpc/pull/181)
|
2021-07-26 15:55:40 +02:00
|
|
|
#[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<Auth> 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),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-17 17:20:32 +02:00
|
|
|
impl RpcBlockchain {
|
|
|
|
fn get_node_synced_height(&self) -> Result<u32, Error> {
|
2021-06-03 11:06:24 +02:00
|
|
|
let info = self.client.get_address_info(&self._storage_address)?;
|
2021-05-17 17:20:32 +02:00
|
|
|
if let Some(GetAddressInfoResultLabel::Simple(label)) = info.labels.first() {
|
|
|
|
Ok(label
|
|
|
|
.parse::<u32>()
|
|
|
|
.unwrap_or_else(|_| self.skip_blocks.unwrap_or(0)))
|
|
|
|
} else {
|
|
|
|
Ok(self.skip_blocks.unwrap_or(0))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Set the synced height in the core node by using a label of a fixed address so that
|
|
|
|
/// another client with the same descriptor doesn't rescan the blockchain
|
|
|
|
fn set_node_synced_height(&self, height: u32) -> Result<(), Error> {
|
|
|
|
Ok(self
|
|
|
|
.client
|
2021-06-03 11:06:24 +02:00
|
|
|
.set_label(&self._storage_address, &height.to_string())?)
|
2021-05-17 17:20:32 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Blockchain for RpcBlockchain {
|
|
|
|
fn get_capabilities(&self) -> HashSet<Capability> {
|
|
|
|
self.capabilities.clone()
|
|
|
|
}
|
|
|
|
|
2022-01-26 15:17:48 +11:00
|
|
|
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
|
|
|
Ok(self.client.send_raw_transaction(tx).map(|_| ())?)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
|
|
|
|
let sat_per_kb = self
|
|
|
|
.client
|
|
|
|
.estimate_smart_fee(target as u16, None)?
|
|
|
|
.fee_rate
|
|
|
|
.ok_or(Error::FeeRateUnavailable)?
|
|
|
|
.as_sat() as f64;
|
|
|
|
|
|
|
|
Ok(FeeRate::from_sat_per_vb((sat_per_kb / 1000f64) as f32))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-23 10:38:35 +11:00
|
|
|
impl GetTx for RpcBlockchain {
|
|
|
|
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
|
|
|
Ok(Some(self.client.get_raw_transaction(txid, None)?))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-26 15:17:48 +11:00
|
|
|
impl GetHeight for RpcBlockchain {
|
|
|
|
fn get_height(&self) -> Result<u32, Error> {
|
|
|
|
Ok(self.client.get_blockchain_info().map(|i| i.blocks as u32)?)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-16 20:42:02 +01:00
|
|
|
impl GetBlockHash for RpcBlockchain {
|
|
|
|
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
|
|
|
|
Ok(self.client.get_block_hash(height)?)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-26 15:17:48 +11:00
|
|
|
impl WalletSync for RpcBlockchain {
|
2022-01-27 16:52:53 +11:00
|
|
|
fn wallet_setup<D: BatchDatabase>(
|
2021-05-17 17:20:32 +02:00
|
|
|
&self,
|
|
|
|
database: &mut D,
|
2022-01-27 16:52:53 +11:00
|
|
|
progress_update: Box<dyn Progress>,
|
2021-05-17 17:20:32 +02:00
|
|
|
) -> Result<(), Error> {
|
|
|
|
let mut scripts_pubkeys = database.iter_script_pubkeys(Some(KeychainKind::External))?;
|
|
|
|
scripts_pubkeys.extend(database.iter_script_pubkeys(Some(KeychainKind::Internal))?);
|
|
|
|
debug!(
|
|
|
|
"importing {} script_pubkeys (some maybe already imported)",
|
|
|
|
scripts_pubkeys.len()
|
|
|
|
);
|
2022-06-07 13:14:52 +02:00
|
|
|
|
|
|
|
if self.is_descriptors {
|
|
|
|
// Core still doesn't support complex descriptors like BDK, but when the wallet type is
|
|
|
|
// "descriptors" we should import individual addresses using `importdescriptors` rather
|
|
|
|
// than `importmulti`, using the `raw()` descriptor which allows us to specify an
|
|
|
|
// arbitrary script
|
|
|
|
let requests = Value::Array(
|
|
|
|
scripts_pubkeys
|
|
|
|
.iter()
|
|
|
|
.map(|s| {
|
|
|
|
let desc = format!("raw({})", s.to_hex());
|
|
|
|
json!({
|
|
|
|
"timestamp": "now",
|
|
|
|
"desc": format!("{}#{}", desc, get_checksum(&desc).unwrap()),
|
|
|
|
})
|
|
|
|
})
|
|
|
|
.collect(),
|
|
|
|
);
|
|
|
|
|
|
|
|
let res: Vec<Value> = self.client.call("importdescriptors", &[requests])?;
|
|
|
|
res.into_iter()
|
|
|
|
.map(|v| match v["success"].as_bool() {
|
|
|
|
Some(true) => Ok(()),
|
|
|
|
Some(false) => Err(Error::Generic(
|
|
|
|
v["error"]["message"]
|
|
|
|
.as_str()
|
|
|
|
.unwrap_or("Unknown error")
|
|
|
|
.to_string(),
|
|
|
|
)),
|
|
|
|
_ => Err(Error::Generic("Unexpected response from Core".to_string())),
|
|
|
|
})
|
|
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
|
|
} else {
|
|
|
|
let requests: Vec<_> = scripts_pubkeys
|
|
|
|
.iter()
|
|
|
|
.map(|s| ImportMultiRequest {
|
|
|
|
timestamp: ImportMultiRescanSince::Timestamp(0),
|
|
|
|
script_pubkey: Some(ImportMultiRequestScriptPubkey::Script(s)),
|
|
|
|
watchonly: Some(true),
|
|
|
|
..Default::default()
|
|
|
|
})
|
|
|
|
.collect();
|
|
|
|
let options = ImportMultiOptions {
|
|
|
|
rescan: Some(false),
|
|
|
|
};
|
|
|
|
self.client.import_multi(&requests, Some(&options))?;
|
|
|
|
}
|
2021-05-17 17:20:32 +02:00
|
|
|
|
2021-09-13 14:52:22 +02:00
|
|
|
loop {
|
|
|
|
let current_height = self.get_height()?;
|
2021-05-17 17:20:32 +02:00
|
|
|
|
2021-09-13 14:52:22 +02:00
|
|
|
// min because block invalidate may cause height to go down
|
|
|
|
let node_synced = self.get_node_synced_height()?.min(current_height);
|
2021-05-17 17:20:32 +02:00
|
|
|
|
2021-09-13 14:52:22 +02:00
|
|
|
let sync_up_to = node_synced.saturating_add(10_000).min(current_height);
|
|
|
|
|
|
|
|
debug!("rescan_blockchain from:{} to:{}", node_synced, sync_up_to);
|
|
|
|
self.client
|
|
|
|
.rescan_blockchain(Some(node_synced as usize), Some(sync_up_to as usize))?;
|
|
|
|
progress_update.update((sync_up_to as f32) / (current_height as f32), None)?;
|
2021-05-17 17:20:32 +02:00
|
|
|
|
2021-09-13 14:52:22 +02:00
|
|
|
self.set_node_synced_height(sync_up_to)?;
|
|
|
|
|
|
|
|
if sync_up_to == current_height {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2021-06-03 15:10:31 +02:00
|
|
|
|
2022-01-26 15:17:48 +11:00
|
|
|
self.wallet_sync(database, progress_update)
|
2021-06-03 15:10:31 +02:00
|
|
|
}
|
|
|
|
|
2022-01-27 16:52:53 +11:00
|
|
|
fn wallet_sync<D: BatchDatabase>(
|
2021-06-03 15:10:31 +02:00
|
|
|
&self,
|
|
|
|
db: &mut D,
|
2022-01-27 16:52:53 +11:00
|
|
|
_progress_update: Box<dyn Progress>,
|
2021-06-03 15:10:31 +02:00
|
|
|
) -> Result<(), Error> {
|
|
|
|
let mut indexes = HashMap::new();
|
|
|
|
for keykind in &[KeychainKind::External, KeychainKind::Internal] {
|
|
|
|
indexes.insert(*keykind, db.get_last_index(*keykind)?.unwrap_or(0));
|
|
|
|
}
|
|
|
|
|
2021-05-17 17:20:32 +02:00
|
|
|
let mut known_txs: HashMap<_, _> = db
|
|
|
|
.iter_txs(true)?
|
|
|
|
.into_iter()
|
|
|
|
.map(|tx| (tx.txid, tx))
|
|
|
|
.collect();
|
|
|
|
let known_utxos: HashSet<_> = db.iter_utxos()?.into_iter().collect();
|
|
|
|
|
|
|
|
//TODO list_since_blocks would be more efficient
|
|
|
|
let current_utxo = self
|
|
|
|
.client
|
|
|
|
.list_unspent(Some(0), None, None, Some(true), None)?;
|
|
|
|
debug!("current_utxo len {}", current_utxo.len());
|
|
|
|
|
|
|
|
//TODO supported up to 1_000 txs, should use since_blocks or do paging
|
|
|
|
let list_txs = self
|
|
|
|
.client
|
|
|
|
.list_transactions(None, Some(1_000), None, Some(true))?;
|
|
|
|
let mut list_txs_ids = HashSet::new();
|
|
|
|
|
|
|
|
for tx_result in list_txs.iter().filter(|t| {
|
2022-03-09 16:15:34 +01:00
|
|
|
// list_txs returns all conflicting txs, we want to
|
2021-05-17 17:20:32 +02:00
|
|
|
// filter out replaced tx => unconfirmed and not in the mempool
|
|
|
|
t.info.confirmations > 0 || self.client.get_mempool_entry(&t.info.txid).is_ok()
|
|
|
|
}) {
|
|
|
|
let txid = tx_result.info.txid;
|
|
|
|
list_txs_ids.insert(txid);
|
|
|
|
if let Some(mut known_tx) = known_txs.get_mut(&txid) {
|
2021-06-12 15:01:44 +02:00
|
|
|
let confirmation_time =
|
2021-11-03 16:05:30 +00:00
|
|
|
BlockTime::new(tx_result.info.blockheight, tx_result.info.blocktime);
|
2021-06-12 15:01:44 +02:00
|
|
|
if confirmation_time != known_tx.confirmation_time {
|
2021-05-17 17:20:32 +02:00
|
|
|
// reorg may change tx height
|
|
|
|
debug!(
|
2021-06-12 15:01:44 +02:00
|
|
|
"updating tx({}) confirmation time to: {:?}",
|
|
|
|
txid, confirmation_time
|
2021-05-17 17:20:32 +02:00
|
|
|
);
|
2021-06-12 15:01:44 +02:00
|
|
|
known_tx.confirmation_time = confirmation_time;
|
2021-10-21 18:35:03 +02:00
|
|
|
db.set_tx(known_tx)?;
|
2021-05-17 17:20:32 +02:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
//TODO check there is already the raw tx in db?
|
|
|
|
let tx_result = self.client.get_transaction(&txid, Some(true))?;
|
|
|
|
let tx: Transaction = deserialize(&tx_result.hex)?;
|
|
|
|
let mut received = 0u64;
|
|
|
|
let mut sent = 0u64;
|
|
|
|
for output in tx.output.iter() {
|
|
|
|
if let Ok(Some((kind, index))) =
|
|
|
|
db.get_path_from_script_pubkey(&output.script_pubkey)
|
|
|
|
{
|
|
|
|
if index > *indexes.get(&kind).unwrap() {
|
|
|
|
indexes.insert(kind, index);
|
|
|
|
}
|
|
|
|
received += output.value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for input in tx.input.iter() {
|
|
|
|
if let Some(previous_output) = db.get_previous_output(&input.previous_output)? {
|
2022-02-17 23:39:11 +01:00
|
|
|
if db.is_mine(&previous_output.script_pubkey)? {
|
|
|
|
sent += previous_output.value;
|
|
|
|
}
|
2021-05-17 17:20:32 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let td = TransactionDetails {
|
|
|
|
transaction: Some(tx),
|
|
|
|
txid: tx_result.info.txid,
|
2021-11-03 16:05:30 +00:00
|
|
|
confirmation_time: BlockTime::new(
|
2021-06-12 15:01:44 +02:00
|
|
|
tx_result.info.blockheight,
|
|
|
|
tx_result.info.blocktime,
|
|
|
|
),
|
2021-05-17 17:20:32 +02:00
|
|
|
received,
|
|
|
|
sent,
|
2022-07-03 14:32:05 +08:00
|
|
|
fee: tx_result.fee.map(|f| f.as_sat().unsigned_abs()),
|
2021-05-17 17:20:32 +02:00
|
|
|
};
|
|
|
|
debug!(
|
|
|
|
"saving tx: {} tx_result.fee:{:?} td.fees:{:?}",
|
2021-06-12 15:01:44 +02:00
|
|
|
td.txid, tx_result.fee, td.fee
|
2021-05-17 17:20:32 +02:00
|
|
|
);
|
|
|
|
db.set_tx(&td)?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for known_txid in known_txs.keys() {
|
|
|
|
if !list_txs_ids.contains(known_txid) {
|
|
|
|
debug!("removing tx: {}", known_txid);
|
|
|
|
db.del_tx(known_txid, false)?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-24 20:32:47 +11:00
|
|
|
// Filter out trasactions that are for script pubkeys that aren't in this wallet.
|
|
|
|
let current_utxos = current_utxo
|
2021-05-17 17:20:32 +02:00
|
|
|
.into_iter()
|
2022-02-24 20:32:47 +11:00
|
|
|
.filter_map(
|
|
|
|
|u| match db.get_path_from_script_pubkey(&u.script_pub_key) {
|
|
|
|
Err(e) => Some(Err(e)),
|
|
|
|
Ok(None) => None,
|
|
|
|
Ok(Some(path)) => Some(Ok(LocalUtxo {
|
|
|
|
outpoint: OutPoint::new(u.txid, u.vout),
|
|
|
|
keychain: path.0,
|
|
|
|
txout: TxOut {
|
|
|
|
value: u.amount.as_sat(),
|
|
|
|
script_pubkey: u.script_pub_key,
|
|
|
|
},
|
2022-03-09 16:15:34 +01:00
|
|
|
is_spent: false,
|
2022-02-24 20:32:47 +11:00
|
|
|
})),
|
|
|
|
},
|
|
|
|
)
|
|
|
|
.collect::<Result<HashSet<_>, Error>>()?;
|
2021-05-17 17:20:32 +02:00
|
|
|
|
|
|
|
let spent: HashSet<_> = known_utxos.difference(¤t_utxos).collect();
|
2022-03-09 16:15:34 +01:00
|
|
|
for utxo in spent {
|
|
|
|
debug!("setting as spent utxo: {:?}", utxo);
|
|
|
|
let mut spent_utxo = utxo.clone();
|
|
|
|
spent_utxo.is_spent = true;
|
|
|
|
db.set_utxo(&spent_utxo)?;
|
2021-05-17 17:20:32 +02:00
|
|
|
}
|
|
|
|
let received: HashSet<_> = current_utxos.difference(&known_utxos).collect();
|
2022-03-09 16:15:34 +01:00
|
|
|
for utxo in received {
|
|
|
|
debug!("adding utxo: {:?}", utxo);
|
|
|
|
db.set_utxo(utxo)?;
|
2021-05-17 17:20:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
for (keykind, index) in indexes {
|
|
|
|
debug!("{:?} max {}", keykind, index);
|
|
|
|
db.set_last_index(keykind, index)?;
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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<Self, Error> {
|
|
|
|
let wallet_name = config.wallet_name.clone();
|
|
|
|
let wallet_url = format!("{}/wallet/{}", config.url, &wallet_name);
|
|
|
|
debug!("connecting to {} auth:{:?}", wallet_url, config.auth);
|
|
|
|
|
2021-08-17 17:52:07 +02:00
|
|
|
let client = Client::new(wallet_url.as_str(), config.auth.clone().into())?;
|
2022-06-07 13:14:52 +02:00
|
|
|
let rpc_version = client.version()?;
|
|
|
|
|
2021-05-17 17:20:32 +02:00
|
|
|
let loaded_wallets = client.list_wallets()?;
|
|
|
|
if loaded_wallets.contains(&wallet_name) {
|
|
|
|
debug!("wallet already loaded {:?}", wallet_name);
|
2022-06-07 13:14:52 +02:00
|
|
|
} else if list_wallet_dir(&client)?.contains(&wallet_name) {
|
|
|
|
client.load_wallet(&wallet_name)?;
|
|
|
|
debug!("wallet loaded {:?}", wallet_name);
|
2021-05-17 17:20:32 +02:00
|
|
|
} else {
|
2022-06-07 13:14:52 +02:00
|
|
|
// pre-0.21 use legacy wallets
|
|
|
|
if rpc_version < 210_000 {
|
2021-05-17 17:20:32 +02:00
|
|
|
client.create_wallet(&wallet_name, Some(true), None, None, None)?;
|
2022-06-07 13:14:52 +02:00
|
|
|
} else {
|
|
|
|
// TODO: move back to api call when https://github.com/rust-bitcoin/rust-bitcoincore-rpc/issues/225 is closed
|
|
|
|
let args = [
|
|
|
|
Value::String(wallet_name.clone()),
|
|
|
|
Value::Bool(true),
|
|
|
|
Value::Bool(false),
|
|
|
|
Value::Null,
|
|
|
|
Value::Bool(false),
|
|
|
|
Value::Bool(true),
|
|
|
|
];
|
|
|
|
let _: Value = client.call("createwallet", &args)?;
|
2021-05-17 17:20:32 +02:00
|
|
|
}
|
2022-06-07 13:14:52 +02:00
|
|
|
|
|
|
|
debug!("wallet created {:?}", wallet_name);
|
2021-05-17 17:20:32 +02:00
|
|
|
}
|
|
|
|
|
2022-06-07 13:14:52 +02:00
|
|
|
let is_descriptors = is_wallet_descriptor(&client)?;
|
|
|
|
|
2021-05-17 17:20:32 +02:00
|
|
|
let blockchain_info = client.get_blockchain_info()?;
|
|
|
|
let network = match blockchain_info.chain.as_str() {
|
|
|
|
"main" => Network::Bitcoin,
|
|
|
|
"test" => Network::Testnet,
|
|
|
|
"regtest" => Network::Regtest,
|
2021-06-01 14:15:46 +02:00
|
|
|
"signet" => Network::Signet,
|
2021-05-17 17:20:32 +02:00
|
|
|
_ => 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<String, Value> = client.call("getindexinfo", &[]).unwrap();
|
|
|
|
if info.contains_key("txindex") {
|
|
|
|
capabilities.insert(Capability::GetAnyTx);
|
|
|
|
capabilities.insert(Capability::AccurateFees);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// this is just a fixed address used only to store a label containing the synced height in the node
|
2021-06-03 11:06:24 +02:00
|
|
|
let mut storage_address =
|
|
|
|
Address::from_str("bc1qst0rewf0wm4kw6qn6kv0e5tc56nkf9yhcxlhqv").unwrap();
|
|
|
|
storage_address.network = network;
|
2021-05-17 17:20:32 +02:00
|
|
|
|
|
|
|
Ok(RpcBlockchain {
|
|
|
|
client,
|
|
|
|
capabilities,
|
2022-06-07 13:14:52 +02:00
|
|
|
is_descriptors,
|
2021-06-03 11:06:24 +02:00
|
|
|
_storage_address: storage_address,
|
2021-05-17 17:20:32 +02:00
|
|
|
skip_blocks: config.skip_blocks,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// return the wallets available in default wallet directory
|
|
|
|
//TODO use bitcoincore_rpc method when PR #179 lands
|
|
|
|
fn list_wallet_dir(client: &Client) -> Result<Vec<String>, Error> {
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
struct Name {
|
|
|
|
name: String,
|
|
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
2021-06-01 14:17:37 +02:00
|
|
|
struct CallResult {
|
2021-05-17 17:20:32 +02:00
|
|
|
wallets: Vec<Name>,
|
|
|
|
}
|
|
|
|
|
2021-06-01 14:17:37 +02:00
|
|
|
let result: CallResult = client.call("listwalletdir", &[])?;
|
2021-05-17 17:20:32 +02:00
|
|
|
Ok(result.wallets.into_iter().map(|n| n.name).collect())
|
|
|
|
}
|
|
|
|
|
2022-06-07 13:14:52 +02:00
|
|
|
/// Returns whether a wallet is legacy or descriptors by calling `getwalletinfo`.
|
|
|
|
///
|
|
|
|
/// This API is mapped by bitcoincore_rpc, but it doesn't have the fields we need (either
|
|
|
|
/// "descriptors" or "format") so we have to call the RPC manually
|
|
|
|
fn is_wallet_descriptor(client: &Client) -> Result<bool, Error> {
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
struct CallResult {
|
|
|
|
descriptors: Option<bool>,
|
|
|
|
}
|
|
|
|
|
|
|
|
let result: CallResult = client.call("getwalletinfo", &[])?;
|
|
|
|
Ok(result.descriptors.unwrap_or(false))
|
|
|
|
}
|
|
|
|
|
2022-03-15 10:48:00 +01:00
|
|
|
/// Factory of [`RpcBlockchain`] instances, implements [`BlockchainFactory`]
|
|
|
|
///
|
|
|
|
/// Internally caches the node url and authentication params and allows getting many different [`RpcBlockchain`]
|
|
|
|
/// objects for different wallet names and with different rescan heights.
|
|
|
|
///
|
|
|
|
/// ## Example
|
|
|
|
///
|
|
|
|
/// ```no_run
|
|
|
|
/// # use bdk::bitcoin::Network;
|
|
|
|
/// # use bdk::blockchain::BlockchainFactory;
|
|
|
|
/// # use bdk::blockchain::rpc::{Auth, RpcBlockchainFactory};
|
|
|
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
|
/// let factory = RpcBlockchainFactory {
|
|
|
|
/// url: "http://127.0.0.1:18332".to_string(),
|
|
|
|
/// auth: Auth::Cookie {
|
|
|
|
/// file: "/home/user/.bitcoin/.cookie".into(),
|
|
|
|
/// },
|
|
|
|
/// network: Network::Testnet,
|
|
|
|
/// wallet_name_prefix: Some("prefix-".to_string()),
|
|
|
|
/// default_skip_blocks: 100_000,
|
|
|
|
/// };
|
|
|
|
/// let main_wallet_blockchain = factory.build("main_wallet", Some(200_000))?;
|
|
|
|
/// # Ok(())
|
|
|
|
/// # }
|
|
|
|
/// ```
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
pub struct RpcBlockchainFactory {
|
|
|
|
/// 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 optional prefix used to build the full wallet name for blockchains
|
|
|
|
pub wallet_name_prefix: Option<String>,
|
|
|
|
/// Default number of blocks to skip which will be inherited by blockchain unless overridden
|
|
|
|
pub default_skip_blocks: u32,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl BlockchainFactory for RpcBlockchainFactory {
|
|
|
|
type Inner = RpcBlockchain;
|
|
|
|
|
|
|
|
fn build(
|
|
|
|
&self,
|
|
|
|
checksum: &str,
|
|
|
|
override_skip_blocks: Option<u32>,
|
|
|
|
) -> Result<Self::Inner, Error> {
|
|
|
|
RpcBlockchain::from_config(&RpcConfig {
|
|
|
|
url: self.url.clone(),
|
|
|
|
auth: self.auth.clone(),
|
|
|
|
network: self.network,
|
|
|
|
wallet_name: format!(
|
|
|
|
"{}{}",
|
|
|
|
self.wallet_name_prefix.as_ref().unwrap_or(&String::new()),
|
|
|
|
checksum
|
|
|
|
),
|
|
|
|
skip_blocks: Some(override_skip_blocks.unwrap_or(self.default_skip_blocks)),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-17 13:48:46 +02:00
|
|
|
#[cfg(test)]
|
2022-06-07 13:14:52 +02:00
|
|
|
#[cfg(any(feature = "test-rpc", feature = "test-rpc-legacy"))]
|
2022-03-15 10:48:00 +01:00
|
|
|
mod test {
|
|
|
|
use super::*;
|
|
|
|
use crate::testutils::blockchain_tests::TestClient;
|
|
|
|
|
|
|
|
use bitcoin::Network;
|
|
|
|
use bitcoincore_rpc::RpcApi;
|
|
|
|
|
|
|
|
crate::bdk_blockchain_tests! {
|
|
|
|
fn test_instance(test_client: &TestClient) -> RpcBlockchain {
|
|
|
|
let config = RpcConfig {
|
|
|
|
url: test_client.bitcoind.rpc_url(),
|
|
|
|
auth: Auth::Cookie { file: test_client.bitcoind.params.cookie_file.clone() },
|
|
|
|
network: Network::Regtest,
|
2022-06-07 13:14:52 +02:00
|
|
|
wallet_name: format!("client-wallet-test-{}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos() ),
|
2022-03-15 10:48:00 +01:00
|
|
|
skip_blocks: None,
|
|
|
|
};
|
|
|
|
RpcBlockchain::from_config(&config).unwrap()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_factory() -> (TestClient, RpcBlockchainFactory) {
|
|
|
|
let test_client = TestClient::default();
|
2021-05-17 17:20:32 +02:00
|
|
|
|
2022-03-15 10:48:00 +01:00
|
|
|
let factory = RpcBlockchainFactory {
|
2021-06-17 13:48:46 +02:00
|
|
|
url: test_client.bitcoind.rpc_url(),
|
2022-03-15 10:48:00 +01:00
|
|
|
auth: Auth::Cookie {
|
|
|
|
file: test_client.bitcoind.params.cookie_file.clone(),
|
|
|
|
},
|
2021-05-17 17:20:32 +02:00
|
|
|
network: Network::Regtest,
|
2022-03-15 10:48:00 +01:00
|
|
|
wallet_name_prefix: Some("prefix-".into()),
|
|
|
|
default_skip_blocks: 0,
|
2021-05-17 17:20:32 +02:00
|
|
|
};
|
2022-03-15 10:48:00 +01:00
|
|
|
|
|
|
|
(test_client, factory)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_rpc_blockchain_factory() {
|
|
|
|
let (_test_client, factory) = get_factory();
|
|
|
|
|
|
|
|
let a = factory.build("aaaaaa", None).unwrap();
|
|
|
|
assert_eq!(a.skip_blocks, Some(0));
|
|
|
|
assert_eq!(
|
|
|
|
a.client
|
|
|
|
.get_wallet_info()
|
|
|
|
.expect("Node connection isn't working")
|
|
|
|
.wallet_name,
|
|
|
|
"prefix-aaaaaa"
|
|
|
|
);
|
|
|
|
|
|
|
|
let b = factory.build("bbbbbb", Some(100)).unwrap();
|
|
|
|
assert_eq!(b.skip_blocks, Some(100));
|
|
|
|
assert_eq!(
|
|
|
|
b.client
|
|
|
|
.get_wallet_info()
|
|
|
|
.expect("Node connection isn't working")
|
|
|
|
.wallet_name,
|
|
|
|
"prefix-bbbbbb"
|
|
|
|
);
|
2021-05-17 17:20:32 +02:00
|
|
|
}
|
|
|
|
}
|