Skip to main content

bitcoind_async_client/client/
v29.rs

1//! This module contains the implementation of the [`Client`] for Bitcoin Core v29.
2
3use 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
35/// Minimum relay fee rate: 1 sat/vB = 0.00001 BTC/kvB
36const 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); // Default to minimum if missing or invalid
51
52        // Ensure fee rate is positive and non-zero
53        if btc_vkb <= 0.0 {
54            return Err(ClientError::Other(
55                "Invalid fee rate: must be positive".to_string(),
56            ));
57        }
58
59        // Convert BTC/vB to sat/vB
60        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                // Dealing with known and common errors
223                -27 => Ok(tx.compute_txid()), // Tx already in chain
224                _ => 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", &params).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 the ENV variable `BITCOIN_XPRIV_RETRIEVABLE` is not set, we return `None`
389        if var("BITCOIN_XPRIV_RETRIEVABLE").is_err() {
390            return Ok(None);
391        }
392
393        let descriptors = self
394            .call::<ListDescriptors>("listdescriptors", &[to_value(true)?]) // true is the xpriv, false is the xpub
395            .await?
396            .descriptors;
397        if descriptors.is_empty() {
398            return Err(ClientError::Other("No descriptors found".to_string()));
399        }
400
401        // We are only interested in the one that contains `tr(`
402        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        // Now we extract the xpriv from the `tr()` up to the first `/`
409        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        // TODO: this should check for -35 error code which is good,
432        //       means that is already created
433        let _wallet_create = self
434            .call::<CreateWallet>("createwallet", &[to_value(wallet_args.clone())?])
435            .await;
436        // TODO: this should check for -35 error code which is good, -18 is bad.
437        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", &params)
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", &params).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    /// 50 BTC in [`Network::Regtest`].
505    const COINBASE_AMOUNT: Amount = Amount::from_sat(50 * 100_000_000);
506
507    /// Only attempts to start tracing once.
508    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        // network
527        let got = client.network().await.unwrap();
528        let expected = Network::Regtest;
529
530        assert_eq!(expected, got);
531        // get_blockchain_info
532        let get_blockchain_info = client.get_blockchain_info().await.unwrap();
533        assert_eq!(get_blockchain_info.blocks, 0);
534
535        // get_current_timestamp
536        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        // get_block
544        let expected = blocks.last().unwrap();
545        let got = client.get_block(expected).await.unwrap().block_hash();
546        assert_eq!(*expected, got);
547
548        // get_block_at
549        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        // get_block_count
559        let expected = blocks.len() as u64;
560        let got = client.get_block_count().await.unwrap();
561        assert_eq!(expected, got);
562
563        // get_block_hash
564        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        // get_block_header_at
570        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        // get_new_address
576        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        // get_transaction
588        let tx = client.get_transaction(&txid).await.unwrap().tx;
589        let got = client.send_raw_transaction(&tx).await.unwrap();
590        let expected = txid; // Don't touch this!
591        assert_eq!(expected, got);
592
593        // get_raw_transaction_verbosity_zero
594        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        // get_raw_transaction_verbosity_one
603        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        // get_raw_mempool
612        let got = client.get_raw_mempool().await.unwrap();
613        let expected = vec![txid];
614        assert_eq!(expected, got.0);
615
616        // get_raw_mempool_verbose
617        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        // get_mempool_info
622        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        // estimate_smart_fee
628        let got = client.estimate_smart_fee(1).await.unwrap();
629        let expected = 1; // 1 sat/vB
630        assert_eq!(expected, got);
631
632        // sign_raw_transaction_with_wallet
633        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        // test_mempool_accept
641        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        // send_raw_transaction
656        let got = client.send_raw_transaction(&tx).await.unwrap();
657        assert!(got.as_byte_array().len() == 32);
658
659        // list_transactions
660        let got = client.list_transactions(None).await.unwrap();
661        assert_eq!(got.0.len(), 10);
662
663        // list_unspent
664        // let's mine one more block
665        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        // listdescriptors
673        let got = client.get_xpriv().await.unwrap().unwrap().network;
674        let expected = NetworkKind::Test;
675        assert_eq!(expected, got);
676
677        // importdescriptors
678        // taken from https://github.com/rust-bitcoin/rust-bitcoin/blob/bb38aeb786f408247d5bbc88b9fa13616c74c009/bitcoin/examples/taproot-psbt.rs#L18C38-L18C149
679        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        // network sanity check
813        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        // gettxout should work with a non-spent UTXO.
823        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        // gettxout should fail with a spent UTXO.
830        let new_address = bitcoind.client.new_address().unwrap();
831        let send_amount = Amount::from_sat(COINBASE_AMOUNT.to_sat() - 2_000); // 2k sats as fees.
832        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    /// Create two transactions.
846    /// 1. Normal one: sends 1 BTC to an address that we control.
847    /// 2. CFFP: replaces the first transaction with a different one that we also control.
848    ///
849    /// This is needed because we must SIGN all these transactions, and we can't sign a transaction
850    /// that we don't control.
851    #[tokio::test()]
852    async fn submit_package() {
853        init_tracing();
854
855        let (bitcoind, client) = get_bitcoind_and_client();
856
857        // network sanity check
858        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                // Destination
881                CreateRawTransactionOutput::AddressAmount {
882                    address: destination.to_string(),
883                    amount: amount.to_btc(),
884                },
885                // Change
886                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        // sanity check
900        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                // Send back
909                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        // Ok now we have a parent and a child transaction.
923        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    /// Similar to [`submit_package`], but with where the parent does not pay fees,
932    /// and the child has to pay fees.
933    ///
934    /// This is called 1P1C because it has one parent and one child.
935    /// See <https://bitcoinops.org/en/bitcoin-core-28-wallet-integration-guide/>
936    /// for more information.
937    #[tokio::test]
938    async fn submit_package_1p1c() {
939        init_tracing();
940
941        let (bitcoind, client) = get_bitcoind_and_client();
942
943        // 1p1c sanity check
944        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        // Assert that the parent tx cannot be broadcasted.
975        let parent_broadcasted = client.send_raw_transaction(&signed_parent).await;
976        assert!(parent_broadcasted.is_err());
977
978        // 5k sats as fees.
979        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        // Assert that the child tx cannot be broadcasted.
1010        let child_broadcasted = client.send_raw_transaction(&signed_child).await;
1011        assert!(child_broadcasted.is_err());
1012
1013        // Let's send as a package 1C1P.
1014        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        // Try to make any RPC call
1033        let result = invalid_client.get_blockchain_info().await;
1034
1035        // Verify we get a 401 Status error, not a Parse error
1036        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 to have funds
1099        mine_blocks(&bitcoind, 101, None).unwrap();
1100
1101        // Send to the next address
1102        let destination = client.get_new_address().await.unwrap();
1103        let amount = Amount::from_btc(0.001).unwrap(); // 0.001 BTC
1104
1105        // Create transaction with RBF enabled
1106        let txid = bitcoind
1107            .client
1108            .send_to_address_rbf(&destination, amount)
1109            .unwrap()
1110            .txid()
1111            .unwrap();
1112
1113        // Verify transaction is in mempool (unconfirmed)
1114        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        // Test psbt_bump_fee with default options
1121        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        // Test psbt_bump_fee with custom fee rate
1143        let options = PsbtBumpFeeOptions {
1144            fee_rate: Some(FeeRate::from_sat_per_kwu(20)), // 20 sat/vB - higher than default
1145            ..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}