Skip to content

Commit a662ffb

Browse files
committed
Pruned RPC Blockchain factory with examples
1 parent 85c75a8 commit a662ffb

File tree

1 file changed

+205
-17
lines changed

1 file changed

+205
-17
lines changed

src/blockchain/pruned_rpc.rs

Lines changed: 205 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,24 @@ use crate::bitcoin::{Address, Network, Transaction, Txid};
1010
use crate::blockchain::*;
1111
use crate::database::BatchDatabase;
1212
use crate::{Error, FeeRate};
13-
use bitcoincore_rpc::Auth as RpcAuth;
13+
use bitcoincore_rpc::jsonrpc::serde_json::Value;
14+
use bitcoincore_rpc::Auth as PrunedRpcAuth;
1415
use bitcoincore_rpc::{Client, RpcApi};
16+
use log::debug;
1517
use serde::{Deserialize, Serialize};
16-
use std::collections::HashSet;
18+
use std::collections::{HashMap, HashSet};
1719
use std::path::PathBuf;
20+
use std::str::FromStr;
1821

1922
/// The main struct for Pruned RPC backend implementing the [crate::blockchain::Blockchain] trait
2023
#[derive(Debug)]
2124
pub struct PrunedRpcBlockchain {
2225
/// Rpc client to the node, includes the wallet name
2326
client: Client,
24-
/// Whether the wallet is a "descriptor" or "legacy" wallet in Core
25-
_is_descriptors: bool,
2627
/// Blockchain capabilities, cached here at startup
2728
capabilities: HashSet<Capability>,
29+
// Whether the wallet is a "descriptor" or "legacy" in core
30+
_is_descriptor: bool,
2831
/// This is a fixed Address used as a hack key to store information on the node
2932
_storage_address: Address,
3033
}
@@ -66,12 +69,12 @@ pub enum Auth {
6669
},
6770
}
6871

69-
impl From<Auth> for RpcAuth {
72+
impl From<Auth> for PrunedRpcAuth {
7073
fn from(auth: Auth) -> Self {
7174
match auth {
72-
Auth::None => RpcAuth::None,
73-
Auth::UserPass { username, password } => RpcAuth::UserPass(username, password),
74-
Auth::Cookie { file } => RpcAuth::CookieFile(file),
75+
Auth::None => PrunedRpcAuth::None,
76+
Auth::UserPass { username, password } => PrunedRpcAuth::UserPass(username, password),
77+
Auth::Cookie { file } => PrunedRpcAuth::CookieFile(file),
7578
}
7679
}
7780
}
@@ -114,15 +117,15 @@ impl GetHeight for PrunedRpcBlockchain {
114117
impl WalletSync for PrunedRpcBlockchain {
115118
fn wallet_setup<D: BatchDatabase>(
116119
&self,
117-
_database: &mut D,
118-
_progress_update: Box<dyn Progress>,
120+
database: &mut D,
121+
progress_update: Box<dyn Progress>,
119122
) -> Result<(), Error> {
120123
todo!()
121124
}
122125

123126
fn wallet_sync<D: BatchDatabase>(
124127
&self,
125-
_database: &mut D,
128+
db: &mut D,
126129
_progress_update: Box<dyn Progress>,
127130
) -> Result<(), Error> {
128131
todo!()
@@ -132,30 +135,215 @@ impl WalletSync for PrunedRpcBlockchain {
132135
impl ConfigurableBlockchain for PrunedRpcBlockchain {
133136
type Config = PrunedRpcConfig;
134137

135-
fn from_config(_config: &Self::Config) -> Result<Self, Error> {
136-
todo!()
138+
fn from_config(config: &Self::Config) -> Result<Self, Error> {
139+
let wallet_name = config.wallet_name.clone();
140+
let wallet_uri = format!("{}/wallet/{}", config.url, &wallet_name);
141+
debug!("connecting to {} auth:{:?}", wallet_uri, config.auth);
142+
143+
let client = Client::new(wallet_uri.as_str(), config.auth.clone().into())?;
144+
let rpc_version = client.version()?;
145+
146+
let loaded_wallets = client.list_wallets()?;
147+
if loaded_wallets.contains(&wallet_name) {
148+
debug!("wallet loaded {:?}", wallet_name);
149+
} else if list_wallet_dir(&client)?.contains(&wallet_name) {
150+
client.load_wallet(&wallet_name)?;
151+
debug!("wallet loaded {:?}", wallet_name);
152+
} else {
153+
if rpc_version < 210_000 {
154+
client.create_wallet(&wallet_name, Some(true), None, None, None)?;
155+
} else {
156+
// TODO: move back to api call when https://github.com/rust-bitcoin/rust-bitcoincore-rpc/issues/225 is closed
157+
let args = [
158+
Value::String(wallet_name.clone()),
159+
Value::Bool(true),
160+
Value::Bool(false),
161+
Value::Null,
162+
Value::Bool(false),
163+
Value::Bool(true),
164+
];
165+
let _: Value = client.call("createwallet", &args)?;
166+
}
167+
168+
debug!("wallet created {:?}", wallet_name);
169+
}
170+
171+
let is_descriptors = is_wallet_descriptor(&client)?;
172+
let blockchain_info = client.get_blockchain_info()?;
173+
let network = match blockchain_info.chain.as_str() {
174+
"main" => Network::Bitcoin,
175+
"test" => Network::Testnet,
176+
"regtest" => Network::Regtest,
177+
"signet" => Network::Signet,
178+
_ => return Err(Error::Generic("Invalid network".to_string())),
179+
};
180+
if network != config.network {
181+
return Err(Error::InvalidNetwork {
182+
requested: config.network,
183+
found: network,
184+
});
185+
}
186+
let mut capabilities: HashSet<_> = vec![Capability::FullHistory].into_iter().collect();
187+
188+
if rpc_version >= 210_000 {
189+
let info: HashMap<String, Value> = client.call("getindexinfo", &[]).unwrap();
190+
if info.contains_key("txindex") {
191+
capabilities.insert(Capability::GetAnyTx);
192+
capabilities.insert(Capability::AccurateFees);
193+
}
194+
}
195+
196+
// fixed address used only to store a label containing the synxed height in the node. Using
197+
// the same address as that used in RpcBlockchain
198+
let mut storage_address =
199+
Address::from_str("bc1qst0rewf0wm4kw6qn6kv0e5tc56nkf9yhcxlhqv").unwrap();
200+
storage_address.network = network;
201+
202+
Ok(PrunedRpcBlockchain {
203+
client,
204+
capabilities,
205+
_is_descriptor: is_descriptors,
206+
_storage_address: storage_address,
207+
})
208+
}
209+
}
210+
211+
/// return the wallets available in default wallet directory
212+
//TODO use bitcoincore_rpc method when PR #179 lands
213+
fn list_wallet_dir(client: &Client) -> Result<Vec<String>, Error> {
214+
#[derive(Deserialize)]
215+
struct Name {
216+
name: String,
217+
}
218+
#[derive(Deserialize)]
219+
struct CallResult {
220+
wallets: Vec<Name>,
137221
}
222+
223+
let result: CallResult = client.call("listwalletdir", &[])?;
224+
Ok(result.wallets.into_iter().map(|n| n.name).collect())
225+
}
226+
227+
/// Returns whether a wallet is legacy or descriptor by calling `getwalletinfo`.
228+
///
229+
/// This API is mapped by bitcoincore_rpc, but it doesn't have the fields we need (either
230+
/// "descriptor" or "format") so we have to call the RPC manually
231+
fn is_wallet_descriptor(client: &Client) -> Result<bool, Error> {
232+
#[derive(Deserialize)]
233+
struct CallResult {
234+
descriptor: Option<bool>,
235+
}
236+
237+
let result: CallResult = client.call("getwalletinfo", &[])?;
238+
Ok(result.descriptor.unwrap_or(false))
138239
}
139240

140241
/// Factory of ['PrunedRpcBlockchain'] instance, implements ['BlockchainFactory']
141242
///
142243
/// Internally caches the node url and authentication params and allows getting many different
143244
/// ['PrunedRpcBlockchain'] objects for different wallet names
245+
///
246+
/// ## Example
247+
/// ```no_run
248+
/// # use bdk::bitcoin::Network;
249+
/// # use bdk::blockchain::BlockchainFactory;
250+
/// # use bdk::blockchain::pruned_rpc::{Auth, PrunedRpcBlockchainFactory};
251+
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
252+
/// let factory = PrunedRpcBlockchainFactory {
253+
/// url: "http://127.0.0.1:18332".to_string(),
254+
/// auth: Auth::Cookie {
255+
/// file: "/home/usr/.bitcoind/.cookie".into(),
256+
/// },
257+
/// network: Network::Testnet,
258+
/// wallet_name_prefix: Some("prefix-".to_string()),
259+
/// default_skip_blocks: 100_000,
260+
/// };
261+
/// let main_wallet_blockchain = factory.build("main_wallet", None)?;
262+
/// # Ok(())
263+
/// # }
264+
///
265+
/// ```
144266
#[derive(Debug, Clone)]
145-
pub struct PrunedRpcBlockchainFactory {}
267+
pub struct PrunedRpcBlockchainFactory {
268+
/// The bitcoin node url
269+
pub url: String,
270+
/// The bitcoin node authentication mechanism
271+
pub auth: Auth,
272+
/// The network we are using (it will be checked the bitcoin node network matches this)
273+
pub network: Network,
274+
/// The optional prefix used to build the full wallet name for blockchains
275+
pub wallet_name_prefix: Option<String>,
276+
/// Default number of blocks to skip which will be inherited by blockchain unless overridden
277+
pub default_skip_blocks: u32,
278+
}
146279

147280
impl BlockchainFactory for PrunedRpcBlockchainFactory {
148281
type Inner = PrunedRpcBlockchain;
149282

150283
fn build(
151284
&self,
152-
_wallet_name: &str,
285+
checksum: &str,
153286
_override_skip_blocks: Option<u32>,
154287
) -> Result<Self::Inner, Error> {
155-
todo!()
288+
PrunedRpcBlockchain::from_config(&PrunedRpcConfig {
289+
url: self.url.clone(),
290+
auth: self.auth.clone(),
291+
network: self.network,
292+
wallet_name: format!(
293+
"{}{}",
294+
self.wallet_name_prefix.as_ref().unwrap_or(&String::new()),
295+
checksum
296+
),
297+
})
156298
}
157299
}
158300

159301
#[cfg(test)]
160302
#[cfg(any(feature = "test-rpc", feature = "test-rpc-legacy"))]
161-
mod test {}
303+
mod test {
304+
use super::*;
305+
use crate::testutils::blockchain_tests::TestClient;
306+
307+
use bitcoin::Network;
308+
use bitcoincore_rpc::RpcApi;
309+
310+
fn get_factory() -> (TestClient, PrunedRpcBlockchainFactory) {
311+
let test_client = TestClient::default();
312+
313+
let factory = PrunedRpcBlockchainFactory {
314+
url: test_client.bitcoind.rpc_url(),
315+
auth: Auth::Cookie {
316+
file: test_client.bitcoind.params.cookie_file.clone(),
317+
},
318+
network: Network::Regtest,
319+
wallet_name_prefix: Some("prefix-".into()),
320+
default_skip_blocks: 0,
321+
};
322+
(test_client, factory)
323+
}
324+
325+
#[test]
326+
fn test_pruned_rpc_factory() {
327+
let (_test_client, factory) = get_factory();
328+
329+
let a = factory.build("aaaaaa", None).unwrap();
330+
// assert_eq!(a.skip_blocks, Some(0));
331+
assert_eq!(
332+
a.client
333+
.get_wallet_info()
334+
.expect("Node connection isn't working")
335+
.wallet_name,
336+
"prefix-aaaaaa"
337+
);
338+
339+
let b = factory.build("bbbbbb", Some(100)).unwrap();
340+
// assert_eq!(b.skip_blocks, Some(100));
341+
assert_eq!(
342+
b.client
343+
.get_wallet_info()
344+
.expect("Node connection isn't working")
345+
.wallet_name,
346+
"prefix-bbbbbb"
347+
);
348+
}
349+
}

0 commit comments

Comments
 (0)