Skip to main content

bitcoind_async_client/client/
v30.rs

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