@@ -10,21 +10,24 @@ use crate::bitcoin::{Address, Network, Transaction, Txid};
10
10
use crate :: blockchain:: * ;
11
11
use crate :: database:: BatchDatabase ;
12
12
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 ;
14
15
use bitcoincore_rpc:: { Client , RpcApi } ;
16
+ use log:: debug;
15
17
use serde:: { Deserialize , Serialize } ;
16
- use std:: collections:: HashSet ;
18
+ use std:: collections:: { HashMap , HashSet } ;
17
19
use std:: path:: PathBuf ;
20
+ use std:: str:: FromStr ;
18
21
19
22
/// The main struct for Pruned RPC backend implementing the [crate::blockchain::Blockchain] trait
20
23
#[ derive( Debug ) ]
21
24
pub struct PrunedRpcBlockchain {
22
25
/// Rpc client to the node, includes the wallet name
23
26
client : Client ,
24
- /// Whether the wallet is a "descriptor" or "legacy" wallet in Core
25
- _is_descriptors : bool ,
26
27
/// Blockchain capabilities, cached here at startup
27
28
capabilities : HashSet < Capability > ,
29
+ // Whether the wallet is a "descriptor" or "legacy" in core
30
+ _is_descriptor : bool ,
28
31
/// This is a fixed Address used as a hack key to store information on the node
29
32
_storage_address : Address ,
30
33
}
@@ -66,12 +69,12 @@ pub enum Auth {
66
69
} ,
67
70
}
68
71
69
- impl From < Auth > for RpcAuth {
72
+ impl From < Auth > for PrunedRpcAuth {
70
73
fn from ( auth : Auth ) -> Self {
71
74
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) ,
75
78
}
76
79
}
77
80
}
@@ -114,15 +117,15 @@ impl GetHeight for PrunedRpcBlockchain {
114
117
impl WalletSync for PrunedRpcBlockchain {
115
118
fn wallet_setup < D : BatchDatabase > (
116
119
& self ,
117
- _database : & mut D ,
118
- _progress_update : Box < dyn Progress > ,
120
+ database : & mut D ,
121
+ progress_update : Box < dyn Progress > ,
119
122
) -> Result < ( ) , Error > {
120
123
todo ! ( )
121
124
}
122
125
123
126
fn wallet_sync < D : BatchDatabase > (
124
127
& self ,
125
- _database : & mut D ,
128
+ db : & mut D ,
126
129
_progress_update : Box < dyn Progress > ,
127
130
) -> Result < ( ) , Error > {
128
131
todo ! ( )
@@ -132,30 +135,215 @@ impl WalletSync for PrunedRpcBlockchain {
132
135
impl ConfigurableBlockchain for PrunedRpcBlockchain {
133
136
type Config = PrunedRpcConfig ;
134
137
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 > ,
137
221
}
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 ) )
138
239
}
139
240
140
241
/// Factory of ['PrunedRpcBlockchain'] instance, implements ['BlockchainFactory']
141
242
///
142
243
/// Internally caches the node url and authentication params and allows getting many different
143
244
/// ['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
+ /// ```
144
266
#[ 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
+ }
146
279
147
280
impl BlockchainFactory for PrunedRpcBlockchainFactory {
148
281
type Inner = PrunedRpcBlockchain ;
149
282
150
283
fn build (
151
284
& self ,
152
- _wallet_name : & str ,
285
+ checksum : & str ,
153
286
_override_skip_blocks : Option < u32 > ,
154
287
) -> 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
+ } )
156
298
}
157
299
}
158
300
159
301
#[ cfg( test) ]
160
302
#[ 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