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(err @ ClientError::Server(_, _)) if err.is_rpc_verify_already_in_utxo_set() => {
222 Ok(tx.compute_txid())
223 }
224 Err(err @ ClientError::Server(_, _)) => Err(err),
225 Err(e) => Err(ClientError::Other(e.to_string())),
226 }
227 }
228
229 async fn test_mempool_accept(
230 &self,
231 tx: &Transaction,
232 ) -> ClientResult<model::TestMempoolAccept> {
233 let txstr = serialize_hex(tx);
234 trace!(%txstr, "Testing mempool accept");
235 let resp = self
236 .call::<TestMempoolAccept>("testmempoolaccept", &[to_value([txstr])?])
237 .await?;
238 resp.into_model()
239 .map_err(|e| ClientError::Parse(e.to_string()))
240 }
241
242 async fn submit_package(&self, txs: &[Transaction]) -> ClientResult<model::SubmitPackage> {
243 let txstrs: Vec<String> = txs.iter().map(serialize_hex).collect();
244 let resp = self
245 .call::<SubmitPackage>("submitpackage", &[to_value(txstrs)?])
246 .await?;
247 trace!(?resp, "Got submit package response");
248
249 resp.into_model()
250 .map_err(|e| ClientError::Parse(e.to_string()))
251 }
252}
253
254impl Wallet for Client {
255 async fn get_new_address(&self) -> ClientResult<Address> {
256 let address_unchecked = self
257 .call::<GetNewAddress>("getnewaddress", &[])
258 .await?
259 .0
260 .parse::<Address<_>>()
261 .map_err(|e| ClientError::Parse(e.to_string()))?
262 .assume_checked();
263 Ok(address_unchecked)
264 }
265 async fn get_transaction(&self, txid: &Txid) -> ClientResult<model::GetTransaction> {
266 let resp = self
267 .call::<GetTransaction>("gettransaction", &[to_value(txid.to_string())?])
268 .await?;
269 resp.into_model()
270 .map_err(|e| ClientError::Parse(e.to_string()))
271 }
272
273 async fn list_transactions(
274 &self,
275 count: Option<usize>,
276 ) -> ClientResult<model::ListTransactions> {
277 let resp = self
278 .call::<ListTransactions>("listtransactions", &[to_value(count)?])
279 .await?;
280 resp.into_model()
281 .map_err(|e| ClientError::Parse(e.to_string()))
282 }
283
284 async fn list_wallets(&self) -> ClientResult<Vec<String>> {
285 self.call::<Vec<String>>("listwallets", &[]).await
286 }
287
288 async fn create_raw_transaction(
289 &self,
290 raw_tx: CreateRawTransactionArguments,
291 ) -> ClientResult<Transaction> {
292 let raw_tx = self
293 .call::<String>(
294 "createrawtransaction",
295 &[to_value(raw_tx.inputs)?, to_value(raw_tx.outputs)?],
296 )
297 .await?;
298 trace!(%raw_tx, "Created raw transaction");
299 consensus::encode::deserialize_hex(&raw_tx)
300 .map_err(|e| ClientError::Other(format!("Failed to deserialize raw transaction: {e}")))
301 }
302
303 async fn wallet_create_funded_psbt(
304 &self,
305 inputs: &[CreateRawTransactionInput],
306 outputs: &[CreateRawTransactionOutput],
307 locktime: Option<u32>,
308 options: Option<WalletCreateFundedPsbtOptions>,
309 bip32_derivs: Option<bool>,
310 ) -> ClientResult<model::WalletCreateFundedPsbt> {
311 let resp = self
312 .call::<WalletCreateFundedPsbt>(
313 "walletcreatefundedpsbt",
314 &[
315 to_value(inputs)?,
316 to_value(outputs)?,
317 to_value(locktime.unwrap_or(0))?,
318 to_value(options.unwrap_or_default())?,
319 to_value(bip32_derivs)?,
320 ],
321 )
322 .await?;
323 resp.into_model()
324 .map_err(|e| ClientError::Parse(e.to_string()))
325 }
326
327 async fn get_address_info(&self, address: &Address) -> ClientResult<model::GetAddressInfo> {
328 trace!(address = %address, "Getting address info");
329 let resp = self
330 .call::<GetAddressInfo>("getaddressinfo", &[to_value(address.to_string())?])
331 .await?;
332 resp.into_model()
333 .map_err(|e| ClientError::Parse(e.to_string()))
334 }
335
336 async fn list_unspent(
337 &self,
338 min_conf: Option<u32>,
339 max_conf: Option<u32>,
340 addresses: Option<&[Address]>,
341 include_unsafe: Option<bool>,
342 query_options: Option<ListUnspentQueryOptions>,
343 ) -> ClientResult<model::ListUnspent> {
344 let addr_strings: Vec<String> = addresses
345 .map(|addrs| addrs.iter().map(|a| a.to_string()).collect())
346 .unwrap_or_default();
347
348 let mut params = vec![
349 to_value(min_conf.unwrap_or(1))?,
350 to_value(max_conf.unwrap_or(9_999_999))?,
351 to_value(addr_strings)?,
352 to_value(include_unsafe.unwrap_or(true))?,
353 ];
354
355 if let Some(query_options) = query_options {
356 params.push(to_value(query_options)?);
357 }
358
359 let resp = self.call::<ListUnspent>("listunspent", ¶ms).await?;
360 trace!(?resp, "Got UTXOs");
361
362 resp.into_model()
363 .map_err(|e| ClientError::Parse(e.to_string()))
364 }
365}
366
367impl Signer for Client {
368 async fn sign_raw_transaction_with_wallet(
369 &self,
370 tx: &Transaction,
371 prev_outputs: Option<Vec<PreviousTransactionOutput>>,
372 ) -> ClientResult<model::SignRawTransactionWithWallet> {
373 let tx_hex = serialize_hex(tx);
374 trace!(tx_hex = %tx_hex, "Signing transaction");
375 trace!(?prev_outputs, "Signing transaction with previous outputs");
376 let resp = self
377 .call::<SignRawTransactionWithWallet>(
378 "signrawtransactionwithwallet",
379 &[to_value(tx_hex)?, to_value(prev_outputs)?],
380 )
381 .await?;
382 resp.into_model()
383 .map_err(|e| ClientError::Parse(e.to_string()))
384 }
385
386 async fn get_xpriv(&self) -> ClientResult<Option<Xpriv>> {
387 if var("BITCOIN_XPRIV_RETRIEVABLE").is_err() {
389 return Ok(None);
390 }
391
392 let descriptors = self
393 .call::<ListDescriptors>("listdescriptors", &[to_value(true)?]) .await?
395 .descriptors;
396 if descriptors.is_empty() {
397 return Err(ClientError::Other("No descriptors found".to_string()));
398 }
399
400 let descriptor = descriptors
402 .iter()
403 .find(|d| d.descriptor.contains("tr("))
404 .map(|d| d.descriptor.clone())
405 .ok_or(ClientError::Xpriv)?;
406
407 let xpriv_str = descriptor
409 .split("tr(")
410 .nth(1)
411 .ok_or(ClientError::Xpriv)?
412 .split("/")
413 .next()
414 .ok_or(ClientError::Xpriv)?;
415
416 let xpriv = xpriv_str.parse::<Xpriv>().map_err(|_| ClientError::Xpriv)?;
417 Ok(Some(xpriv))
418 }
419
420 async fn import_descriptors(
421 &self,
422 descriptors: Vec<ImportDescriptorInput>,
423 wallet_name: String,
424 ) -> ClientResult<ImportDescriptors> {
425 let wallet_args = CreateWalletArguments {
426 name: wallet_name,
427 load_on_startup: Some(true),
428 };
429
430 let _wallet_create = self
433 .call::<CreateWallet>("createwallet", &[to_value(wallet_args.clone())?])
434 .await;
435 let _wallet_load = self
437 .call::<CreateWallet>("loadwallet", &[to_value(wallet_args)?])
438 .await;
439
440 let result = self
441 .call::<ImportDescriptors>("importdescriptors", &[to_value(descriptors)?])
442 .await?;
443 Ok(result)
444 }
445
446 async fn wallet_process_psbt(
447 &self,
448 psbt: &str,
449 sign: Option<bool>,
450 sighashtype: Option<SighashType>,
451 bip32_derivs: Option<bool>,
452 ) -> ClientResult<model::WalletProcessPsbt> {
453 let mut params = vec![to_value(psbt)?, to_value(sign.unwrap_or(true))?];
454
455 if let Some(sighashtype) = sighashtype {
456 params.push(to_value(sighashtype)?);
457 }
458
459 if let Some(bip32_derivs) = bip32_derivs {
460 params.push(to_value(bip32_derivs)?);
461 }
462
463 let resp = self
464 .call::<WalletProcessPsbt>("walletprocesspsbt", ¶ms)
465 .await?;
466 resp.into_model()
467 .map_err(|e| ClientError::Parse(e.to_string()))
468 }
469
470 async fn psbt_bump_fee(
471 &self,
472 txid: &Txid,
473 options: Option<PsbtBumpFeeOptions>,
474 ) -> ClientResult<model::PsbtBumpFee> {
475 let mut params = vec![to_value(txid.to_string())?];
476
477 if let Some(options) = options {
478 params.push(to_value(options)?);
479 }
480
481 let resp = self.call::<PsbtBumpFee>("psbtbumpfee", ¶ms).await?;
482 resp.into_model()
483 .map_err(|e| ClientError::Parse(e.to_string()))
484 }
485}
486
487#[cfg(test)]
488mod test {
489
490 use std::sync::Once;
491
492 use bitcoin::{hashes::Hash, transaction, Amount, FeeRate, NetworkKind};
493 use corepc_types::v29::ImportDescriptorsResult;
494 use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
495
496 use super::*;
497 use crate::{
498 test_utils::corepc_node_helpers::{get_bitcoind_and_client, mine_blocks},
499 types::{CreateRawTransactionInput, CreateRawTransactionOutput},
500 Auth,
501 };
502
503 const COINBASE_AMOUNT: Amount = Amount::from_sat(50 * 100_000_000);
505
506 fn init_tracing() {
508 static INIT: Once = Once::new();
509
510 INIT.call_once(|| {
511 tracing_subscriber::registry()
512 .with(fmt::layer())
513 .with(EnvFilter::from_default_env())
514 .try_init()
515 .ok();
516 });
517 }
518
519 #[tokio::test()]
520 async fn client_works() {
521 init_tracing();
522
523 let (bitcoind, client) = get_bitcoind_and_client();
524
525 let got = client.network().await.unwrap();
527 let expected = Network::Regtest;
528
529 assert_eq!(expected, got);
530 let get_blockchain_info = client.get_blockchain_info().await.unwrap();
532 assert_eq!(get_blockchain_info.blocks, 0);
533
534 let _ = client
536 .get_current_timestamp()
537 .await
538 .expect("must be able to get current timestamp");
539
540 let blocks = mine_blocks(&bitcoind, 101, None).unwrap();
541
542 let expected = blocks.last().unwrap();
544 let got = client.get_block(expected).await.unwrap().block_hash();
545 assert_eq!(*expected, got);
546
547 let target_height = blocks.len() as u64;
549 let expected = blocks.last().unwrap();
550 let got = client
551 .get_block_at(target_height)
552 .await
553 .unwrap()
554 .block_hash();
555 assert_eq!(*expected, got);
556
557 let expected = blocks.len() as u64;
559 let got = client.get_block_count().await.unwrap();
560 assert_eq!(expected, got);
561
562 let target_height = blocks.len() as u64;
564 let expected = blocks.last().unwrap();
565 let got = client.get_block_hash(target_height).await.unwrap();
566 assert_eq!(*expected, got);
567
568 let target_height = blocks.len() as u64;
570 let expected = blocks.last().unwrap();
571 let got = client.get_block_header_at(target_height).await.unwrap();
572 assert_eq!(*expected, got.block_hash());
573
574 let address = client.get_new_address().await.unwrap();
576 let txid = client
577 .call::<String>(
578 "sendtoaddress",
579 &[to_value(address.to_string()).unwrap(), to_value(1).unwrap()],
580 )
581 .await
582 .unwrap()
583 .parse::<Txid>()
584 .unwrap();
585
586 let tx = client.get_transaction(&txid).await.unwrap().tx;
588 let got = client.send_raw_transaction(&tx).await.unwrap();
589 let expected = txid; assert_eq!(expected, got);
591
592 let got = client
594 .get_raw_transaction_verbosity_zero(&txid)
595 .await
596 .unwrap()
597 .0
598 .compute_txid();
599 assert_eq!(expected, got);
600
601 let got = client
603 .get_raw_transaction_verbosity_one(&txid)
604 .await
605 .unwrap()
606 .transaction
607 .compute_txid();
608 assert_eq!(expected, got);
609
610 let got = client.get_raw_mempool().await.unwrap();
612 let expected = vec![txid];
613 assert_eq!(expected, got.0);
614
615 let got = client.get_raw_mempool_verbose().await.unwrap();
617 assert_eq!(got.0.len(), 1);
618 assert_eq!(got.0.get(&txid).unwrap().height, 101);
619
620 let got = client.get_mempool_info().await.unwrap();
622 assert!(got.loaded.unwrap_or(false));
623 assert_eq!(got.size, 1);
624 assert_eq!(got.unbroadcast_count, Some(1));
625
626 let got = client.estimate_smart_fee(1).await.unwrap();
628 let expected = 1; assert_eq!(expected, got);
630
631 let got = client
633 .sign_raw_transaction_with_wallet(&tx, None)
634 .await
635 .unwrap();
636 assert!(got.complete);
637 assert!(got.errors.is_empty());
638
639 let txids = client
641 .test_mempool_accept(&tx)
642 .await
643 .expect("must be able to test mempool accept");
644 let got = txids
645 .results
646 .first()
647 .expect("there must be at least one txid");
648 assert_eq!(
649 got.txid,
650 tx.compute_txid(),
651 "txids must match in the mempool"
652 );
653
654 let got = client.send_raw_transaction(&tx).await.unwrap();
656 assert!(got.as_byte_array().len() == 32);
657
658 let got = client.list_transactions(None).await.unwrap();
660 assert_eq!(got.0.len(), 10);
661
662 mine_blocks(&bitcoind, 1, None).unwrap();
665 let got = client
666 .list_unspent(None, None, None, None, None)
667 .await
668 .unwrap();
669 assert_eq!(got.0.len(), 3);
670
671 let got = client.get_xpriv().await.unwrap().unwrap().network;
673 let expected = NetworkKind::Test;
674 assert_eq!(expected, got);
675
676 let descriptor_string = "tr([e61b318f/20000'/20']tprv8ZgxMBicQKsPd4arFr7sKjSnKFDVMR2JHw9Y8L9nXN4kiok4u28LpHijEudH3mMYoL4pM5UL9Bgdz2M4Cy8EzfErmU9m86ZTw6hCzvFeTg7/101/*)#2plamwqs".to_owned();
679 let timestamp = "now".to_owned();
680 let list_descriptors = vec![ImportDescriptorInput {
681 desc: descriptor_string,
682 active: Some(true),
683 timestamp,
684 }];
685 let got = client
686 .import_descriptors(list_descriptors, "strata".to_owned())
687 .await
688 .unwrap()
689 .0;
690 let expected = vec![ImportDescriptorsResult {
691 success: true,
692 warnings: Some(vec![
693 "Range not given, using default keypool range".to_string()
694 ]),
695 error: None,
696 }];
697 assert_eq!(expected, got);
698
699 let psbt_address = client.get_new_address().await.unwrap();
700 let psbt_outputs = vec![CreateRawTransactionOutput::AddressAmount {
701 address: psbt_address.to_string(),
702 amount: 1.0,
703 }];
704
705 let funded_psbt = client
706 .wallet_create_funded_psbt(&[], &psbt_outputs, None, None, None)
707 .await
708 .unwrap();
709 assert!(!funded_psbt.psbt.inputs.is_empty());
710 assert!(funded_psbt.fee.to_sat() > 0);
711
712 let processed_psbt = client
713 .wallet_process_psbt(&funded_psbt.psbt.to_string(), None, None, None)
714 .await
715 .unwrap();
716 assert!(!processed_psbt.psbt.inputs.is_empty());
717 assert!(processed_psbt.complete);
718
719 let finalized_psbt = client
720 .wallet_process_psbt(&funded_psbt.psbt.to_string(), Some(true), None, None)
721 .await
722 .unwrap();
723 assert!(finalized_psbt.complete);
724 assert!(finalized_psbt.hex.is_some());
725 let signed_tx = finalized_psbt.hex.as_ref().unwrap();
726 let signed_txid = signed_tx.compute_txid();
727 let got = client
728 .test_mempool_accept(signed_tx)
729 .await
730 .unwrap()
731 .results
732 .first()
733 .unwrap()
734 .txid;
735 assert_eq!(signed_txid, got);
736
737 let info_address = client.get_new_address().await.unwrap();
738 let address_info = client.get_address_info(&info_address).await.unwrap();
739 assert_eq!(address_info.address, info_address.as_unchecked().clone());
740 assert!(address_info.is_mine);
741 assert!(address_info.solvable.unwrap_or(false));
742
743 let unspent_address = client.get_new_address().await.unwrap();
744 let unspent_txid = client
745 .call::<String>(
746 "sendtoaddress",
747 &[
748 to_value(unspent_address.to_string()).unwrap(),
749 to_value(1.0).unwrap(),
750 ],
751 )
752 .await
753 .unwrap();
754 mine_blocks(&bitcoind, 1, None).unwrap();
755
756 let utxos = client
757 .list_unspent(Some(1), Some(9_999_999), None, Some(true), None)
758 .await
759 .unwrap();
760 assert!(!utxos.0.is_empty());
761
762 let utxos_filtered = client
763 .list_unspent(
764 Some(1),
765 Some(9_999_999),
766 Some(std::slice::from_ref(&unspent_address)),
767 Some(true),
768 None,
769 )
770 .await
771 .unwrap();
772 assert!(!utxos_filtered.0.is_empty());
773 let found_utxo = utxos_filtered.0.iter().any(|utxo| {
774 utxo.txid.to_string() == unspent_txid
775 && utxo.address.clone().assume_checked().to_string() == unspent_address.to_string()
776 });
777 assert!(found_utxo);
778
779 let query_options = ListUnspentQueryOptions {
780 minimum_amount: Some(Amount::from_btc(0.5).unwrap()),
781 maximum_amount: Some(Amount::from_btc(2.0).unwrap()),
782 maximum_count: Some(10),
783 };
784 let utxos_with_query = client
785 .list_unspent(
786 Some(1),
787 Some(9_999_999),
788 None,
789 Some(true),
790 Some(query_options),
791 )
792 .await
793 .unwrap();
794 assert!(!utxos_with_query.0.is_empty());
795 for utxo in &utxos_with_query.0 {
796 let amount_btc = utxo.amount.to_btc();
797 assert!((0.5..=2.0).contains(&amount_btc));
798 }
799
800 let tx = finalized_psbt.hex.unwrap();
801 assert!(!tx.input.is_empty());
802 assert!(!tx.output.is_empty());
803 }
804
805 #[tokio::test()]
806 async fn get_tx_out() {
807 init_tracing();
808
809 let (bitcoind, client) = get_bitcoind_and_client();
810
811 let got = client.network().await.unwrap();
813 let expected = Network::Regtest;
814 assert_eq!(expected, got);
815
816 let address = bitcoind.client.new_address().unwrap();
817 let blocks = mine_blocks(&bitcoind, 101, Some(address)).unwrap();
818 let last_block = client.get_block(blocks.first().unwrap()).await.unwrap();
819 let coinbase_tx = last_block.coinbase().unwrap();
820
821 let got = client
823 .get_tx_out(&coinbase_tx.compute_txid(), 0, true)
824 .await
825 .unwrap();
826 assert_eq!(got.tx_out.value, COINBASE_AMOUNT);
827
828 let new_address = bitcoind.client.new_address().unwrap();
830 let send_amount = Amount::from_sat(COINBASE_AMOUNT.to_sat() - 2_000); let _send_tx = bitcoind
832 .client
833 .send_to_address(&new_address, send_amount)
834 .unwrap()
835 .txid()
836 .unwrap();
837 let result = client
838 .get_tx_out(&coinbase_tx.compute_txid(), 0, true)
839 .await;
840 trace!(?result, "gettxout result");
841 assert!(result.is_err());
842 }
843
844 #[tokio::test()]
851 async fn submit_package() {
852 init_tracing();
853
854 let (bitcoind, client) = get_bitcoind_and_client();
855
856 let got = client.network().await.unwrap();
858 let expected = Network::Regtest;
859 assert_eq!(expected, got);
860
861 let blocks = mine_blocks(&bitcoind, 101, None).unwrap();
862 let last_block = client.get_block(blocks.first().unwrap()).await.unwrap();
863 let coinbase_tx = last_block.coinbase().unwrap();
864
865 let destination = client.get_new_address().await.unwrap();
866 let change_address = client.get_new_address().await.unwrap();
867 let amount = Amount::from_btc(1.0).unwrap();
868 let fees = Amount::from_btc(0.0001).unwrap();
869 let change_amount = COINBASE_AMOUNT - amount - fees;
870 let amount_minus_fees = Amount::from_sat(amount.to_sat() - 2_000);
871
872 let send_back_address = client.get_new_address().await.unwrap();
873 let parent_raw_tx = CreateRawTransactionArguments {
874 inputs: vec![CreateRawTransactionInput {
875 txid: coinbase_tx.compute_txid().to_string(),
876 vout: 0,
877 }],
878 outputs: vec![
879 CreateRawTransactionOutput::AddressAmount {
881 address: destination.to_string(),
882 amount: amount.to_btc(),
883 },
884 CreateRawTransactionOutput::AddressAmount {
886 address: change_address.to_string(),
887 amount: change_amount.to_btc(),
888 },
889 ],
890 };
891 let parent = client.create_raw_transaction(parent_raw_tx).await.unwrap();
892 let signed_parent = client
893 .sign_raw_transaction_with_wallet(&parent, None)
894 .await
895 .unwrap()
896 .tx;
897
898 let parent_submitted = client.send_raw_transaction(&signed_parent).await.unwrap();
900
901 let child_raw_tx = CreateRawTransactionArguments {
902 inputs: vec![CreateRawTransactionInput {
903 txid: parent_submitted.to_string(),
904 vout: 0,
905 }],
906 outputs: vec![
907 CreateRawTransactionOutput::AddressAmount {
909 address: send_back_address.to_string(),
910 amount: amount_minus_fees.to_btc(),
911 },
912 ],
913 };
914 let child = client.create_raw_transaction(child_raw_tx).await.unwrap();
915 let signed_child = client
916 .sign_raw_transaction_with_wallet(&child, None)
917 .await
918 .unwrap()
919 .tx;
920
921 let result = client
923 .submit_package(&[signed_parent, signed_child])
924 .await
925 .unwrap();
926 assert_eq!(result.tx_results.len(), 2);
927 assert_eq!(result.package_msg, "success");
928 }
929
930 #[tokio::test]
937 async fn submit_package_1p1c() {
938 init_tracing();
939
940 let (bitcoind, client) = get_bitcoind_and_client();
941
942 let server_version = bitcoind.client.server_version().unwrap();
944 assert!(server_version > 28);
945
946 let destination = client.get_new_address().await.unwrap();
947
948 let blocks = mine_blocks(&bitcoind, 101, None).unwrap();
949 let last_block = client.get_block(blocks.first().unwrap()).await.unwrap();
950 let coinbase_tx = last_block.coinbase().unwrap();
951
952 let parent_raw_tx = CreateRawTransactionArguments {
953 inputs: vec![CreateRawTransactionInput {
954 txid: coinbase_tx.compute_txid().to_string(),
955 vout: 0,
956 }],
957 outputs: vec![CreateRawTransactionOutput::AddressAmount {
958 address: destination.to_string(),
959 amount: COINBASE_AMOUNT.to_btc(),
960 }],
961 };
962 let mut parent = client.create_raw_transaction(parent_raw_tx).await.unwrap();
963 parent.version = transaction::Version(3);
964 assert_eq!(parent.version, transaction::Version(3));
965 trace!(?parent, "parent:");
966 let signed_parent = client
967 .sign_raw_transaction_with_wallet(&parent, None)
968 .await
969 .unwrap()
970 .tx;
971 assert_eq!(signed_parent.version, transaction::Version(3));
972
973 let parent_broadcasted = client.send_raw_transaction(&signed_parent).await;
975 assert!(parent_broadcasted.is_err());
976
977 let amount_minus_fees = Amount::from_sat(COINBASE_AMOUNT.to_sat() - 43_000);
979 let child_raw_tx = CreateRawTransactionArguments {
980 inputs: vec![CreateRawTransactionInput {
981 txid: signed_parent.compute_txid().to_string(),
982 vout: 0,
983 }],
984 outputs: vec![CreateRawTransactionOutput::AddressAmount {
985 address: destination.to_string(),
986 amount: amount_minus_fees.to_btc(),
987 }],
988 };
989 let mut child = client.create_raw_transaction(child_raw_tx).await.unwrap();
990 child.version = transaction::Version(3);
991 assert_eq!(child.version, transaction::Version(3));
992 trace!(?child, "child:");
993 let prev_outputs = vec![PreviousTransactionOutput {
994 txid: parent.compute_txid(),
995 vout: 0,
996 script_pubkey: parent.output[0].script_pubkey.to_hex_string(),
997 redeem_script: None,
998 witness_script: None,
999 amount: Some(COINBASE_AMOUNT.to_btc()),
1000 }];
1001 let signed_child = client
1002 .sign_raw_transaction_with_wallet(&child, Some(prev_outputs))
1003 .await
1004 .unwrap()
1005 .tx;
1006 assert_eq!(signed_child.version, transaction::Version(3));
1007
1008 let child_broadcasted = client.send_raw_transaction(&signed_child).await;
1010 assert!(child_broadcasted.is_err());
1011
1012 let result = client
1014 .submit_package(&[signed_parent, signed_child])
1015 .await
1016 .unwrap();
1017 assert_eq!(result.tx_results.len(), 2);
1018 assert_eq!(result.package_msg, "success");
1019 }
1020
1021 #[tokio::test]
1022 async fn test_invalid_credentials_return_401_error() {
1023 init_tracing();
1024
1025 let (bitcoind, _) = get_bitcoind_and_client();
1026 let url = bitcoind.rpc_url();
1027
1028 let auth = Auth::UserPass("wrong_user".to_string(), "wrong_password".to_string());
1029 let invalid_client = Client::new(url, auth, None, None, None).unwrap();
1030
1031 let result = invalid_client.get_blockchain_info().await;
1033
1034 assert!(result.is_err());
1036 let error = result.unwrap_err();
1037
1038 match error {
1039 ClientError::Status(status_code, message) => {
1040 assert_eq!(status_code, 401);
1041 assert!(message.contains("Unauthorized"));
1042 }
1043 _ => panic!("Expected Status(401, _) error, but got: {error:?}"),
1044 }
1045 }
1046
1047 #[tokio::test]
1048 async fn test_send_raw_transaction_exposes_rpc_error_code_on_http_500() {
1049 init_tracing();
1050
1051 let (_bitcoind, client) = get_bitcoind_and_client();
1052
1053 let result = client
1054 .call::<String>("sendrawtransaction", &[to_value("deadbeef").unwrap()])
1055 .await;
1056
1057 match result {
1058 Err(ClientError::Server(code, message)) => {
1059 assert_eq!(code, -22);
1060 assert!(
1061 message.to_lowercase().contains("decode"),
1062 "expected decode-related RPC error message, got: {message}"
1063 );
1064 }
1065 other => panic!("Expected Server(-22, _), got: {other:?}"),
1066 }
1067 }
1068
1069 #[tokio::test]
1070 async fn test_get_raw_transaction_exposes_rpc_error_code_on_http_500() {
1071 init_tracing();
1072
1073 let (_bitcoind, client) = get_bitcoind_and_client();
1074 let missing_txid = Txid::from_slice(&[0u8; 32]).expect("must be a valid txid");
1075
1076 let error = client
1077 .get_raw_transaction_verbosity_zero(&missing_txid)
1078 .await
1079 .expect_err("missing txid must fail");
1080
1081 assert!(
1082 !matches!(error, ClientError::Status(..) | ClientError::Parse(..)),
1083 "expected parsed RPC error, got transport/parsing error: {error:?}"
1084 );
1085 assert!(
1086 error.is_tx_not_found(),
1087 "expected tx-not-found classification, got: {error:?}"
1088 );
1089 }
1090
1091 #[tokio::test]
1092 async fn psbt_bump_fee() {
1093 init_tracing();
1094
1095 let (bitcoind, client) = get_bitcoind_and_client();
1096
1097 mine_blocks(&bitcoind, 101, None).unwrap();
1099
1100 let destination = client.get_new_address().await.unwrap();
1102 let amount = Amount::from_btc(0.001).unwrap(); let txid = bitcoind
1106 .client
1107 .send_to_address_rbf(&destination, amount)
1108 .unwrap()
1109 .txid()
1110 .unwrap();
1111
1112 let mempool = client.get_raw_mempool().await.unwrap();
1114 assert!(
1115 mempool.0.contains(&txid),
1116 "Transaction should be in mempool for RBF"
1117 );
1118
1119 let signed_tx = client
1121 .psbt_bump_fee(&txid, None)
1122 .await
1123 .unwrap()
1124 .psbt
1125 .extract_tx()
1126 .unwrap();
1127 let signed_txid = signed_tx.compute_txid();
1128 let got = client
1129 .test_mempool_accept(&signed_tx)
1130 .await
1131 .unwrap()
1132 .results
1133 .first()
1134 .unwrap()
1135 .txid;
1136 assert_eq!(
1137 got, signed_txid,
1138 "Bumped transaction should be accepted in mempool"
1139 );
1140
1141 let options = PsbtBumpFeeOptions {
1143 fee_rate: Some(FeeRate::from_sat_per_kwu(20)), ..Default::default()
1145 };
1146 trace!(?options, "Calling psbt_bump_fee");
1147 let signed_tx = client
1148 .psbt_bump_fee(&txid, Some(options))
1149 .await
1150 .unwrap()
1151 .psbt
1152 .extract_tx()
1153 .unwrap();
1154 let signed_txid = signed_tx.compute_txid();
1155 let got = client
1156 .test_mempool_accept(&signed_tx)
1157 .await
1158 .unwrap()
1159 .results
1160 .first()
1161 .unwrap()
1162 .txid;
1163 assert_eq!(
1164 got, signed_txid,
1165 "Bumped transaction should be accepted in mempool"
1166 );
1167 }
1168
1169 #[cfg(feature = "raw_rpc")]
1170 #[tokio::test]
1171 async fn call_raw() {
1172 init_tracing();
1173
1174 let (bitcoind, client) = get_bitcoind_and_client();
1175
1176 mine_blocks(&bitcoind, 5, None).unwrap();
1177
1178 let expected = client.get_block_count().await.unwrap();
1179
1180 let got: u64 = client.call_raw("getblockcount", &[]).await.unwrap();
1181
1182 assert_eq!(expected, got);
1183
1184 let height = 0;
1185
1186 let expected_hash = client.get_block_hash(height).await.unwrap();
1187
1188 let got_hash: BlockHash = client
1189 .call_raw("getblockhash", &[to_value(height).unwrap()])
1190 .await
1191 .unwrap();
1192
1193 assert_eq!(expected_hash, got_hash);
1194 }
1195
1196 #[test]
1197 fn test_network_chain_response() {
1198 let test_cases = vec![
1199 ("main", Network::Bitcoin),
1200 ("test", Network::Testnet),
1201 ("testnet4", Network::Testnet4),
1202 ("signet", Network::Signet),
1203 ("regtest", Network::Regtest),
1204 ];
1205
1206 for (bitcoind_chain_str, expected_network) in test_cases {
1207 let result = Network::from_core_arg(bitcoind_chain_str);
1208 assert!(result.is_ok(), "failed for chain: {}", bitcoind_chain_str);
1209 assert_eq!(result.unwrap(), expected_network);
1210 }
1211 }
1212}