1use std::env::var;
4
5use bitcoin::{
6 bip32::Xpriv,
7 block::Header,
8 consensus::{self, encode::serialize_hex},
9 Address, Block, BlockHash, Network, Transaction, Txid,
10};
11use corepc_types::model;
12use corepc_types::v29::{
13 CreateWallet, GetAddressInfo, GetBlockHeader, GetBlockVerboseOne, GetBlockVerboseZero,
14 GetBlockchainInfo, GetMempoolInfo, GetNewAddress, GetRawMempool, GetRawMempoolVerbose,
15 GetRawTransaction, GetRawTransactionVerbose, GetTransaction, GetTxOut, ImportDescriptors,
16 ListDescriptors, ListTransactions, ListUnspent, PsbtBumpFee, SignRawTransactionWithWallet,
17 SubmitPackage, TestMempoolAccept, WalletCreateFundedPsbt, WalletProcessPsbt,
18};
19use serde_json::value::{RawValue, Value};
20use tracing::*;
21
22use crate::{
23 client::Client,
24 error::ClientError,
25 to_value,
26 traits::{Broadcaster, Reader, Signer, Wallet},
27 types::{
28 CreateRawTransactionArguments, CreateRawTransactionInput, CreateRawTransactionOutput,
29 CreateWalletArguments, ImportDescriptorInput, ListUnspentQueryOptions,
30 PreviousTransactionOutput, PsbtBumpFeeOptions, SighashType, WalletCreateFundedPsbtOptions,
31 },
32 ClientResult,
33};
34
35const MIN_FEE_RATE_BTC_VKB: f64 = 0.00001;
37
38impl Reader for Client {
39 async fn estimate_smart_fee(&self, conf_target: u16) -> ClientResult<u64> {
40 let result = self
41 .call::<Box<RawValue>>("estimatesmartfee", &[to_value(conf_target)?])
42 .await?
43 .to_string();
44
45 let result_map: Value = result.parse::<Value>()?;
46
47 let btc_vkb = result_map
48 .get("feerate")
49 .and_then(|v| v.as_f64())
50 .unwrap_or(MIN_FEE_RATE_BTC_VKB); if btc_vkb <= 0.0 {
54 return Err(ClientError::Other(
55 "Invalid fee rate: must be positive".to_string(),
56 ));
57 }
58
59 let sat_vb = (btc_vkb * 100_000_000.0 / 1_000.0) as u64;
61
62 Ok(sat_vb)
63 }
64
65 async fn get_block_header(&self, hash: &BlockHash) -> ClientResult<Header> {
66 let get_block_header = self
67 .call::<GetBlockHeader>(
68 "getblockheader",
69 &[to_value(hash.to_string())?, to_value(false)?],
70 )
71 .await?;
72 let header = get_block_header
73 .block_header()
74 .map_err(|err| ClientError::Other(format!("header decode: {err}")))?;
75 Ok(header)
76 }
77
78 async fn get_block(&self, hash: &BlockHash) -> ClientResult<Block> {
79 let get_block = self
80 .call::<GetBlockVerboseZero>("getblock", &[to_value(hash.to_string())?, to_value(0)?])
81 .await?;
82 let block = get_block
83 .into_model()
84 .map_err(|e| ClientError::Parse(e.to_string()))?
85 .0;
86 Ok(block)
87 }
88
89 async fn get_block_height(&self, hash: &BlockHash) -> ClientResult<u64> {
90 let block_verobose = self
91 .call::<GetBlockVerboseOne>("getblock", &[to_value(hash.to_string())?])
92 .await?;
93
94 let block_height = block_verobose.height as u64;
95 Ok(block_height)
96 }
97
98 async fn get_block_header_at(&self, height: u64) -> ClientResult<Header> {
99 let hash = self.get_block_hash(height).await?;
100 self.get_block_header(&hash).await
101 }
102
103 async fn get_block_at(&self, height: u64) -> ClientResult<Block> {
104 let hash = self.get_block_hash(height).await?;
105 self.get_block(&hash).await
106 }
107
108 async fn get_block_count(&self) -> ClientResult<u64> {
109 self.call::<u64>("getblockcount", &[]).await
110 }
111
112 async fn get_block_hash(&self, height: u64) -> ClientResult<BlockHash> {
113 self.call::<BlockHash>("getblockhash", &[to_value(height)?])
114 .await
115 }
116
117 async fn get_blockchain_info(&self) -> ClientResult<model::GetBlockchainInfo> {
118 let res = self
119 .call::<GetBlockchainInfo>("getblockchaininfo", &[])
120 .await?;
121 res.into_model()
122 .map_err(|e| ClientError::Parse(e.to_string()))
123 }
124
125 async fn get_current_timestamp(&self) -> ClientResult<u32> {
126 let best_block_hash = self.call::<BlockHash>("getbestblockhash", &[]).await?;
127 let block = self.get_block(&best_block_hash).await?;
128 Ok(block.header.time)
129 }
130
131 async fn get_raw_mempool(&self) -> ClientResult<model::GetRawMempool> {
132 let resp = self.call::<GetRawMempool>("getrawmempool", &[]).await?;
133 resp.into_model()
134 .map_err(|e| ClientError::Parse(e.to_string()))
135 }
136
137 async fn get_raw_mempool_verbose(&self) -> ClientResult<model::GetRawMempoolVerbose> {
138 let resp = self
139 .call::<GetRawMempoolVerbose>("getrawmempool", &[to_value(true)?])
140 .await?;
141
142 resp.into_model()
143 .map_err(|e| ClientError::Parse(e.to_string()))
144 }
145
146 async fn get_mempool_info(&self) -> ClientResult<model::GetMempoolInfo> {
147 let resp = self.call::<GetMempoolInfo>("getmempoolinfo", &[]).await?;
148 resp.into_model()
149 .map_err(|e| ClientError::Parse(e.to_string()))
150 }
151
152 async fn get_raw_transaction_verbosity_zero(
153 &self,
154 txid: &Txid,
155 ) -> ClientResult<model::GetRawTransaction> {
156 let resp = self
157 .call::<GetRawTransaction>(
158 "getrawtransaction",
159 &[to_value(txid.to_string())?, to_value(0)?],
160 )
161 .await?;
162 resp.into_model()
163 .map_err(|e| ClientError::Parse(e.to_string()))
164 }
165
166 async fn get_raw_transaction_verbosity_one(
167 &self,
168 txid: &Txid,
169 ) -> ClientResult<model::GetRawTransactionVerbose> {
170 let resp = self
171 .call::<GetRawTransactionVerbose>(
172 "getrawtransaction",
173 &[to_value(txid.to_string())?, to_value(1)?],
174 )
175 .await?;
176 resp.into_model()
177 .map_err(|e| ClientError::Parse(e.to_string()))
178 }
179
180 async fn get_tx_out(
181 &self,
182 txid: &Txid,
183 vout: u32,
184 include_mempool: bool,
185 ) -> ClientResult<model::GetTxOut> {
186 let resp = self
187 .call::<GetTxOut>(
188 "gettxout",
189 &[
190 to_value(txid.to_string())?,
191 to_value(vout)?,
192 to_value(include_mempool)?,
193 ],
194 )
195 .await?;
196 resp.into_model()
197 .map_err(|e| ClientError::Parse(e.to_string()))
198 }
199
200 async fn network(&self) -> ClientResult<Network> {
201 let chain = self
202 .call::<GetBlockchainInfo>("getblockchaininfo", &[])
203 .await?
204 .chain;
205 Network::from_core_arg(&chain).map_err(|e| ClientError::Parse(e.to_string()))
206 }
207}
208
209impl Broadcaster for Client {
210 async fn send_raw_transaction(&self, tx: &Transaction) -> ClientResult<Txid> {
211 let txstr = serialize_hex(tx);
212 trace!(txstr = %txstr, "Sending raw transaction");
213 match self
214 .call::<Txid>("sendrawtransaction", &[to_value(txstr)?])
215 .await
216 {
217 Ok(txid) => {
218 trace!(?txid, "Transaction sent");
219 Ok(txid)
220 }
221 Err(ClientError::Server(i, s)) => match i {
222 -27 => Ok(tx.compute_txid()), _ => Err(ClientError::Server(i, s)),
225 },
226 Err(e) => Err(ClientError::Other(e.to_string())),
227 }
228 }
229
230 async fn test_mempool_accept(
231 &self,
232 tx: &Transaction,
233 ) -> ClientResult<model::TestMempoolAccept> {
234 let txstr = serialize_hex(tx);
235 trace!(%txstr, "Testing mempool accept");
236 let resp = self
237 .call::<TestMempoolAccept>("testmempoolaccept", &[to_value([txstr])?])
238 .await?;
239 resp.into_model()
240 .map_err(|e| ClientError::Parse(e.to_string()))
241 }
242
243 async fn submit_package(&self, txs: &[Transaction]) -> ClientResult<model::SubmitPackage> {
244 let txstrs: Vec<String> = txs.iter().map(serialize_hex).collect();
245 let resp = self
246 .call::<SubmitPackage>("submitpackage", &[to_value(txstrs)?])
247 .await?;
248 trace!(?resp, "Got submit package response");
249
250 resp.into_model()
251 .map_err(|e| ClientError::Parse(e.to_string()))
252 }
253}
254
255impl Wallet for Client {
256 async fn get_new_address(&self) -> ClientResult<Address> {
257 let address_unchecked = self
258 .call::<GetNewAddress>("getnewaddress", &[])
259 .await?
260 .0
261 .parse::<Address<_>>()
262 .map_err(|e| ClientError::Parse(e.to_string()))?
263 .assume_checked();
264 Ok(address_unchecked)
265 }
266 async fn get_transaction(&self, txid: &Txid) -> ClientResult<model::GetTransaction> {
267 let resp = self
268 .call::<GetTransaction>("gettransaction", &[to_value(txid.to_string())?])
269 .await?;
270 resp.into_model()
271 .map_err(|e| ClientError::Parse(e.to_string()))
272 }
273
274 async fn list_transactions(
275 &self,
276 count: Option<usize>,
277 ) -> ClientResult<model::ListTransactions> {
278 let resp = self
279 .call::<ListTransactions>("listtransactions", &[to_value(count)?])
280 .await?;
281 resp.into_model()
282 .map_err(|e| ClientError::Parse(e.to_string()))
283 }
284
285 async fn list_wallets(&self) -> ClientResult<Vec<String>> {
286 self.call::<Vec<String>>("listwallets", &[]).await
287 }
288
289 async fn create_raw_transaction(
290 &self,
291 raw_tx: CreateRawTransactionArguments,
292 ) -> ClientResult<Transaction> {
293 let raw_tx = self
294 .call::<String>(
295 "createrawtransaction",
296 &[to_value(raw_tx.inputs)?, to_value(raw_tx.outputs)?],
297 )
298 .await?;
299 trace!(%raw_tx, "Created raw transaction");
300 consensus::encode::deserialize_hex(&raw_tx)
301 .map_err(|e| ClientError::Other(format!("Failed to deserialize raw transaction: {e}")))
302 }
303
304 async fn wallet_create_funded_psbt(
305 &self,
306 inputs: &[CreateRawTransactionInput],
307 outputs: &[CreateRawTransactionOutput],
308 locktime: Option<u32>,
309 options: Option<WalletCreateFundedPsbtOptions>,
310 bip32_derivs: Option<bool>,
311 ) -> ClientResult<model::WalletCreateFundedPsbt> {
312 let resp = self
313 .call::<WalletCreateFundedPsbt>(
314 "walletcreatefundedpsbt",
315 &[
316 to_value(inputs)?,
317 to_value(outputs)?,
318 to_value(locktime.unwrap_or(0))?,
319 to_value(options.unwrap_or_default())?,
320 to_value(bip32_derivs)?,
321 ],
322 )
323 .await?;
324 resp.into_model()
325 .map_err(|e| ClientError::Parse(e.to_string()))
326 }
327
328 async fn get_address_info(&self, address: &Address) -> ClientResult<model::GetAddressInfo> {
329 trace!(address = %address, "Getting address info");
330 let resp = self
331 .call::<GetAddressInfo>("getaddressinfo", &[to_value(address.to_string())?])
332 .await?;
333 resp.into_model()
334 .map_err(|e| ClientError::Parse(e.to_string()))
335 }
336
337 async fn list_unspent(
338 &self,
339 min_conf: Option<u32>,
340 max_conf: Option<u32>,
341 addresses: Option<&[Address]>,
342 include_unsafe: Option<bool>,
343 query_options: Option<ListUnspentQueryOptions>,
344 ) -> ClientResult<model::ListUnspent> {
345 let addr_strings: Vec<String> = addresses
346 .map(|addrs| addrs.iter().map(|a| a.to_string()).collect())
347 .unwrap_or_default();
348
349 let mut params = vec![
350 to_value(min_conf.unwrap_or(1))?,
351 to_value(max_conf.unwrap_or(9_999_999))?,
352 to_value(addr_strings)?,
353 to_value(include_unsafe.unwrap_or(true))?,
354 ];
355
356 if let Some(query_options) = query_options {
357 params.push(to_value(query_options)?);
358 }
359
360 let resp = self.call::<ListUnspent>("listunspent", ¶ms).await?;
361 trace!(?resp, "Got UTXOs");
362
363 resp.into_model()
364 .map_err(|e| ClientError::Parse(e.to_string()))
365 }
366}
367
368impl Signer for Client {
369 async fn sign_raw_transaction_with_wallet(
370 &self,
371 tx: &Transaction,
372 prev_outputs: Option<Vec<PreviousTransactionOutput>>,
373 ) -> ClientResult<model::SignRawTransactionWithWallet> {
374 let tx_hex = serialize_hex(tx);
375 trace!(tx_hex = %tx_hex, "Signing transaction");
376 trace!(?prev_outputs, "Signing transaction with previous outputs");
377 let resp = self
378 .call::<SignRawTransactionWithWallet>(
379 "signrawtransactionwithwallet",
380 &[to_value(tx_hex)?, to_value(prev_outputs)?],
381 )
382 .await?;
383 resp.into_model()
384 .map_err(|e| ClientError::Parse(e.to_string()))
385 }
386
387 async fn get_xpriv(&self) -> ClientResult<Option<Xpriv>> {
388 if var("BITCOIN_XPRIV_RETRIEVABLE").is_err() {
390 return Ok(None);
391 }
392
393 let descriptors = self
394 .call::<ListDescriptors>("listdescriptors", &[to_value(true)?]) .await?
396 .descriptors;
397 if descriptors.is_empty() {
398 return Err(ClientError::Other("No descriptors found".to_string()));
399 }
400
401 let descriptor = descriptors
403 .iter()
404 .find(|d| d.descriptor.contains("tr("))
405 .map(|d| d.descriptor.clone())
406 .ok_or(ClientError::Xpriv)?;
407
408 let xpriv_str = descriptor
410 .split("tr(")
411 .nth(1)
412 .ok_or(ClientError::Xpriv)?
413 .split("/")
414 .next()
415 .ok_or(ClientError::Xpriv)?;
416
417 let xpriv = xpriv_str.parse::<Xpriv>().map_err(|_| ClientError::Xpriv)?;
418 Ok(Some(xpriv))
419 }
420
421 async fn import_descriptors(
422 &self,
423 descriptors: Vec<ImportDescriptorInput>,
424 wallet_name: String,
425 ) -> ClientResult<ImportDescriptors> {
426 let wallet_args = CreateWalletArguments {
427 name: wallet_name,
428 load_on_startup: Some(true),
429 };
430
431 let _wallet_create = self
434 .call::<CreateWallet>("createwallet", &[to_value(wallet_args.clone())?])
435 .await;
436 let _wallet_load = self
438 .call::<CreateWallet>("loadwallet", &[to_value(wallet_args)?])
439 .await;
440
441 let result = self
442 .call::<ImportDescriptors>("importdescriptors", &[to_value(descriptors)?])
443 .await?;
444 Ok(result)
445 }
446
447 async fn wallet_process_psbt(
448 &self,
449 psbt: &str,
450 sign: Option<bool>,
451 sighashtype: Option<SighashType>,
452 bip32_derivs: Option<bool>,
453 ) -> ClientResult<model::WalletProcessPsbt> {
454 let mut params = vec![to_value(psbt)?, to_value(sign.unwrap_or(true))?];
455
456 if let Some(sighashtype) = sighashtype {
457 params.push(to_value(sighashtype)?);
458 }
459
460 if let Some(bip32_derivs) = bip32_derivs {
461 params.push(to_value(bip32_derivs)?);
462 }
463
464 let resp = self
465 .call::<WalletProcessPsbt>("walletprocesspsbt", ¶ms)
466 .await?;
467 resp.into_model()
468 .map_err(|e| ClientError::Parse(e.to_string()))
469 }
470
471 async fn psbt_bump_fee(
472 &self,
473 txid: &Txid,
474 options: Option<PsbtBumpFeeOptions>,
475 ) -> ClientResult<model::PsbtBumpFee> {
476 let mut params = vec![to_value(txid.to_string())?];
477
478 if let Some(options) = options {
479 params.push(to_value(options)?);
480 }
481
482 let resp = self.call::<PsbtBumpFee>("psbtbumpfee", ¶ms).await?;
483 resp.into_model()
484 .map_err(|e| ClientError::Parse(e.to_string()))
485 }
486}
487
488#[cfg(test)]
489mod test {
490
491 use std::sync::Once;
492
493 use bitcoin::{hashes::Hash, transaction, Amount, FeeRate, NetworkKind};
494 use corepc_types::v29::ImportDescriptorsResult;
495 use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
496
497 use super::*;
498 use crate::{
499 test_utils::corepc_node_helpers::{get_bitcoind_and_client, mine_blocks},
500 types::{CreateRawTransactionInput, CreateRawTransactionOutput},
501 Auth,
502 };
503
504 const COINBASE_AMOUNT: Amount = Amount::from_sat(50 * 100_000_000);
506
507 fn init_tracing() {
509 static INIT: Once = Once::new();
510
511 INIT.call_once(|| {
512 tracing_subscriber::registry()
513 .with(fmt::layer())
514 .with(EnvFilter::from_default_env())
515 .try_init()
516 .ok();
517 });
518 }
519
520 #[tokio::test()]
521 async fn client_works() {
522 init_tracing();
523
524 let (bitcoind, client) = get_bitcoind_and_client();
525
526 let got = client.network().await.unwrap();
528 let expected = Network::Regtest;
529
530 assert_eq!(expected, got);
531 let get_blockchain_info = client.get_blockchain_info().await.unwrap();
533 assert_eq!(get_blockchain_info.blocks, 0);
534
535 let _ = client
537 .get_current_timestamp()
538 .await
539 .expect("must be able to get current timestamp");
540
541 let blocks = mine_blocks(&bitcoind, 101, None).unwrap();
542
543 let expected = blocks.last().unwrap();
545 let got = client.get_block(expected).await.unwrap().block_hash();
546 assert_eq!(*expected, got);
547
548 let target_height = blocks.len() as u64;
550 let expected = blocks.last().unwrap();
551 let got = client
552 .get_block_at(target_height)
553 .await
554 .unwrap()
555 .block_hash();
556 assert_eq!(*expected, got);
557
558 let expected = blocks.len() as u64;
560 let got = client.get_block_count().await.unwrap();
561 assert_eq!(expected, got);
562
563 let target_height = blocks.len() as u64;
565 let expected = blocks.last().unwrap();
566 let got = client.get_block_hash(target_height).await.unwrap();
567 assert_eq!(*expected, got);
568
569 let target_height = blocks.len() as u64;
571 let expected = blocks.last().unwrap();
572 let got = client.get_block_header_at(target_height).await.unwrap();
573 assert_eq!(*expected, got.block_hash());
574
575 let address = client.get_new_address().await.unwrap();
577 let txid = client
578 .call::<String>(
579 "sendtoaddress",
580 &[to_value(address.to_string()).unwrap(), to_value(1).unwrap()],
581 )
582 .await
583 .unwrap()
584 .parse::<Txid>()
585 .unwrap();
586
587 let tx = client.get_transaction(&txid).await.unwrap().tx;
589 let got = client.send_raw_transaction(&tx).await.unwrap();
590 let expected = txid; assert_eq!(expected, got);
592
593 let got = client
595 .get_raw_transaction_verbosity_zero(&txid)
596 .await
597 .unwrap()
598 .0
599 .compute_txid();
600 assert_eq!(expected, got);
601
602 let got = client
604 .get_raw_transaction_verbosity_one(&txid)
605 .await
606 .unwrap()
607 .transaction
608 .compute_txid();
609 assert_eq!(expected, got);
610
611 let got = client.get_raw_mempool().await.unwrap();
613 let expected = vec![txid];
614 assert_eq!(expected, got.0);
615
616 let got = client.get_raw_mempool_verbose().await.unwrap();
618 assert_eq!(got.0.len(), 1);
619 assert_eq!(got.0.get(&txid).unwrap().height, 101);
620
621 let got = client.get_mempool_info().await.unwrap();
623 assert!(got.loaded.unwrap_or(false));
624 assert_eq!(got.size, 1);
625 assert_eq!(got.unbroadcast_count, Some(1));
626
627 let got = client.estimate_smart_fee(1).await.unwrap();
629 let expected = 1; assert_eq!(expected, got);
631
632 let got = client
634 .sign_raw_transaction_with_wallet(&tx, None)
635 .await
636 .unwrap();
637 assert!(got.complete);
638 assert!(got.errors.is_empty());
639
640 let txids = client
642 .test_mempool_accept(&tx)
643 .await
644 .expect("must be able to test mempool accept");
645 let got = txids
646 .results
647 .first()
648 .expect("there must be at least one txid");
649 assert_eq!(
650 got.txid,
651 tx.compute_txid(),
652 "txids must match in the mempool"
653 );
654
655 let got = client.send_raw_transaction(&tx).await.unwrap();
657 assert!(got.as_byte_array().len() == 32);
658
659 let got = client.list_transactions(None).await.unwrap();
661 assert_eq!(got.0.len(), 10);
662
663 mine_blocks(&bitcoind, 1, None).unwrap();
666 let got = client
667 .list_unspent(None, None, None, None, None)
668 .await
669 .unwrap();
670 assert_eq!(got.0.len(), 3);
671
672 let got = client.get_xpriv().await.unwrap().unwrap().network;
674 let expected = NetworkKind::Test;
675 assert_eq!(expected, got);
676
677 let descriptor_string = "tr([e61b318f/20000'/20']tprv8ZgxMBicQKsPd4arFr7sKjSnKFDVMR2JHw9Y8L9nXN4kiok4u28LpHijEudH3mMYoL4pM5UL9Bgdz2M4Cy8EzfErmU9m86ZTw6hCzvFeTg7/101/*)#2plamwqs".to_owned();
680 let timestamp = "now".to_owned();
681 let list_descriptors = vec![ImportDescriptorInput {
682 desc: descriptor_string,
683 active: Some(true),
684 timestamp,
685 }];
686 let got = client
687 .import_descriptors(list_descriptors, "strata".to_owned())
688 .await
689 .unwrap()
690 .0;
691 let expected = vec![ImportDescriptorsResult {
692 success: true,
693 warnings: Some(vec![
694 "Range not given, using default keypool range".to_string()
695 ]),
696 error: None,
697 }];
698 assert_eq!(expected, got);
699
700 let psbt_address = client.get_new_address().await.unwrap();
701 let psbt_outputs = vec![CreateRawTransactionOutput::AddressAmount {
702 address: psbt_address.to_string(),
703 amount: 1.0,
704 }];
705
706 let funded_psbt = client
707 .wallet_create_funded_psbt(&[], &psbt_outputs, None, None, None)
708 .await
709 .unwrap();
710 assert!(!funded_psbt.psbt.inputs.is_empty());
711 assert!(funded_psbt.fee.to_sat() > 0);
712
713 let processed_psbt = client
714 .wallet_process_psbt(&funded_psbt.psbt.to_string(), None, None, None)
715 .await
716 .unwrap();
717 assert!(!processed_psbt.psbt.inputs.is_empty());
718 assert!(processed_psbt.complete);
719
720 let finalized_psbt = client
721 .wallet_process_psbt(&funded_psbt.psbt.to_string(), Some(true), None, None)
722 .await
723 .unwrap();
724 assert!(finalized_psbt.complete);
725 assert!(finalized_psbt.hex.is_some());
726 let signed_tx = finalized_psbt.hex.as_ref().unwrap();
727 let signed_txid = signed_tx.compute_txid();
728 let got = client
729 .test_mempool_accept(signed_tx)
730 .await
731 .unwrap()
732 .results
733 .first()
734 .unwrap()
735 .txid;
736 assert_eq!(signed_txid, got);
737
738 let info_address = client.get_new_address().await.unwrap();
739 let address_info = client.get_address_info(&info_address).await.unwrap();
740 assert_eq!(address_info.address, info_address.as_unchecked().clone());
741 assert!(address_info.is_mine);
742 assert!(address_info.solvable.unwrap_or(false));
743
744 let unspent_address = client.get_new_address().await.unwrap();
745 let unspent_txid = client
746 .call::<String>(
747 "sendtoaddress",
748 &[
749 to_value(unspent_address.to_string()).unwrap(),
750 to_value(1.0).unwrap(),
751 ],
752 )
753 .await
754 .unwrap();
755 mine_blocks(&bitcoind, 1, None).unwrap();
756
757 let utxos = client
758 .list_unspent(Some(1), Some(9_999_999), None, Some(true), None)
759 .await
760 .unwrap();
761 assert!(!utxos.0.is_empty());
762
763 let utxos_filtered = client
764 .list_unspent(
765 Some(1),
766 Some(9_999_999),
767 Some(std::slice::from_ref(&unspent_address)),
768 Some(true),
769 None,
770 )
771 .await
772 .unwrap();
773 assert!(!utxos_filtered.0.is_empty());
774 let found_utxo = utxos_filtered.0.iter().any(|utxo| {
775 utxo.txid.to_string() == unspent_txid
776 && utxo.address.clone().assume_checked().to_string() == unspent_address.to_string()
777 });
778 assert!(found_utxo);
779
780 let query_options = ListUnspentQueryOptions {
781 minimum_amount: Some(Amount::from_btc(0.5).unwrap()),
782 maximum_amount: Some(Amount::from_btc(2.0).unwrap()),
783 maximum_count: Some(10),
784 };
785 let utxos_with_query = client
786 .list_unspent(
787 Some(1),
788 Some(9_999_999),
789 None,
790 Some(true),
791 Some(query_options),
792 )
793 .await
794 .unwrap();
795 assert!(!utxos_with_query.0.is_empty());
796 for utxo in &utxos_with_query.0 {
797 let amount_btc = utxo.amount.to_btc();
798 assert!((0.5..=2.0).contains(&amount_btc));
799 }
800
801 let tx = finalized_psbt.hex.unwrap();
802 assert!(!tx.input.is_empty());
803 assert!(!tx.output.is_empty());
804 }
805
806 #[tokio::test()]
807 async fn get_tx_out() {
808 init_tracing();
809
810 let (bitcoind, client) = get_bitcoind_and_client();
811
812 let got = client.network().await.unwrap();
814 let expected = Network::Regtest;
815 assert_eq!(expected, got);
816
817 let address = bitcoind.client.new_address().unwrap();
818 let blocks = mine_blocks(&bitcoind, 101, Some(address)).unwrap();
819 let last_block = client.get_block(blocks.first().unwrap()).await.unwrap();
820 let coinbase_tx = last_block.coinbase().unwrap();
821
822 let got = client
824 .get_tx_out(&coinbase_tx.compute_txid(), 0, true)
825 .await
826 .unwrap();
827 assert_eq!(got.tx_out.value, COINBASE_AMOUNT);
828
829 let new_address = bitcoind.client.new_address().unwrap();
831 let send_amount = Amount::from_sat(COINBASE_AMOUNT.to_sat() - 2_000); let _send_tx = bitcoind
833 .client
834 .send_to_address(&new_address, send_amount)
835 .unwrap()
836 .txid()
837 .unwrap();
838 let result = client
839 .get_tx_out(&coinbase_tx.compute_txid(), 0, true)
840 .await;
841 trace!(?result, "gettxout result");
842 assert!(result.is_err());
843 }
844
845 #[tokio::test()]
852 async fn submit_package() {
853 init_tracing();
854
855 let (bitcoind, client) = get_bitcoind_and_client();
856
857 let got = client.network().await.unwrap();
859 let expected = Network::Regtest;
860 assert_eq!(expected, got);
861
862 let blocks = mine_blocks(&bitcoind, 101, None).unwrap();
863 let last_block = client.get_block(blocks.first().unwrap()).await.unwrap();
864 let coinbase_tx = last_block.coinbase().unwrap();
865
866 let destination = client.get_new_address().await.unwrap();
867 let change_address = client.get_new_address().await.unwrap();
868 let amount = Amount::from_btc(1.0).unwrap();
869 let fees = Amount::from_btc(0.0001).unwrap();
870 let change_amount = COINBASE_AMOUNT - amount - fees;
871 let amount_minus_fees = Amount::from_sat(amount.to_sat() - 2_000);
872
873 let send_back_address = client.get_new_address().await.unwrap();
874 let parent_raw_tx = CreateRawTransactionArguments {
875 inputs: vec![CreateRawTransactionInput {
876 txid: coinbase_tx.compute_txid().to_string(),
877 vout: 0,
878 }],
879 outputs: vec![
880 CreateRawTransactionOutput::AddressAmount {
882 address: destination.to_string(),
883 amount: amount.to_btc(),
884 },
885 CreateRawTransactionOutput::AddressAmount {
887 address: change_address.to_string(),
888 amount: change_amount.to_btc(),
889 },
890 ],
891 };
892 let parent = client.create_raw_transaction(parent_raw_tx).await.unwrap();
893 let signed_parent = client
894 .sign_raw_transaction_with_wallet(&parent, None)
895 .await
896 .unwrap()
897 .tx;
898
899 let parent_submitted = client.send_raw_transaction(&signed_parent).await.unwrap();
901
902 let child_raw_tx = CreateRawTransactionArguments {
903 inputs: vec![CreateRawTransactionInput {
904 txid: parent_submitted.to_string(),
905 vout: 0,
906 }],
907 outputs: vec![
908 CreateRawTransactionOutput::AddressAmount {
910 address: send_back_address.to_string(),
911 amount: amount_minus_fees.to_btc(),
912 },
913 ],
914 };
915 let child = client.create_raw_transaction(child_raw_tx).await.unwrap();
916 let signed_child = client
917 .sign_raw_transaction_with_wallet(&child, None)
918 .await
919 .unwrap()
920 .tx;
921
922 let result = client
924 .submit_package(&[signed_parent, signed_child])
925 .await
926 .unwrap();
927 assert_eq!(result.tx_results.len(), 2);
928 assert_eq!(result.package_msg, "success");
929 }
930
931 #[tokio::test]
938 async fn submit_package_1p1c() {
939 init_tracing();
940
941 let (bitcoind, client) = get_bitcoind_and_client();
942
943 let server_version = bitcoind.client.server_version().unwrap();
945 assert!(server_version > 28);
946
947 let destination = client.get_new_address().await.unwrap();
948
949 let blocks = mine_blocks(&bitcoind, 101, None).unwrap();
950 let last_block = client.get_block(blocks.first().unwrap()).await.unwrap();
951 let coinbase_tx = last_block.coinbase().unwrap();
952
953 let parent_raw_tx = CreateRawTransactionArguments {
954 inputs: vec![CreateRawTransactionInput {
955 txid: coinbase_tx.compute_txid().to_string(),
956 vout: 0,
957 }],
958 outputs: vec![CreateRawTransactionOutput::AddressAmount {
959 address: destination.to_string(),
960 amount: COINBASE_AMOUNT.to_btc(),
961 }],
962 };
963 let mut parent = client.create_raw_transaction(parent_raw_tx).await.unwrap();
964 parent.version = transaction::Version(3);
965 assert_eq!(parent.version, transaction::Version(3));
966 trace!(?parent, "parent:");
967 let signed_parent = client
968 .sign_raw_transaction_with_wallet(&parent, None)
969 .await
970 .unwrap()
971 .tx;
972 assert_eq!(signed_parent.version, transaction::Version(3));
973
974 let parent_broadcasted = client.send_raw_transaction(&signed_parent).await;
976 assert!(parent_broadcasted.is_err());
977
978 let amount_minus_fees = Amount::from_sat(COINBASE_AMOUNT.to_sat() - 43_000);
980 let child_raw_tx = CreateRawTransactionArguments {
981 inputs: vec![CreateRawTransactionInput {
982 txid: signed_parent.compute_txid().to_string(),
983 vout: 0,
984 }],
985 outputs: vec![CreateRawTransactionOutput::AddressAmount {
986 address: destination.to_string(),
987 amount: amount_minus_fees.to_btc(),
988 }],
989 };
990 let mut child = client.create_raw_transaction(child_raw_tx).await.unwrap();
991 child.version = transaction::Version(3);
992 assert_eq!(child.version, transaction::Version(3));
993 trace!(?child, "child:");
994 let prev_outputs = vec![PreviousTransactionOutput {
995 txid: parent.compute_txid(),
996 vout: 0,
997 script_pubkey: parent.output[0].script_pubkey.to_hex_string(),
998 redeem_script: None,
999 witness_script: None,
1000 amount: Some(COINBASE_AMOUNT.to_btc()),
1001 }];
1002 let signed_child = client
1003 .sign_raw_transaction_with_wallet(&child, Some(prev_outputs))
1004 .await
1005 .unwrap()
1006 .tx;
1007 assert_eq!(signed_child.version, transaction::Version(3));
1008
1009 let child_broadcasted = client.send_raw_transaction(&signed_child).await;
1011 assert!(child_broadcasted.is_err());
1012
1013 let result = client
1015 .submit_package(&[signed_parent, signed_child])
1016 .await
1017 .unwrap();
1018 assert_eq!(result.tx_results.len(), 2);
1019 assert_eq!(result.package_msg, "success");
1020 }
1021
1022 #[tokio::test]
1023 async fn test_invalid_credentials_return_401_error() {
1024 init_tracing();
1025
1026 let (bitcoind, _) = get_bitcoind_and_client();
1027 let url = bitcoind.rpc_url();
1028
1029 let auth = Auth::UserPass("wrong_user".to_string(), "wrong_password".to_string());
1030 let invalid_client = Client::new(url, auth, None, None, None).unwrap();
1031
1032 let result = invalid_client.get_blockchain_info().await;
1034
1035 assert!(result.is_err());
1037 let error = result.unwrap_err();
1038
1039 match error {
1040 ClientError::Status(status_code, message) => {
1041 assert_eq!(status_code, 401);
1042 assert!(message.contains("Unauthorized"));
1043 }
1044 _ => panic!("Expected Status(401, _) error, but got: {error:?}"),
1045 }
1046 }
1047
1048 #[tokio::test]
1049 async fn test_send_raw_transaction_exposes_rpc_error_code_on_http_500() {
1050 init_tracing();
1051
1052 let (_bitcoind, client) = get_bitcoind_and_client();
1053
1054 let result = client
1055 .call::<String>("sendrawtransaction", &[to_value("deadbeef").unwrap()])
1056 .await;
1057
1058 match result {
1059 Err(ClientError::Server(code, message)) => {
1060 assert_eq!(code, -22);
1061 assert!(
1062 message.to_lowercase().contains("decode"),
1063 "expected decode-related RPC error message, got: {message}"
1064 );
1065 }
1066 other => panic!("Expected Server(-22, _), got: {other:?}"),
1067 }
1068 }
1069
1070 #[tokio::test]
1071 async fn test_get_raw_transaction_exposes_rpc_error_code_on_http_500() {
1072 init_tracing();
1073
1074 let (_bitcoind, client) = get_bitcoind_and_client();
1075 let missing_txid = Txid::from_slice(&[0u8; 32]).expect("must be a valid txid");
1076
1077 let error = client
1078 .get_raw_transaction_verbosity_zero(&missing_txid)
1079 .await
1080 .expect_err("missing txid must fail");
1081
1082 assert!(
1083 !matches!(error, ClientError::Status(..) | ClientError::Parse(..)),
1084 "expected parsed RPC error, got transport/parsing error: {error:?}"
1085 );
1086 assert!(
1087 error.is_tx_not_found(),
1088 "expected tx-not-found classification, got: {error:?}"
1089 );
1090 }
1091
1092 #[tokio::test]
1093 async fn psbt_bump_fee() {
1094 init_tracing();
1095
1096 let (bitcoind, client) = get_bitcoind_and_client();
1097
1098 mine_blocks(&bitcoind, 101, None).unwrap();
1100
1101 let destination = client.get_new_address().await.unwrap();
1103 let amount = Amount::from_btc(0.001).unwrap(); let txid = bitcoind
1107 .client
1108 .send_to_address_rbf(&destination, amount)
1109 .unwrap()
1110 .txid()
1111 .unwrap();
1112
1113 let mempool = client.get_raw_mempool().await.unwrap();
1115 assert!(
1116 mempool.0.contains(&txid),
1117 "Transaction should be in mempool for RBF"
1118 );
1119
1120 let signed_tx = client
1122 .psbt_bump_fee(&txid, None)
1123 .await
1124 .unwrap()
1125 .psbt
1126 .extract_tx()
1127 .unwrap();
1128 let signed_txid = signed_tx.compute_txid();
1129 let got = client
1130 .test_mempool_accept(&signed_tx)
1131 .await
1132 .unwrap()
1133 .results
1134 .first()
1135 .unwrap()
1136 .txid;
1137 assert_eq!(
1138 got, signed_txid,
1139 "Bumped transaction should be accepted in mempool"
1140 );
1141
1142 let options = PsbtBumpFeeOptions {
1144 fee_rate: Some(FeeRate::from_sat_per_kwu(20)), ..Default::default()
1146 };
1147 trace!(?options, "Calling psbt_bump_fee");
1148 let signed_tx = client
1149 .psbt_bump_fee(&txid, Some(options))
1150 .await
1151 .unwrap()
1152 .psbt
1153 .extract_tx()
1154 .unwrap();
1155 let signed_txid = signed_tx.compute_txid();
1156 let got = client
1157 .test_mempool_accept(&signed_tx)
1158 .await
1159 .unwrap()
1160 .results
1161 .first()
1162 .unwrap()
1163 .txid;
1164 assert_eq!(
1165 got, signed_txid,
1166 "Bumped transaction should be accepted in mempool"
1167 );
1168 }
1169
1170 #[cfg(feature = "raw_rpc")]
1171 #[tokio::test]
1172 async fn call_raw() {
1173 init_tracing();
1174
1175 let (bitcoind, client) = get_bitcoind_and_client();
1176
1177 mine_blocks(&bitcoind, 5, None).unwrap();
1178
1179 let expected = client.get_block_count().await.unwrap();
1180
1181 let got: u64 = client.call_raw("getblockcount", &[]).await.unwrap();
1182
1183 assert_eq!(expected, got);
1184
1185 let height = 0;
1186
1187 let expected_hash = client.get_block_hash(height).await.unwrap();
1188
1189 let got_hash: BlockHash = client
1190 .call_raw("getblockhash", &[to_value(height).unwrap()])
1191 .await
1192 .unwrap();
1193
1194 assert_eq!(expected_hash, got_hash);
1195 }
1196
1197 #[test]
1198 fn test_network_chain_response() {
1199 let test_cases = vec![
1200 ("main", Network::Bitcoin),
1201 ("test", Network::Testnet),
1202 ("testnet4", Network::Testnet4),
1203 ("signet", Network::Signet),
1204 ("regtest", Network::Regtest),
1205 ];
1206
1207 for (bitcoind_chain_str, expected_network) in test_cases {
1208 let result = Network::from_core_arg(bitcoind_chain_str);
1209 assert!(result.is_ok(), "failed for chain: {}", bitcoind_chain_str);
1210 assert_eq!(result.unwrap(), expected_network);
1211 }
1212 }
1213}