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(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", &params).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 the ENV variable `BITCOIN_XPRIV_RETRIEVABLE` is not set, we return `None`
388        if var("BITCOIN_XPRIV_RETRIEVABLE").is_err() {
389            return Ok(None);
390        }
391
392        let descriptors = self
393            .call::<ListDescriptors>("listdescriptors", &[to_value(true)?]) // true is the xpriv, false is the xpub
394            .await?
395            .descriptors;
396        if descriptors.is_empty() {
397            return Err(ClientError::Other("No descriptors found".to_string()));
398        }
399
400        // We are only interested in the one that contains `tr(`
401        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        // Now we extract the xpriv from the `tr()` up to the first `/`
408        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        // TODO: this should check for -35 error code which is good,
431        //       means that is already created
432        let _wallet_create = self
433            .call::<CreateWallet>("createwallet", &[to_value(wallet_args.clone())?])
434            .await;
435        // TODO: this should check for -35 error code which is good, -18 is bad.
436        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", &params)
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", &params).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    /// 50 BTC in [`Network::Regtest`].
504    const COINBASE_AMOUNT: Amount = Amount::from_sat(50 * 100_000_000);
505
506    /// Only attempts to start tracing once.
507    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        // network
526        let got = client.network().await.unwrap();
527        let expected = Network::Regtest;
528
529        assert_eq!(expected, got);
530        // get_blockchain_info
531        let get_blockchain_info = client.get_blockchain_info().await.unwrap();
532        assert_eq!(get_blockchain_info.blocks, 0);
533
534        // get_current_timestamp
535        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        // get_block
543        let expected = blocks.last().unwrap();
544        let got = client.get_block(expected).await.unwrap().block_hash();
545        assert_eq!(*expected, got);
546
547        // get_block_at
548        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        // get_block_count
558        let expected = blocks.len() as u64;
559        let got = client.get_block_count().await.unwrap();
560        assert_eq!(expected, got);
561
562        // get_block_hash
563        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        // get_block_header_at
569        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        // get_new_address
575        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        // get_transaction
587        let tx = client.get_transaction(&txid).await.unwrap().tx;
588        let got = client.send_raw_transaction(&tx).await.unwrap();
589        let expected = txid; // Don't touch this!
590        assert_eq!(expected, got);
591
592        // get_raw_transaction_verbosity_zero
593        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        // get_raw_transaction_verbosity_one
602        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        // get_raw_mempool
611        let got = client.get_raw_mempool().await.unwrap();
612        let expected = vec![txid];
613        assert_eq!(expected, got.0);
614
615        // get_raw_mempool_verbose
616        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        // get_mempool_info
621        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        // estimate_smart_fee
627        let got = client.estimate_smart_fee(1).await.unwrap();
628        let expected = 1; // 1 sat/vB
629        assert_eq!(expected, got);
630
631        // sign_raw_transaction_with_wallet
632        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        // test_mempool_accept
640        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        // send_raw_transaction
655        let got = client.send_raw_transaction(&tx).await.unwrap();
656        assert!(got.as_byte_array().len() == 32);
657
658        // list_transactions
659        let got = client.list_transactions(None).await.unwrap();
660        assert_eq!(got.0.len(), 10);
661
662        // list_unspent
663        // let's mine one more block
664        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        // listdescriptors
672        let got = client.get_xpriv().await.unwrap().unwrap().network;
673        let expected = NetworkKind::Test;
674        assert_eq!(expected, got);
675
676        // importdescriptors
677        // taken from https://github.com/rust-bitcoin/rust-bitcoin/blob/bb38aeb786f408247d5bbc88b9fa13616c74c009/bitcoin/examples/taproot-psbt.rs#L18C38-L18C149
678        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        // network sanity check
812        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        // gettxout should work with a non-spent UTXO.
822        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        // gettxout should fail with a spent UTXO.
829        let new_address = bitcoind.client.new_address().unwrap();
830        let send_amount = Amount::from_sat(COINBASE_AMOUNT.to_sat() - 2_000); // 2k sats as fees.
831        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    /// Create two transactions.
845    /// 1. Normal one: sends 1 BTC to an address that we control.
846    /// 2. CFFP: replaces the first transaction with a different one that we also control.
847    ///
848    /// This is needed because we must SIGN all these transactions, and we can't sign a transaction
849    /// that we don't control.
850    #[tokio::test()]
851    async fn submit_package() {
852        init_tracing();
853
854        let (bitcoind, client) = get_bitcoind_and_client();
855
856        // network sanity check
857        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                // Destination
880                CreateRawTransactionOutput::AddressAmount {
881                    address: destination.to_string(),
882                    amount: amount.to_btc(),
883                },
884                // Change
885                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        // sanity check
899        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                // Send back
908                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        // Ok now we have a parent and a child transaction.
922        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    /// Similar to [`submit_package`], but with where the parent does not pay fees,
931    /// and the child has to pay fees.
932    ///
933    /// This is called 1P1C because it has one parent and one child.
934    /// See <https://bitcoinops.org/en/bitcoin-core-28-wallet-integration-guide/>
935    /// for more information.
936    #[tokio::test]
937    async fn submit_package_1p1c() {
938        init_tracing();
939
940        let (bitcoind, client) = get_bitcoind_and_client();
941
942        // 1p1c sanity check
943        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        // Assert that the parent tx cannot be broadcasted.
974        let parent_broadcasted = client.send_raw_transaction(&signed_parent).await;
975        assert!(parent_broadcasted.is_err());
976
977        // 5k sats as fees.
978        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        // Assert that the child tx cannot be broadcasted.
1009        let child_broadcasted = client.send_raw_transaction(&signed_child).await;
1010        assert!(child_broadcasted.is_err());
1011
1012        // Let's send as a package 1C1P.
1013        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        // Try to make any RPC call
1032        let result = invalid_client.get_blockchain_info().await;
1033
1034        // Verify we get a 401 Status error, not a Parse error
1035        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 to have funds
1098        mine_blocks(&bitcoind, 101, None).unwrap();
1099
1100        // Send to the next address
1101        let destination = client.get_new_address().await.unwrap();
1102        let amount = Amount::from_btc(0.001).unwrap(); // 0.001 BTC
1103
1104        // Create transaction with RBF enabled
1105        let txid = bitcoind
1106            .client
1107            .send_to_address_rbf(&destination, amount)
1108            .unwrap()
1109            .txid()
1110            .unwrap();
1111
1112        // Verify transaction is in mempool (unconfirmed)
1113        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        // Test psbt_bump_fee with default options
1120        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        // Test psbt_bump_fee with custom fee rate
1142        let options = PsbtBumpFeeOptions {
1143            fee_rate: Some(FeeRate::from_sat_per_kwu(20)), // 20 sat/vB - higher than default
1144            ..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}