@@ -17,11 +17,14 @@ use crate::docker::{
17
17
self , docker_daemon_ip, free_local_port:: free_local_port, DockerImage , LogMessage ,
18
18
DOCKER_NETWORK ,
19
19
} ;
20
+ use serde:: export:: { Formatter , TryFrom } ;
21
+ use std:: fmt;
22
+ use std:: fmt:: Display ;
20
23
21
- const IMAGE : & str = "coblox/bitcoin-core:0.17 .0" ;
24
+ const IMAGE : & str = "coblox/bitcoin-core:0.20 .0" ;
22
25
23
- const USERNAME : & str = "bitcoin" ;
24
- const PASSWORD : & str = "t68ej4UX2pB0cLlGwSwHFBLKxXYgomkXyFyxuBmm2U8=" ;
26
+ pub const USERNAME : & str = "bitcoin" ;
27
+ pub const PASSWORD : & str = "t68ej4UX2pB0cLlGwSwHFBLKxXYgomkXyFyxuBmm2U8=" ;
25
28
26
29
#[ derive( derive_more:: Display , Copy , Clone ) ]
27
30
#[ display( fmt = "{}:{}" , ip, port) ]
@@ -60,6 +63,7 @@ pub async fn new_bitcoind_instance() -> anyhow::Result<BitcoindInstance> {
60
63
"-debug=1" ,
61
64
"-acceptnonstdtxn=0" ,
62
65
"-txindex" ,
66
+ "-fallbackfee=0.0002" ,
63
67
] ) ;
64
68
65
69
let p2p_port = free_local_port ( ) . await ?;
@@ -86,10 +90,15 @@ pub async fn new_bitcoind_instance() -> anyhow::Result<BitcoindInstance> {
86
90
LogMessage ( "Flushed wallet.dat" ) ,
87
91
vec ! [ ] ,
88
92
)
89
- . await ?;
93
+ . await
94
+ . context ( "unable to start bitcoind docker image" ) ?;
90
95
91
- let account_0 = fund_new_account ( http_endpoint) . await ?;
92
- let account_1 = fund_new_account ( http_endpoint) . await ?;
96
+ let account_0 = fund_new_account ( http_endpoint)
97
+ . await
98
+ . context ( "failed to fund first account" ) ?;
99
+ let account_1 = fund_new_account ( http_endpoint)
100
+ . await
101
+ . context ( "failed to fund second account" ) ?;
93
102
94
103
Ok ( BitcoindInstance {
95
104
p2p_uri,
@@ -115,16 +124,68 @@ async fn fund_new_account(endpoint: BitcoindHttpEndpoint) -> anyhow::Result<Acco
115
124
}
116
125
117
126
pub async fn mine_a_block ( endpoint : BitcoindHttpEndpoint ) -> anyhow:: Result < ( ) > {
118
- reqwest:: Client :: new ( )
127
+ let client = reqwest:: Client :: new ( ) ;
128
+
129
+ let new_address: NewAddressResponse = client
130
+ . post ( & endpoint. to_string ( ) )
131
+ . basic_auth ( USERNAME , Some ( PASSWORD ) )
132
+ . json ( & NewAddressRequest :: new ( "bech32" ) )
133
+ . send ( )
134
+ . await
135
+ . context ( "failed to create new address" ) ?
136
+ . json :: < NewAddressResponse > ( )
137
+ . await ?;
138
+ assert ! ( new_address. error. is_none( ) ) ;
139
+
140
+ let _ = client
119
141
. post ( & endpoint. to_string ( ) )
120
142
. basic_auth ( USERNAME , Some ( PASSWORD ) )
121
- . json ( & GenerateRequest :: new ( 1 ) )
143
+ . json ( & GenerateToAddressRequest :: new ( 1 , new_address . result ) )
122
144
. send ( )
123
145
. await ?;
124
146
125
147
Ok ( ( ) )
126
148
}
127
149
150
+ pub struct DerivationPath {
151
+ path : Vec < ChildNumber > ,
152
+ }
153
+
154
+ impl Display for DerivationPath {
155
+ fn fmt ( & self , f : & mut Formatter < ' _ > ) -> fmt:: Result {
156
+ for i in 0 ..self . path . len ( ) {
157
+ let elem = self . path . get ( i) . unwrap ( ) ;
158
+
159
+ let separator = if i == self . path . len ( ) - 1 { "" } else { "/" } ;
160
+
161
+ match elem {
162
+ ChildNumber :: Normal { index } => {
163
+ write ! ( f, "{:?}{:0}" , index, separator) ?;
164
+ }
165
+ ChildNumber :: Hardened { index } => {
166
+ write ! ( f, "{:?}h{:0}" , index, separator) ?;
167
+ }
168
+ }
169
+ }
170
+
171
+ Ok ( ( ) )
172
+ }
173
+ }
174
+
175
+ impl DerivationPath {
176
+ pub fn bip44_bitcoin_testnet ( ) -> anyhow:: Result < Self > {
177
+ Ok ( Self {
178
+ path : vec ! [
179
+ ChildNumber :: from_hardened_idx( 44 ) ?,
180
+ ChildNumber :: from_hardened_idx( 1 ) ?,
181
+ ChildNumber :: from_hardened_idx( 0 ) ?,
182
+ ChildNumber :: from_normal_idx( 0 ) ?,
183
+ ChildNumber :: from_normal_idx( 0 ) ?,
184
+ ] ,
185
+ } )
186
+ }
187
+ }
188
+
128
189
#[ derive( Clone ) ]
129
190
pub struct Account {
130
191
pub master : ExtendedPrivKey ,
@@ -140,17 +201,12 @@ impl Account {
140
201
. context ( "failed to generate new random extended private key from seed" ) ?;
141
202
142
203
// define derivation path to derive private keys from the master key
143
- let derivation_path = vec ! [
144
- ChildNumber :: from_hardened_idx( 44 ) ?,
145
- ChildNumber :: from_hardened_idx( 1 ) ?,
146
- ChildNumber :: from_hardened_idx( 0 ) ?,
147
- ChildNumber :: from_normal_idx( 0 ) ?,
148
- ChildNumber :: from_normal_idx( 0 ) ?,
149
- ] ;
204
+ let derivation_path =
205
+ DerivationPath :: bip44_bitcoin_testnet ( ) . context ( "failed to create derivation path" ) ?;
150
206
151
207
// derive a private key from the master key
152
208
let priv_key = master
153
- . derive_priv ( & Secp256k1 :: new ( ) , & derivation_path)
209
+ . derive_priv ( & Secp256k1 :: new ( ) , & derivation_path. path )
154
210
. map ( |secret_key| secret_key. private_key ) ?;
155
211
156
212
// it is not great to store derived data in here but since the derivation can fail, it is better to fail early instead of later
@@ -169,20 +225,55 @@ impl Account {
169
225
}
170
226
171
227
#[ derive( Debug , serde:: Serialize ) ]
172
- pub struct GenerateRequest {
228
+ pub struct NewAddressRequest {
229
+ jsonrpc : String ,
230
+ id : String ,
231
+ method : String ,
232
+ params : Vec < String > ,
233
+ }
234
+
235
+ impl NewAddressRequest {
236
+ pub fn new ( address_format : & str ) -> Self {
237
+ let mut params = Vec :: new ( ) ;
238
+ let label = "" ;
239
+ params. push ( label. to_owned ( ) ) ;
240
+ params. push ( address_format. to_owned ( ) ) ;
241
+
242
+ NewAddressRequest {
243
+ jsonrpc : "1.0" . to_string ( ) ,
244
+ id : "getnewaddress" . to_string ( ) ,
245
+ method : "getnewaddress" . to_string ( ) ,
246
+ params,
247
+ }
248
+ }
249
+ }
250
+
251
+ #[ derive( Debug , serde:: Serialize ) ]
252
+ pub struct GenerateToAddressRequest {
173
253
jsonrpc : String ,
174
254
id : String ,
175
255
method : String ,
176
- params : Vec < u32 > ,
256
+ params : Vec < serde_json :: Value > ,
177
257
}
178
258
179
- impl GenerateRequest {
180
- pub fn new ( number : u32 ) -> Self {
181
- GenerateRequest {
259
+ impl GenerateToAddressRequest {
260
+ pub fn new ( number : u32 , address : Address ) -> Self {
261
+ let mut params = Vec :: new ( ) ;
262
+
263
+ let number = serde_json:: Value :: Number (
264
+ serde_json:: Number :: try_from ( number) . expect ( "can convert to number" ) ,
265
+ ) ;
266
+ assert ! ( number. is_u64( ) ) ;
267
+ params. push ( number) ;
268
+
269
+ let address = serde_json:: Value :: String ( address. to_string ( ) ) ;
270
+ params. push ( address) ;
271
+
272
+ GenerateToAddressRequest {
182
273
jsonrpc : "1.0" . to_string ( ) ,
183
- id : "generate " . to_string ( ) ,
184
- method : "generate " . to_string ( ) ,
185
- params : vec ! [ number ] ,
274
+ id : "generatetoaddress " . to_string ( ) ,
275
+ method : "generatetoaddress " . to_string ( ) ,
276
+ params,
186
277
}
187
278
}
188
279
}
@@ -208,22 +299,48 @@ impl FundRequest {
208
299
209
300
#[ derive( Debug , serde:: Deserialize ) ]
210
301
struct FundResponse {
211
- result : sha256d:: Hash ,
212
- error : Option < String > ,
302
+ result : Option < sha256d:: Hash > ,
303
+ error : Option < JsonRpcError > ,
304
+ id : String ,
305
+ }
306
+
307
+ #[ derive( Debug , serde:: Deserialize ) ]
308
+ struct NewAddressResponse {
309
+ result : Address ,
310
+ error : Option < JsonRpcError > ,
213
311
id : String ,
214
312
}
215
313
314
+ #[ derive( Debug , serde:: Deserialize , thiserror:: Error ) ]
315
+ #[ error( "JSON-RPC request failed with code {code}: {message}" ) ]
316
+ pub struct JsonRpcError {
317
+ code : i64 ,
318
+ message : String ,
319
+ }
320
+
216
321
async fn fund ( endpoint : & str , address : Address , amount : Amount ) -> anyhow:: Result < sha256d:: Hash > {
217
322
let client = reqwest:: Client :: new ( ) ;
218
323
219
- let _ = client
324
+ let new_address = client
220
325
. post ( endpoint)
221
326
. basic_auth ( USERNAME , Some ( PASSWORD ) )
222
- . json ( & GenerateRequest :: new ( 101 ) )
327
+ . json ( & NewAddressRequest :: new ( "bech32" ) )
223
328
. send ( )
329
+ . await
330
+ . context ( "failed to create new address" ) ?
331
+ . json :: < NewAddressResponse > ( )
224
332
. await ?;
333
+ assert ! ( new_address. error. is_none( ) ) ;
225
334
226
- let response = client
335
+ let _ = client
336
+ . post ( endpoint)
337
+ . basic_auth ( USERNAME , Some ( PASSWORD ) )
338
+ . json ( & GenerateToAddressRequest :: new ( 101 , new_address. result ) )
339
+ . send ( )
340
+ . await
341
+ . context ( "failed to generate blocks" ) ?;
342
+
343
+ let response: FundResponse = client
227
344
. post ( endpoint)
228
345
. basic_auth ( USERNAME , Some ( PASSWORD ) )
229
346
. json ( & FundRequest :: new ( address, amount) )
@@ -232,7 +349,15 @@ async fn fund(endpoint: &str, address: Address, amount: Amount) -> anyhow::Resul
232
349
. json :: < FundResponse > ( )
233
350
. await ?;
234
351
235
- Ok ( response. result )
352
+ match response. error {
353
+ None => match response. result {
354
+ None => Err ( anyhow:: Error :: msg (
355
+ "no transaction hash returned without yielding error" ,
356
+ ) ) ,
357
+ Some ( tx_hash) => Ok ( tx_hash) ,
358
+ } ,
359
+ Some ( error) => Err ( anyhow:: Error :: new ( error) ) ,
360
+ }
236
361
}
237
362
238
363
fn derive_address ( secret_key : secp256k1:: SecretKey ) -> Address {
@@ -250,3 +375,32 @@ fn derive_p2wpkh_regtest_address(public_key: secp256k1::PublicKey) -> Address {
250
375
Network :: Regtest ,
251
376
)
252
377
}
378
+
379
+ #[ cfg( test) ]
380
+ mod tests {
381
+ use super :: * ;
382
+ use std:: str:: FromStr ;
383
+
384
+ #[ test]
385
+ fn format_derivation_path ( ) {
386
+ let derivation_path = DerivationPath :: bip44_bitcoin_testnet ( ) . unwrap ( ) ;
387
+
388
+ let to_string = derivation_path. to_string ( ) ;
389
+ assert_eq ! ( to_string, "44h/1h/0h/0/0" )
390
+ }
391
+
392
+ #[ test]
393
+ fn generate_to_address_request_does_not_panic ( ) {
394
+ let number = 101 ;
395
+ let address = Address :: from_str ( "2MubReUTptB6isbuFmsRiN3BPHaeHpiAjQM" ) . unwrap ( ) ;
396
+
397
+ let _ = GenerateToAddressRequest :: new ( number, address) ;
398
+ }
399
+
400
+ #[ test]
401
+ fn new_address_request_does_not_panic ( ) {
402
+ let format = "bech32" ;
403
+
404
+ let _ = NewAddressRequest :: new ( format) ;
405
+ }
406
+ }
0 commit comments