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::v30::{
13 CreateWallet, EstimateSmartFee, GetAddressInfo, GetBlockHeader, GetBlockVerboseOne,
14 GetBlockVerboseZero, GetBlockchainInfo, GetMempoolInfo, GetNewAddress, GetRawMempool,
15 GetRawMempoolVerbose, GetRawTransaction, GetRawTransactionVerbose, GetTransaction, GetTxOut,
16 ImportDescriptors, ListDescriptors, ListTransactions, ListUnspent, PsbtBumpFee,
17 SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, WalletCreateFundedPsbt,
18 WalletProcessPsbt,
19};
20use tracing::*;
21
22use crate::{
23 client::Client,
24 error::ClientError,
25 to_value,
26 traits::{Broadcaster, Reader, Signer, Wallet},
27 types::{
28 BroadcastOptions, CreateRawTransactionArguments, CreateRawTransactionInput,
29 CreateRawTransactionOutput, CreateWalletArguments, ImportDescriptorInput,
30 ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFeeOptions,
31 SendRawTransactionOptions, SighashType, WalletCreateFundedPsbtOptions,
32 },
33 ClientResult,
34};
35
36impl Reader for Client {
37 async fn estimate_smart_fee(&self, conf_target: u16) -> ClientResult<model::EstimateSmartFee> {
38 let resp = self
39 .call::<EstimateSmartFee>("estimatesmartfee", &[to_value(conf_target)?])
40 .await?;
41
42 resp.into_model()
43 .map_err(|e| ClientError::Parse(e.to_string()))
44 }
45
46 async fn get_block_header(&self, hash: &BlockHash) -> ClientResult<Header> {
47 let get_block_header = self
48 .call::<GetBlockHeader>(
49 "getblockheader",
50 &[to_value(hash.to_string())?, to_value(false)?],
51 )
52 .await?;
53 let header = get_block_header
54 .block_header()
55 .map_err(|err| ClientError::Other(format!("header decode: {err}")))?;
56 Ok(header)
57 }
58
59 async fn get_block(&self, hash: &BlockHash) -> ClientResult<Block> {
60 let get_block = self
61 .call::<GetBlockVerboseZero>("getblock", &[to_value(hash.to_string())?, to_value(0)?])
62 .await?;
63 let block = get_block
64 .into_model()
65 .map_err(|e| ClientError::Parse(e.to_string()))?
66 .0;
67 Ok(block)
68 }
69
70 async fn get_block_height(&self, hash: &BlockHash) -> ClientResult<u64> {
71 let block_verobose = self
72 .call::<GetBlockVerboseOne>("getblock", &[to_value(hash.to_string())?])
73 .await?;
74
75 let block_height = block_verobose.height as u64;
76 Ok(block_height)
77 }
78
79 async fn get_block_header_at(&self, height: u64) -> ClientResult<Header> {
80 let hash = self.get_block_hash(height).await?;
81 self.get_block_header(&hash).await
82 }
83
84 async fn get_block_at(&self, height: u64) -> ClientResult<Block> {
85 let hash = self.get_block_hash(height).await?;
86 self.get_block(&hash).await
87 }
88
89 async fn get_block_count(&self) -> ClientResult<u64> {
90 self.call::<u64>("getblockcount", &[]).await
91 }
92
93 async fn get_block_hash(&self, height: u64) -> ClientResult<BlockHash> {
94 self.call::<BlockHash>("getblockhash", &[to_value(height)?])
95 .await
96 }
97
98 async fn get_blockchain_info(&self) -> ClientResult<model::GetBlockchainInfo> {
99 let res = self
100 .call::<GetBlockchainInfo>("getblockchaininfo", &[])
101 .await?;
102 res.into_model()
103 .map_err(|e| ClientError::Parse(e.to_string()))
104 }
105
106 async fn get_current_timestamp(&self) -> ClientResult<u32> {
107 let best_block_hash = self.call::<BlockHash>("getbestblockhash", &[]).await?;
108 let block = self.get_block(&best_block_hash).await?;
109 Ok(block.header.time)
110 }
111
112 async fn get_raw_mempool(&self) -> ClientResult<model::GetRawMempool> {
113 let resp = self.call::<GetRawMempool>("getrawmempool", &[]).await?;
114 resp.into_model()
115 .map_err(|e| ClientError::Parse(e.to_string()))
116 }
117
118 async fn get_raw_mempool_verbose(&self) -> ClientResult<model::GetRawMempoolVerbose> {
119 let resp = self
120 .call::<GetRawMempoolVerbose>("getrawmempool", &[to_value(true)?])
121 .await?;
122
123 resp.into_model()
124 .map_err(|e| ClientError::Parse(e.to_string()))
125 }
126
127 async fn get_mempool_info(&self) -> ClientResult<model::GetMempoolInfo> {
128 let resp = self.call::<GetMempoolInfo>("getmempoolinfo", &[]).await?;
129 resp.into_model()
130 .map_err(|e| ClientError::Parse(e.to_string()))
131 }
132
133 async fn get_raw_transaction_verbosity_zero(
134 &self,
135 txid: &Txid,
136 ) -> ClientResult<model::GetRawTransaction> {
137 let resp = self
138 .call::<GetRawTransaction>(
139 "getrawtransaction",
140 &[to_value(txid.to_string())?, to_value(0)?],
141 )
142 .await?;
143 resp.into_model()
144 .map_err(|e| ClientError::Parse(e.to_string()))
145 }
146
147 async fn get_raw_transaction_verbosity_one(
148 &self,
149 txid: &Txid,
150 ) -> ClientResult<model::GetRawTransactionVerbose> {
151 let resp = self
152 .call::<GetRawTransactionVerbose>(
153 "getrawtransaction",
154 &[to_value(txid.to_string())?, to_value(1)?],
155 )
156 .await?;
157 resp.into_model()
158 .map_err(|e| ClientError::Parse(e.to_string()))
159 }
160
161 async fn get_tx_out(
162 &self,
163 txid: &Txid,
164 vout: u32,
165 include_mempool: bool,
166 ) -> ClientResult<model::GetTxOut> {
167 let resp = self
168 .call::<GetTxOut>(
169 "gettxout",
170 &[
171 to_value(txid.to_string())?,
172 to_value(vout)?,
173 to_value(include_mempool)?,
174 ],
175 )
176 .await?;
177 resp.into_model()
178 .map_err(|e| ClientError::Parse(e.to_string()))
179 }
180
181 async fn network(&self) -> ClientResult<Network> {
182 let chain = self
183 .call::<GetBlockchainInfo>("getblockchaininfo", &[])
184 .await?
185 .chain;
186 Network::from_core_arg(&chain).map_err(|e| ClientError::Parse(e.to_string()))
187 }
188}
189
190impl Broadcaster for Client {
191 async fn send_raw_transaction(
192 &self,
193 tx: &Transaction,
194 options: Option<SendRawTransactionOptions>,
195 ) -> ClientResult<Txid> {
196 let txstr = serialize_hex(tx);
197 trace!(txstr = %txstr, "Sending raw transaction");
198 let mut params = vec![to_value(txstr)?];
199 if let Some(options) = options {
200 params.extend(options.to_params());
201 }
202
203 match self.call::<Txid>("sendrawtransaction", ¶ms).await {
204 Ok(txid) => {
205 trace!(?txid, "Transaction sent");
206 Ok(txid)
207 }
208 Err(err @ ClientError::Server(_, _)) if err.is_rpc_verify_already_in_utxo_set() => {
209 Ok(tx.compute_txid())
210 }
211 Err(err @ ClientError::Server(_, _)) => Err(err),
212 Err(e) => Err(ClientError::Other(e.to_string())),
213 }
214 }
215
216 async fn test_mempool_accept(
217 &self,
218 tx: &Transaction,
219 ) -> ClientResult<model::TestMempoolAccept> {
220 let txstr = serialize_hex(tx);
221 trace!(%txstr, "Testing mempool accept");
222 let resp = self
223 .call::<TestMempoolAccept>("testmempoolaccept", &[to_value([txstr])?])
224 .await?;
225 resp.into_model()
226 .map_err(|e| ClientError::Parse(e.to_string()))
227 }
228
229 async fn submit_package(
230 &self,
231 txs: &[Transaction],
232 options: Option<BroadcastOptions>,
233 ) -> ClientResult<model::SubmitPackage> {
234 let txstrs: Vec<String> = txs.iter().map(serialize_hex).collect();
235 let mut params = vec![to_value(txstrs)?];
236 if let Some(options) = options {
237 params.extend(options.to_params());
238 }
239
240 let resp = self.call::<SubmitPackage>("submitpackage", ¶ms).await?;
241 trace!(?resp, "Got submit package response");
242
243 resp.into_model()
244 .map_err(|e| ClientError::Parse(e.to_string()))
245 }
246}
247
248impl Wallet for Client {
249 async fn get_new_address(&self) -> ClientResult<Address> {
250 let address_unchecked = self
251 .call::<GetNewAddress>("getnewaddress", &[])
252 .await?
253 .0
254 .parse::<Address<_>>()
255 .map_err(|e| ClientError::Parse(e.to_string()))?
256 .assume_checked();
257 Ok(address_unchecked)
258 }
259 async fn get_transaction(&self, txid: &Txid) -> ClientResult<model::GetTransaction> {
260 let resp = self
261 .call::<GetTransaction>("gettransaction", &[to_value(txid.to_string())?])
262 .await?;
263 resp.into_model()
264 .map_err(|e| ClientError::Parse(e.to_string()))
265 }
266
267 async fn list_transactions(
268 &self,
269 count: Option<usize>,
270 ) -> ClientResult<model::ListTransactions> {
271 let resp = self
272 .call::<ListTransactions>("listtransactions", &[to_value(count)?])
273 .await?;
274 resp.into_model()
275 .map_err(|e| ClientError::Parse(e.to_string()))
276 }
277
278 async fn list_wallets(&self) -> ClientResult<Vec<String>> {
279 self.call::<Vec<String>>("listwallets", &[]).await
280 }
281
282 async fn create_raw_transaction(
283 &self,
284 raw_tx: CreateRawTransactionArguments,
285 ) -> ClientResult<Transaction> {
286 let raw_tx = self
287 .call::<String>(
288 "createrawtransaction",
289 &[to_value(raw_tx.inputs)?, to_value(raw_tx.outputs)?],
290 )
291 .await?;
292 trace!(%raw_tx, "Created raw transaction");
293 consensus::encode::deserialize_hex(&raw_tx)
294 .map_err(|e| ClientError::Other(format!("Failed to deserialize raw transaction: {e}")))
295 }
296
297 async fn wallet_create_funded_psbt(
298 &self,
299 inputs: &[CreateRawTransactionInput],
300 outputs: &[CreateRawTransactionOutput],
301 locktime: Option<u32>,
302 options: Option<WalletCreateFundedPsbtOptions>,
303 bip32_derivs: Option<bool>,
304 ) -> ClientResult<model::WalletCreateFundedPsbt> {
305 let resp = self
306 .call::<WalletCreateFundedPsbt>(
307 "walletcreatefundedpsbt",
308 &[
309 to_value(inputs)?,
310 to_value(outputs)?,
311 to_value(locktime.unwrap_or(0))?,
312 to_value(options.unwrap_or_default())?,
313 to_value(bip32_derivs)?,
314 ],
315 )
316 .await?;
317 resp.into_model()
318 .map_err(|e| ClientError::Parse(e.to_string()))
319 }
320
321 async fn get_address_info(&self, address: &Address) -> ClientResult<model::GetAddressInfo> {
322 trace!(address = %address, "Getting address info");
323 let resp = self
324 .call::<GetAddressInfo>("getaddressinfo", &[to_value(address.to_string())?])
325 .await?;
326 resp.into_model()
327 .map_err(|e| ClientError::Parse(e.to_string()))
328 }
329
330 async fn list_unspent(
331 &self,
332 min_conf: Option<u32>,
333 max_conf: Option<u32>,
334 addresses: Option<&[Address]>,
335 include_unsafe: Option<bool>,
336 query_options: Option<ListUnspentQueryOptions>,
337 ) -> ClientResult<model::ListUnspent> {
338 let addr_strings: Vec<String> = addresses
339 .map(|addrs| addrs.iter().map(|a| a.to_string()).collect())
340 .unwrap_or_default();
341
342 let mut params = vec![
343 to_value(min_conf.unwrap_or(1))?,
344 to_value(max_conf.unwrap_or(9_999_999))?,
345 to_value(addr_strings)?,
346 to_value(include_unsafe.unwrap_or(true))?,
347 ];
348
349 if let Some(query_options) = query_options {
350 params.push(to_value(query_options)?);
351 }
352
353 let resp = self.call::<ListUnspent>("listunspent", ¶ms).await?;
354 trace!(?resp, "Got UTXOs");
355
356 resp.into_model()
357 .map_err(|e| ClientError::Parse(e.to_string()))
358 }
359}
360
361impl Signer for Client {
362 async fn sign_raw_transaction_with_wallet(
363 &self,
364 tx: &Transaction,
365 prev_outputs: Option<Vec<PreviousTransactionOutput>>,
366 ) -> ClientResult<model::SignRawTransactionWithWallet> {
367 let tx_hex = serialize_hex(tx);
368 trace!(tx_hex = %tx_hex, "Signing transaction");
369 trace!(?prev_outputs, "Signing transaction with previous outputs");
370 let resp = self
371 .call::<SignRawTransactionWithWallet>(
372 "signrawtransactionwithwallet",
373 &[to_value(tx_hex)?, to_value(prev_outputs)?],
374 )
375 .await?;
376 resp.into_model()
377 .map_err(|e| ClientError::Parse(e.to_string()))
378 }
379
380 async fn get_xpriv(&self) -> ClientResult<Option<Xpriv>> {
381 if var("BITCOIN_XPRIV_RETRIEVABLE").is_err() {
383 return Ok(None);
384 }
385
386 let descriptors = self
387 .call::<ListDescriptors>("listdescriptors", &[to_value(true)?]) .await?
389 .descriptors;
390 if descriptors.is_empty() {
391 return Err(ClientError::Other("No descriptors found".to_string()));
392 }
393
394 let descriptor = descriptors
396 .iter()
397 .find(|d| d.descriptor.contains("tr("))
398 .map(|d| d.descriptor.clone())
399 .ok_or(ClientError::Xpriv)?;
400
401 let xpriv_str = descriptor
403 .split("tr(")
404 .nth(1)
405 .ok_or(ClientError::Xpriv)?
406 .split("/")
407 .next()
408 .ok_or(ClientError::Xpriv)?;
409
410 let xpriv = xpriv_str.parse::<Xpriv>().map_err(|_| ClientError::Xpriv)?;
411 Ok(Some(xpriv))
412 }
413
414 async fn import_descriptors(
415 &self,
416 descriptors: Vec<ImportDescriptorInput>,
417 wallet_name: String,
418 ) -> ClientResult<ImportDescriptors> {
419 let wallet_args = CreateWalletArguments {
420 name: wallet_name,
421 load_on_startup: Some(true),
422 };
423
424 let _wallet_create = self
427 .call::<CreateWallet>("createwallet", &[to_value(wallet_args.clone())?])
428 .await;
429 let _wallet_load = self
431 .call::<CreateWallet>("loadwallet", &[to_value(wallet_args)?])
432 .await;
433
434 let result = self
435 .call::<ImportDescriptors>("importdescriptors", &[to_value(descriptors)?])
436 .await?;
437 Ok(result)
438 }
439
440 async fn wallet_process_psbt(
441 &self,
442 psbt: &str,
443 sign: Option<bool>,
444 sighashtype: Option<SighashType>,
445 bip32_derivs: Option<bool>,
446 ) -> ClientResult<model::WalletProcessPsbt> {
447 let mut params = vec![to_value(psbt)?, to_value(sign.unwrap_or(true))?];
448
449 if let Some(sighashtype) = sighashtype {
450 params.push(to_value(sighashtype)?);
451 }
452
453 if let Some(bip32_derivs) = bip32_derivs {
454 params.push(to_value(bip32_derivs)?);
455 }
456
457 let resp = self
458 .call::<WalletProcessPsbt>("walletprocesspsbt", ¶ms)
459 .await?;
460 resp.into_model()
461 .map_err(|e| ClientError::Parse(e.to_string()))
462 }
463
464 async fn psbt_bump_fee(
465 &self,
466 txid: &Txid,
467 options: Option<PsbtBumpFeeOptions>,
468 ) -> ClientResult<model::PsbtBumpFee> {
469 let mut params = vec![to_value(txid.to_string())?];
470
471 if let Some(options) = options {
472 params.push(to_value(options)?);
473 }
474
475 let resp = self.call::<PsbtBumpFee>("psbtbumpfee", ¶ms).await?;
476 resp.into_model()
477 .map_err(|e| ClientError::Parse(e.to_string()))
478 }
479}
480
481#[cfg(test)]
482mod test {
483
484 use std::{env, sync::Once, time::Duration};
485
486 use bitcoin::{
487 hashes::Hash, opcodes::all::OP_RETURN, script::Builder, transaction, Amount, FeeRate,
488 NetworkKind,
489 };
490 use corepc_node::{Conf, Node, P2P};
491 use corepc_types::v30::ImportDescriptorsResult;
492 use serde_json::Value;
493 use tokio::time::sleep;
494 use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
495
496 use super::*;
497 use crate::{
498 test_utils::corepc_node_helpers::{
499 assert_max_burn_amount_rejected, get_bitcoind_and_client, mine_blocks,
500 },
501 types::{
502 BroadcastOptions, CreateRawTransactionInput, CreateRawTransactionOutput,
503 SendRawTransactionOptions,
504 },
505 Auth,
506 };
507
508 const COINBASE_AMOUNT: Amount = Amount::from_sat(50 * 100_000_000);
510
511 const FEE_ESTIMATION_BLOCKS: usize = 5;
513
514 const FEE_ESTIMATION_TXS_PER_BLOCK: usize = 5;
516
517 const FEE_ESTIMATION_FEE_RATE: FeeRate = FeeRate::from_sat_per_kwu(500);
519
520 const FEE_ESTIMATION_WAIT_ATTEMPTS: usize = 1_200;
522
523 const FEE_ESTIMATION_WAIT_INTERVAL: Duration = Duration::from_millis(50);
525
526 fn init_tracing() {
528 static INIT: Once = Once::new();
529
530 INIT.call_once(|| {
531 tracing_subscriber::registry()
532 .with(fmt::layer())
533 .with(EnvFilter::from_default_env())
534 .try_init()
535 .ok();
536 });
537 }
538
539 fn get_p2p_bitcoind_and_client() -> (Node, Node, Client) {
541 unsafe {
542 env::set_var("BITCOIN_XPRIV_RETRIEVABLE", "true");
543 }
544
545 let mut estimator_conf = Conf::default();
546 estimator_conf.args.push("-txindex=1");
547 estimator_conf.p2p = P2P::Yes;
548 let estimator = Node::from_downloaded_with_conf(&estimator_conf).unwrap();
549
550 let mut broadcaster_conf = Conf::default();
551 broadcaster_conf.args.push("-txindex=1");
552 broadcaster_conf.p2p = estimator.p2p_connect(false).unwrap();
553 let broadcaster = Node::from_downloaded_with_conf(&broadcaster_conf).unwrap();
554
555 let client = Client::new(
556 estimator.rpc_url(),
557 Auth::CookieFile(estimator.params.cookie_file.clone()),
558 None,
559 None,
560 None,
561 )
562 .unwrap();
563
564 (estimator, broadcaster, client)
565 }
566
567 async fn wait_for_block_count(client: &Client, expected: u64) {
569 for _ in 0..FEE_ESTIMATION_WAIT_ATTEMPTS {
570 if client.get_block_count().await.unwrap() == expected {
571 return;
572 }
573 sleep(FEE_ESTIMATION_WAIT_INTERVAL).await;
574 }
575 panic!("timed out waiting for block height {expected}");
576 }
577
578 async fn wait_for_mempool_len(client: &Client, expected: usize) {
580 for _ in 0..FEE_ESTIMATION_WAIT_ATTEMPTS {
581 if client.get_raw_mempool().await.unwrap().0.len() >= expected {
582 return;
583 }
584 sleep(FEE_ESTIMATION_WAIT_INTERVAL).await;
585 }
586 panic!("timed out waiting for {expected} transactions in mempool");
587 }
588
589 async fn populate_fee_estimation_history(
591 estimator: &Node,
592 broadcaster: &Node,
593 estimator_client: &Client,
594 ) {
595 let funding_address = broadcaster.client.new_address().unwrap();
596 mine_blocks(broadcaster, 101, Some(funding_address)).unwrap();
597 wait_for_block_count(estimator_client, 101).await;
598
599 for _ in 0..FEE_ESTIMATION_BLOCKS {
600 for _ in 0..FEE_ESTIMATION_TXS_PER_BLOCK {
601 let address = broadcaster.client.new_address().unwrap();
602 let txid = broadcaster
603 .client
604 .call::<String>(
605 "sendtoaddress",
606 &[
607 to_value(address.to_string()).unwrap(),
608 to_value(0.1).unwrap(),
609 to_value("").unwrap(),
610 to_value("").unwrap(),
611 to_value(false).unwrap(),
612 to_value(true).unwrap(),
613 Value::Null,
614 to_value("unset").unwrap(),
615 Value::Null,
616 to_value(FEE_ESTIMATION_FEE_RATE.to_sat_per_kwu() as f64 / 250.0)
617 .unwrap(),
618 ],
619 )
620 .unwrap();
621 txid.parse::<Txid>().unwrap();
622 }
623
624 wait_for_mempool_len(estimator_client, FEE_ESTIMATION_TXS_PER_BLOCK).await;
625 mine_blocks(estimator, 1, None).unwrap();
626 }
627 }
628
629 #[tokio::test()]
630 async fn client_works() {
631 init_tracing();
632
633 let (bitcoind, client) = get_bitcoind_and_client();
634
635 let got = client.network().await.unwrap();
637 let expected = Network::Regtest;
638
639 assert_eq!(expected, got);
640 let get_blockchain_info = client.get_blockchain_info().await.unwrap();
642 assert_eq!(get_blockchain_info.blocks, 0);
643
644 let _ = client
646 .get_current_timestamp()
647 .await
648 .expect("must be able to get current timestamp");
649
650 let blocks = mine_blocks(&bitcoind, 101, None).unwrap();
651
652 let expected = blocks.last().unwrap();
654 let got = client.get_block(expected).await.unwrap().block_hash();
655 assert_eq!(*expected, got);
656
657 let target_height = blocks.len() as u64;
659 let expected = blocks.last().unwrap();
660 let got = client
661 .get_block_at(target_height)
662 .await
663 .unwrap()
664 .block_hash();
665 assert_eq!(*expected, got);
666
667 let expected = blocks.len() as u64;
669 let got = client.get_block_count().await.unwrap();
670 assert_eq!(expected, got);
671
672 let target_height = blocks.len() as u64;
674 let expected = blocks.last().unwrap();
675 let got = client.get_block_hash(target_height).await.unwrap();
676 assert_eq!(*expected, got);
677
678 let target_height = blocks.len() as u64;
680 let expected = blocks.last().unwrap();
681 let got = client.get_block_header_at(target_height).await.unwrap();
682 assert_eq!(*expected, got.block_hash());
683
684 let address = client.get_new_address().await.unwrap();
686 let txid = client
687 .call::<String>(
688 "sendtoaddress",
689 &[to_value(address.to_string()).unwrap(), to_value(1).unwrap()],
690 )
691 .await
692 .unwrap()
693 .parse::<Txid>()
694 .unwrap();
695
696 let tx = client.get_transaction(&txid).await.unwrap().tx;
698 let got = client.send_raw_transaction(&tx, None).await.unwrap();
699 let expected = txid; assert_eq!(expected, got);
701
702 let got = client
704 .get_raw_transaction_verbosity_zero(&txid)
705 .await
706 .unwrap()
707 .0
708 .compute_txid();
709 assert_eq!(expected, got);
710
711 let got = client
713 .get_raw_transaction_verbosity_one(&txid)
714 .await
715 .unwrap()
716 .transaction
717 .compute_txid();
718 assert_eq!(expected, got);
719
720 let got = client.get_raw_mempool().await.unwrap();
722 let expected = vec![txid];
723 assert_eq!(expected, got.0);
724
725 let got = client.get_raw_mempool_verbose().await.unwrap();
727 assert_eq!(got.0.len(), 1);
728 assert_eq!(got.0.get(&txid).unwrap().height, 101);
729
730 let got = client.get_mempool_info().await.unwrap();
732 assert!(got.loaded.unwrap_or(false));
733 assert_eq!(got.size, 1);
734 assert_eq!(got.unbroadcast_count, Some(1));
735
736 let got = client
738 .sign_raw_transaction_with_wallet(&tx, None)
739 .await
740 .unwrap();
741 assert!(got.complete);
742 assert!(got.errors.is_empty());
743
744 let txids = client
746 .test_mempool_accept(&tx)
747 .await
748 .expect("must be able to test mempool accept");
749 let got = txids
750 .results
751 .first()
752 .expect("there must be at least one txid");
753 assert_eq!(
754 got.txid,
755 tx.compute_txid(),
756 "txids must match in the mempool"
757 );
758
759 let got = client.send_raw_transaction(&tx, None).await.unwrap();
761 assert!(got.as_byte_array().len() == 32);
762
763 let got = client.list_transactions(None).await.unwrap();
765 assert_eq!(got.0.len(), 10);
766
767 mine_blocks(&bitcoind, 1, None).unwrap();
770 let got = client
771 .list_unspent(None, None, None, None, None)
772 .await
773 .unwrap();
774 assert_eq!(got.0.len(), 3);
775
776 let got = client.get_xpriv().await.unwrap().unwrap().network;
778 let expected = NetworkKind::Test;
779 assert_eq!(expected, got);
780
781 let descriptor_string = "tr([e61b318f/20000'/20']tprv8ZgxMBicQKsPd4arFr7sKjSnKFDVMR2JHw9Y8L9nXN4kiok4u28LpHijEudH3mMYoL4pM5UL9Bgdz2M4Cy8EzfErmU9m86ZTw6hCzvFeTg7/101/*)#2plamwqs".to_owned();
784 let timestamp = "now".to_owned();
785 let list_descriptors = vec![ImportDescriptorInput {
786 desc: descriptor_string,
787 active: Some(true),
788 timestamp,
789 }];
790 let got = client
791 .import_descriptors(list_descriptors, "strata".to_owned())
792 .await
793 .unwrap()
794 .0;
795 let expected = vec![ImportDescriptorsResult {
796 success: true,
797 warnings: Some(vec![
798 "Range not given, using default keypool range".to_string()
799 ]),
800 error: None,
801 }];
802 assert_eq!(expected, got);
803
804 let psbt_address = client.get_new_address().await.unwrap();
805 let psbt_outputs = vec![CreateRawTransactionOutput::AddressAmount {
806 address: psbt_address.to_string(),
807 amount: 1.0,
808 }];
809
810 let funded_psbt = client
811 .wallet_create_funded_psbt(&[], &psbt_outputs, None, None, None)
812 .await
813 .unwrap();
814 assert!(!funded_psbt.psbt.inputs.is_empty());
815 assert!(funded_psbt.fee.to_sat() > 0);
816
817 let processed_psbt = client
818 .wallet_process_psbt(&funded_psbt.psbt.to_string(), None, None, None)
819 .await
820 .unwrap();
821 assert!(!processed_psbt.psbt.inputs.is_empty());
822 assert!(processed_psbt.complete);
823
824 let finalized_psbt = client
825 .wallet_process_psbt(&funded_psbt.psbt.to_string(), Some(true), None, None)
826 .await
827 .unwrap();
828 assert!(finalized_psbt.complete);
829 assert!(finalized_psbt.hex.is_some());
830 let signed_tx = finalized_psbt.hex.as_ref().unwrap();
831 let signed_txid = signed_tx.compute_txid();
832 let got = client
833 .test_mempool_accept(signed_tx)
834 .await
835 .unwrap()
836 .results
837 .first()
838 .unwrap()
839 .txid;
840 assert_eq!(signed_txid, got);
841
842 let info_address = client.get_new_address().await.unwrap();
843 let address_info = client.get_address_info(&info_address).await.unwrap();
844 assert_eq!(address_info.address, info_address.as_unchecked().clone());
845 assert!(address_info.is_mine);
846 assert!(address_info.solvable.unwrap_or(false));
847
848 let unspent_address = client.get_new_address().await.unwrap();
849 let unspent_txid = client
850 .call::<String>(
851 "sendtoaddress",
852 &[
853 to_value(unspent_address.to_string()).unwrap(),
854 to_value(1.0).unwrap(),
855 ],
856 )
857 .await
858 .unwrap();
859 mine_blocks(&bitcoind, 1, None).unwrap();
860
861 let utxos = client
862 .list_unspent(Some(1), Some(9_999_999), None, Some(true), None)
863 .await
864 .unwrap();
865 assert!(!utxos.0.is_empty());
866
867 let utxos_filtered = client
868 .list_unspent(
869 Some(1),
870 Some(9_999_999),
871 Some(std::slice::from_ref(&unspent_address)),
872 Some(true),
873 None,
874 )
875 .await
876 .unwrap();
877 assert!(!utxos_filtered.0.is_empty());
878 let found_utxo = utxos_filtered.0.iter().any(|utxo| {
879 utxo.txid.to_string() == unspent_txid
880 && utxo.address.clone().assume_checked().to_string() == unspent_address.to_string()
881 });
882 assert!(found_utxo);
883
884 let query_options = ListUnspentQueryOptions {
885 minimum_amount: Some(Amount::from_btc(0.5).unwrap()),
886 maximum_amount: Some(Amount::from_btc(2.0).unwrap()),
887 maximum_count: Some(10),
888 };
889 let utxos_with_query = client
890 .list_unspent(
891 Some(1),
892 Some(9_999_999),
893 None,
894 Some(true),
895 Some(query_options),
896 )
897 .await
898 .unwrap();
899 assert!(!utxos_with_query.0.is_empty());
900 for utxo in &utxos_with_query.0 {
901 let amount_btc = utxo.amount.to_btc();
902 assert!((0.5..=2.0).contains(&amount_btc));
903 }
904
905 let tx = finalized_psbt.hex.unwrap();
906 assert!(!tx.input.is_empty());
907 assert!(!tx.output.is_empty());
908 }
909
910 #[tokio::test()]
911 async fn estimate_smart_fee_returns_fee_rate_after_observed_regtest_history() {
912 init_tracing();
913
914 let (estimator, broadcaster, client) = get_p2p_bitcoind_and_client();
915 populate_fee_estimation_history(&estimator, &broadcaster, &client).await;
916
917 let got = client.estimate_smart_fee(1).await.unwrap();
918 assert_eq!(got.fee_rate, Some(FEE_ESTIMATION_FEE_RATE));
919 assert!(got.errors.is_none());
920 assert_eq!(got.blocks, 2);
921 }
922
923 async fn signed_op_return_burn_transaction(
924 bitcoind: &Node,
925 client: &Client,
926 ) -> (Transaction, Amount) {
927 let blocks = mine_blocks(bitcoind, 101, None).unwrap();
928 let spendable_block = client.get_block(blocks.first().unwrap()).await.unwrap();
929 let coinbase_tx = spendable_block.coinbase().unwrap();
930
931 let burn_amount = Amount::from_sat(1_000);
932 let fee = Amount::from_sat(10_000);
933 let change_amount = COINBASE_AMOUNT - burn_amount - fee;
934 let burn_address = client.get_new_address().await.unwrap();
935 let change_address = client.get_new_address().await.unwrap();
936 let raw_tx = CreateRawTransactionArguments {
937 inputs: vec![CreateRawTransactionInput {
938 txid: coinbase_tx.compute_txid().to_string(),
939 vout: 0,
940 }],
941 outputs: vec![
942 CreateRawTransactionOutput::AddressAmount {
943 address: burn_address.to_string(),
944 amount: burn_amount.to_btc(),
945 },
946 CreateRawTransactionOutput::AddressAmount {
947 address: change_address.to_string(),
948 amount: change_amount.to_btc(),
949 },
950 ],
951 };
952 let mut tx = client.create_raw_transaction(raw_tx).await.unwrap();
953 tx.output[0].script_pubkey = Builder::new()
954 .push_opcode(OP_RETURN)
955 .push_slice([1u8; 32])
956 .into_script();
957
958 let signed_tx = client
959 .sign_raw_transaction_with_wallet(&tx, None)
960 .await
961 .unwrap()
962 .tx;
963
964 (signed_tx, burn_amount)
965 }
966
967 #[tokio::test()]
968 async fn send_raw_transaction_accepts_explicit_max_burn_amount() {
969 init_tracing();
970
971 let (bitcoind, client) = get_bitcoind_and_client();
972 let (signed_tx, burn_amount) = signed_op_return_burn_transaction(&bitcoind, &client).await;
973
974 let rejected = client.send_raw_transaction(&signed_tx, None).await;
975 assert_max_burn_amount_rejected(rejected, "sendrawtransaction");
976
977 let txid = client
978 .send_raw_transaction(
979 &signed_tx,
980 Some(SendRawTransactionOptions {
981 max_burn_amount: Some(burn_amount),
982 ..Default::default()
983 }),
984 )
985 .await
986 .unwrap();
987
988 assert_eq!(txid, signed_tx.compute_txid());
989 }
990
991 #[tokio::test()]
992 async fn submit_package_accepts_explicit_max_burn_amount() {
993 init_tracing();
994
995 let (bitcoind, client) = get_bitcoind_and_client();
996 let (signed_tx, burn_amount) = signed_op_return_burn_transaction(&bitcoind, &client).await;
997
998 let rejected = client.submit_package(&[signed_tx.clone()], None).await;
999 assert_max_burn_amount_rejected(rejected, "submitpackage");
1000
1001 let result = client
1002 .submit_package(
1003 &[signed_tx],
1004 Some(BroadcastOptions {
1005 max_burn_amount: Some(burn_amount),
1006 ..Default::default()
1007 }),
1008 )
1009 .await
1010 .unwrap();
1011
1012 assert_eq!(result.package_msg, "success");
1013 assert_eq!(result.tx_results.len(), 1);
1014 }
1015
1016 #[tokio::test()]
1017 async fn get_tx_out() {
1018 init_tracing();
1019
1020 let (bitcoind, client) = get_bitcoind_and_client();
1021
1022 let got = client.network().await.unwrap();
1024 let expected = Network::Regtest;
1025 assert_eq!(expected, got);
1026
1027 let address = bitcoind.client.new_address().unwrap();
1028 let blocks = mine_blocks(&bitcoind, 101, Some(address)).unwrap();
1029 let last_block = client.get_block(blocks.first().unwrap()).await.unwrap();
1030 let coinbase_tx = last_block.coinbase().unwrap();
1031
1032 let got = client
1034 .get_tx_out(&coinbase_tx.compute_txid(), 0, true)
1035 .await
1036 .unwrap();
1037 assert_eq!(got.tx_out.value, COINBASE_AMOUNT);
1038
1039 let new_address = bitcoind.client.new_address().unwrap();
1041 let send_amount = Amount::from_sat(COINBASE_AMOUNT.to_sat() - 2_000); let _send_tx = bitcoind
1043 .client
1044 .send_to_address(&new_address, send_amount)
1045 .unwrap()
1046 .txid()
1047 .unwrap();
1048 let result = client
1049 .get_tx_out(&coinbase_tx.compute_txid(), 0, true)
1050 .await;
1051 trace!(?result, "gettxout result");
1052 assert!(result.is_err());
1053 }
1054
1055 #[tokio::test()]
1062 async fn submit_package() {
1063 init_tracing();
1064
1065 let (bitcoind, client) = get_bitcoind_and_client();
1066
1067 let got = client.network().await.unwrap();
1069 let expected = Network::Regtest;
1070 assert_eq!(expected, got);
1071
1072 let blocks = mine_blocks(&bitcoind, 101, None).unwrap();
1073 let last_block = client.get_block(blocks.first().unwrap()).await.unwrap();
1074 let coinbase_tx = last_block.coinbase().unwrap();
1075
1076 let destination = client.get_new_address().await.unwrap();
1077 let change_address = client.get_new_address().await.unwrap();
1078 let amount = Amount::from_btc(1.0).unwrap();
1079 let fees = Amount::from_btc(0.0001).unwrap();
1080 let change_amount = COINBASE_AMOUNT - amount - fees;
1081 let amount_minus_fees = Amount::from_sat(amount.to_sat() - 2_000);
1082
1083 let send_back_address = client.get_new_address().await.unwrap();
1084 let parent_raw_tx = CreateRawTransactionArguments {
1085 inputs: vec![CreateRawTransactionInput {
1086 txid: coinbase_tx.compute_txid().to_string(),
1087 vout: 0,
1088 }],
1089 outputs: vec![
1090 CreateRawTransactionOutput::AddressAmount {
1092 address: destination.to_string(),
1093 amount: amount.to_btc(),
1094 },
1095 CreateRawTransactionOutput::AddressAmount {
1097 address: change_address.to_string(),
1098 amount: change_amount.to_btc(),
1099 },
1100 ],
1101 };
1102 let parent = client.create_raw_transaction(parent_raw_tx).await.unwrap();
1103 let signed_parent = client
1104 .sign_raw_transaction_with_wallet(&parent, None)
1105 .await
1106 .unwrap()
1107 .tx;
1108
1109 let parent_submitted = client
1111 .send_raw_transaction(&signed_parent, None)
1112 .await
1113 .unwrap();
1114
1115 let child_raw_tx = CreateRawTransactionArguments {
1116 inputs: vec![CreateRawTransactionInput {
1117 txid: parent_submitted.to_string(),
1118 vout: 0,
1119 }],
1120 outputs: vec![
1121 CreateRawTransactionOutput::AddressAmount {
1123 address: send_back_address.to_string(),
1124 amount: amount_minus_fees.to_btc(),
1125 },
1126 ],
1127 };
1128 let child = client.create_raw_transaction(child_raw_tx).await.unwrap();
1129 let signed_child = client
1130 .sign_raw_transaction_with_wallet(&child, None)
1131 .await
1132 .unwrap()
1133 .tx;
1134
1135 let result = client
1137 .submit_package(&[signed_parent, signed_child], None)
1138 .await
1139 .unwrap();
1140 assert_eq!(result.tx_results.len(), 2);
1141 assert_eq!(result.package_msg, "success");
1142 }
1143
1144 #[tokio::test]
1151 async fn submit_package_1p1c() {
1152 init_tracing();
1153
1154 let (bitcoind, client) = get_bitcoind_and_client();
1155
1156 let server_version = bitcoind.client.server_version().unwrap();
1158 assert!(server_version > 28);
1159
1160 let destination = client.get_new_address().await.unwrap();
1161
1162 let blocks = mine_blocks(&bitcoind, 101, None).unwrap();
1163 let last_block = client.get_block(blocks.first().unwrap()).await.unwrap();
1164 let coinbase_tx = last_block.coinbase().unwrap();
1165
1166 let parent_raw_tx = CreateRawTransactionArguments {
1167 inputs: vec![CreateRawTransactionInput {
1168 txid: coinbase_tx.compute_txid().to_string(),
1169 vout: 0,
1170 }],
1171 outputs: vec![CreateRawTransactionOutput::AddressAmount {
1172 address: destination.to_string(),
1173 amount: COINBASE_AMOUNT.to_btc(),
1174 }],
1175 };
1176 let mut parent = client.create_raw_transaction(parent_raw_tx).await.unwrap();
1177 parent.version = transaction::Version(3);
1178 assert_eq!(parent.version, transaction::Version(3));
1179 trace!(?parent, "parent:");
1180 let signed_parent = client
1181 .sign_raw_transaction_with_wallet(&parent, None)
1182 .await
1183 .unwrap()
1184 .tx;
1185 assert_eq!(signed_parent.version, transaction::Version(3));
1186
1187 let parent_broadcasted = client.send_raw_transaction(&signed_parent, None).await;
1189 assert!(parent_broadcasted.is_err());
1190
1191 let amount_minus_fees = Amount::from_sat(COINBASE_AMOUNT.to_sat() - 43_000);
1193 let child_raw_tx = CreateRawTransactionArguments {
1194 inputs: vec![CreateRawTransactionInput {
1195 txid: signed_parent.compute_txid().to_string(),
1196 vout: 0,
1197 }],
1198 outputs: vec![CreateRawTransactionOutput::AddressAmount {
1199 address: destination.to_string(),
1200 amount: amount_minus_fees.to_btc(),
1201 }],
1202 };
1203 let mut child = client.create_raw_transaction(child_raw_tx).await.unwrap();
1204 child.version = transaction::Version(3);
1205 assert_eq!(child.version, transaction::Version(3));
1206 trace!(?child, "child:");
1207 let prev_outputs = vec![PreviousTransactionOutput {
1208 txid: parent.compute_txid(),
1209 vout: 0,
1210 script_pubkey: parent.output[0].script_pubkey.to_hex_string(),
1211 redeem_script: None,
1212 witness_script: None,
1213 amount: Some(COINBASE_AMOUNT.to_btc()),
1214 }];
1215 let signed_child = client
1216 .sign_raw_transaction_with_wallet(&child, Some(prev_outputs))
1217 .await
1218 .unwrap()
1219 .tx;
1220 assert_eq!(signed_child.version, transaction::Version(3));
1221
1222 let child_broadcasted = client.send_raw_transaction(&signed_child, None).await;
1224 assert!(child_broadcasted.is_err());
1225
1226 let result = client
1228 .submit_package(&[signed_parent, signed_child], None)
1229 .await
1230 .unwrap();
1231 assert_eq!(result.tx_results.len(), 2);
1232 assert_eq!(result.package_msg, "success");
1233 }
1234
1235 #[tokio::test]
1236 async fn test_invalid_credentials_return_401_error() {
1237 init_tracing();
1238
1239 let (bitcoind, _) = get_bitcoind_and_client();
1240 let url = bitcoind.rpc_url();
1241
1242 let auth = Auth::UserPass("wrong_user".to_string(), "wrong_password".to_string());
1243 let invalid_client = Client::new(url, auth, None, None, None).unwrap();
1244
1245 let result = invalid_client.get_blockchain_info().await;
1247
1248 assert!(result.is_err());
1250 let error = result.unwrap_err();
1251
1252 match error {
1253 ClientError::Status(status_code, message) => {
1254 assert_eq!(status_code, 401);
1255 assert!(message.contains("Unauthorized"));
1256 }
1257 _ => panic!("Expected Status(401, _) error, but got: {error:?}"),
1258 }
1259 }
1260
1261 #[tokio::test]
1262 async fn test_send_raw_transaction_exposes_rpc_error_code_on_http_500() {
1263 init_tracing();
1264
1265 let (_bitcoind, client) = get_bitcoind_and_client();
1266
1267 let result = client
1268 .call::<String>("sendrawtransaction", &[to_value("deadbeef").unwrap()])
1269 .await;
1270
1271 match result {
1272 Err(ClientError::Server(code, message)) => {
1273 assert_eq!(code, -22);
1274 assert!(
1275 message.to_lowercase().contains("decode"),
1276 "expected decode-related RPC error message, got: {message}"
1277 );
1278 }
1279 other => panic!("Expected Server(-22, _), got: {other:?}"),
1280 }
1281 }
1282
1283 #[tokio::test]
1284 async fn test_get_raw_transaction_exposes_rpc_error_code_on_http_500() {
1285 init_tracing();
1286
1287 let (_bitcoind, client) = get_bitcoind_and_client();
1288 let missing_txid = Txid::from_slice(&[0u8; 32]).expect("must be a valid txid");
1289
1290 let error = client
1291 .get_raw_transaction_verbosity_zero(&missing_txid)
1292 .await
1293 .expect_err("missing txid must fail");
1294
1295 assert!(
1296 !matches!(error, ClientError::Status(..) | ClientError::Parse(..)),
1297 "expected parsed RPC error, got transport/parsing error: {error:?}"
1298 );
1299 assert!(
1300 error.is_tx_not_found(),
1301 "expected tx-not-found classification, got: {error:?}"
1302 );
1303 }
1304
1305 #[tokio::test]
1306 async fn psbt_bump_fee() {
1307 init_tracing();
1308
1309 let (bitcoind, client) = get_bitcoind_and_client();
1310
1311 mine_blocks(&bitcoind, 101, None).unwrap();
1313
1314 let destination = client.get_new_address().await.unwrap();
1316 let amount = Amount::from_btc(0.001).unwrap(); let txid = bitcoind
1320 .client
1321 .send_to_address_rbf(&destination, amount)
1322 .unwrap()
1323 .txid()
1324 .unwrap();
1325
1326 let mempool = client.get_raw_mempool().await.unwrap();
1328 assert!(
1329 mempool.0.contains(&txid),
1330 "Transaction should be in mempool for RBF"
1331 );
1332
1333 let signed_tx = client
1335 .psbt_bump_fee(&txid, None)
1336 .await
1337 .unwrap()
1338 .psbt
1339 .extract_tx()
1340 .unwrap();
1341 let signed_txid = signed_tx.compute_txid();
1342 let got = client
1343 .test_mempool_accept(&signed_tx)
1344 .await
1345 .unwrap()
1346 .results
1347 .first()
1348 .unwrap()
1349 .txid;
1350 assert_eq!(
1351 got, signed_txid,
1352 "Bumped transaction should be accepted in mempool"
1353 );
1354
1355 let options = PsbtBumpFeeOptions {
1357 fee_rate: Some(FeeRate::from_sat_per_vb(20).unwrap()), ..Default::default()
1359 };
1360 trace!(?options, "Calling psbt_bump_fee");
1361 let signed_tx = client
1362 .psbt_bump_fee(&txid, Some(options))
1363 .await
1364 .unwrap()
1365 .psbt
1366 .extract_tx()
1367 .unwrap();
1368 let signed_txid = signed_tx.compute_txid();
1369 let got = client
1370 .test_mempool_accept(&signed_tx)
1371 .await
1372 .unwrap()
1373 .results
1374 .first()
1375 .unwrap()
1376 .txid;
1377 assert_eq!(
1378 got, signed_txid,
1379 "Bumped transaction should be accepted in mempool"
1380 );
1381 }
1382
1383 #[cfg(feature = "raw_rpc")]
1384 #[tokio::test]
1385 async fn call_raw() {
1386 init_tracing();
1387
1388 let (bitcoind, client) = get_bitcoind_and_client();
1389
1390 mine_blocks(&bitcoind, 5, None).unwrap();
1391
1392 let expected = client.get_block_count().await.unwrap();
1393
1394 let got: u64 = client.call_raw("getblockcount", &[]).await.unwrap();
1395
1396 assert_eq!(expected, got);
1397
1398 let height = 0;
1399
1400 let expected_hash = client.get_block_hash(height).await.unwrap();
1401
1402 let got_hash: BlockHash = client
1403 .call_raw("getblockhash", &[to_value(height).unwrap()])
1404 .await
1405 .unwrap();
1406
1407 assert_eq!(expected_hash, got_hash);
1408 }
1409
1410 #[test]
1411 fn test_network_chain_response() {
1412 let test_cases = vec![
1413 ("main", Network::Bitcoin),
1414 ("test", Network::Testnet),
1415 ("testnet4", Network::Testnet4),
1416 ("signet", Network::Signet),
1417 ("regtest", Network::Regtest),
1418 ];
1419
1420 for (bitcoind_chain_str, expected_network) in test_cases {
1421 let result = Network::from_core_arg(bitcoind_chain_str);
1422 assert!(result.is_ok(), "failed for chain: {}", bitcoind_chain_str);
1423 assert_eq!(result.unwrap(), expected_network);
1424 }
1425 }
1426}