• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stacks-network / stacks-core / 26250451051-1

21 May 2026 08:11PM UTC coverage: 85.585% (-0.1%) from 85.712%
26250451051-1

Pull #7215

github

ec9d4c
web-flow
Merge 9487bf852 into af1280aac
Pull Request #7215: Chore: fix flake in non_blocking_minority_configured_to_favour_...

188844 of 220651 relevant lines covered (85.58%)

18975267.44 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.66
/stacks-node/src/burnchains/rpc/bitcoin_rpc_client/mod.rs
1
// Copyright (C) 2025 Stacks Open Internet Foundation
2
//
3
// This program is free software: you can redistribute it and/or modify
4
// it under the terms of the GNU General Public License as published by
5
// the Free Software Foundation, either version 3 of the License, or
6
// (at your option) any later version.
7
//
8
// This program is distributed in the hope that it will be useful,
9
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
// GNU General Public License for more details.
12
//
13
// You should have received a copy of the GNU General Public License
14
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
15

16
//! Bitcoin RPC client module.
17
//!
18
//! This module provides a typed interface for interacting with a Bitcoin Core node via RPC.
19
//! It includes structures representing RPC request parameters and responses,
20
//! as well as a client implementation ([`BitcoinRpcClient`]) for common node operations
21
//! such as creating wallets, listing UTXOs, importing descriptors, generating blocks, and sending transactions.
22
//!
23
//! Designed for use with Bitcoin Core versions v0.25.0 and newer
24

25
use std::time::Duration;
26

27
use serde::{Deserialize, Deserializer};
28
use serde_json::value::RawValue;
29
use serde_json::{json, Value};
30
use stacks::burnchains::bitcoin::address::BitcoinAddress;
31
use stacks::burnchains::Txid;
32
use stacks::config::Config;
33
use stacks::types::chainstate::BurnchainHeaderHash;
34
use stacks::types::Address;
35
use stacks::util::hash::hex_bytes;
36
use stacks_common::deps_common::bitcoin::blockdata::script::Script;
37
use stacks_common::deps_common::bitcoin::blockdata::transaction::Transaction;
38
use stacks_common::deps_common::bitcoin::network::serialize::{
39
    serialize_hex, Error as bitcoin_serialize_error,
40
};
41

42
use crate::burnchains::rpc::rpc_transport::{RpcAuth, RpcError, RpcTransport};
43

44
#[cfg(test)]
45
pub mod test_utils;
46

47
#[cfg(test)]
48
mod tests;
49

50
/// Response structure for the `gettransaction` RPC call.
51
///
52
/// Contains metadata about a wallet transaction, currently limited to the confirmation count.
53
///
54
/// # Notes
55
/// This struct supports a subset of available fields to match current usage.
56
/// Additional fields can be added in the future as needed.
57
#[derive(Debug, Clone, Deserialize)]
58
pub struct GetTransactionResponse {
59
    pub confirmations: i32,
60
}
61

62
/// Response returned by the `getdescriptorinfo` RPC call.
63
///
64
/// Contains information about a parsed descriptor, including its checksum.
65
///
66
/// # Notes
67
/// This struct supports a subset of available fields to match current usage.
68
/// Additional fields can be added in the future as needed.
69
#[derive(Debug, Clone, Deserialize)]
70
pub struct DescriptorInfoResponse {
71
    pub checksum: String,
72
}
73

74
/// Represents the `timestamp` parameter accepted by the `importdescriptors` RPC method.
75
///
76
/// This indicates when the imported descriptor starts being relevant for address tracking.
77
/// It affects wallet rescanning behavior.
78
#[derive(Debug, Clone)]
79
pub enum Timestamp {
80
    /// Tells the wallet to start tracking from the current blockchain time
81
    Now,
82
    /// A Unix timestamp (in seconds) specifying when the wallet should begin scanning.
83
    Time(u64),
84
}
85

86
/// Serializes [`Timestamp`] to either the string `"now"` or a numeric timestamp,
87
/// matching the format expected by Bitcoin Core.
88
impl serde::Serialize for Timestamp {
89
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
364✔
90
    where
364✔
91
        S: serde::Serializer,
364✔
92
    {
93
        match *self {
364✔
94
            Timestamp::Now => serializer.serialize_str("now"),
×
95
            Timestamp::Time(timestamp) => serializer.serialize_u64(timestamp),
364✔
96
        }
97
    }
364✔
98
}
99

100
/// Represents a single descriptor import request for use with the `importdescriptors` RPC method.
101
///
102
/// This struct defines a descriptor to import into the loaded wallet,
103
/// along with metadata that influences how the wallet handles it (e.g., scan time, internal/external).
104
///
105
/// # Notes
106
/// This struct supports a subset of available fields to match current usage.
107
/// Additional fields can be added in the future as needed.
108
#[derive(Debug, Clone, Serialize)]
109
pub struct ImportDescriptorsRequest {
110
    /// A descriptor string (e.g., `addr(...)#checksum`) with a valid checksum suffix.
111
    #[serde(rename = "desc")]
112
    pub descriptor: String,
113
    /// Specifies when the wallet should begin tracking addresses from this descriptor.
114
    pub timestamp: Timestamp,
115
    /// Optional flag indicating whether the descriptor is used for change addresses.
116
    #[serde(skip_serializing_if = "Option::is_none")]
117
    pub internal: Option<bool>,
118
}
119

120
/// Response returned by the `importdescriptors` RPC method for each imported descriptor.
121
///
122
/// # Notes
123
/// This struct supports a subset of available fields to match current usage.
124
/// Additional fields can be added in the future as needed.
125
#[derive(Debug, Clone, Deserialize)]
126
pub struct ImportDescriptorsResponse {
127
    /// whether the descriptor was imported successfully
128
    pub success: bool,
129
    /// Optional list of warnings encountered during the import process
130
    #[serde(default)]
131
    pub warnings: Vec<String>,
132
    /// Optional detailed error information if the import failed for this descriptor
133
    pub error: Option<ImportDescriptorsErrorMessage>,
134
}
135

136
/// Represents a single UTXO (unspent transaction output) returned by the `listunspent` RPC method.
137
///
138
/// # Notes
139
/// This struct supports a subset of available fields to match current usage.
140
/// Additional fields can be added in the future as needed.
141
#[derive(Debug, Clone, Deserialize)]
142
pub struct ListUnspentResponse {
143
    /// The transaction ID of the UTXO.
144
    #[serde(deserialize_with = "deserialize_string_to_txid")]
145
    pub txid: Txid,
146
    /// The index of the output in the transaction.
147
    pub vout: u32,
148
    /// The Bitcoin destination address
149
    #[serde(deserialize_with = "deserialize_string_to_bitcoin_address")]
150
    pub address: BitcoinAddress,
151
    /// The script associated with the output.
152
    #[serde(
153
        rename = "scriptPubKey",
154
        deserialize_with = "deserialize_string_to_script"
155
    )]
156
    pub script_pub_key: Script,
157
    /// The amount in BTC, deserialized as a string to preserve full precision.
158
    #[serde(deserialize_with = "deserialize_btc_string_to_sat")]
159
    pub amount: u64,
160
    /// The number of confirmations for the transaction.
161
    pub confirmations: u32,
162
}
163

164
/// Deserializes a JSON string (hex-encoded in big-endian order) into [`Txid`].
165
fn deserialize_string_to_txid<'de, D>(deserializer: D) -> Result<Txid, D::Error>
937,728✔
166
where
937,728✔
167
    D: Deserializer<'de>,
937,728✔
168
{
169
    let hex_str: String = Deserialize::deserialize(deserializer)?;
937,728✔
170
    let txid = Txid::from_hex(&hex_str).map_err(serde::de::Error::custom)?;
937,728✔
171
    Ok(txid)
937,728✔
172
}
937,728✔
173

174
/// Deserializes a JSON string into [`BitcoinAddress`]
175
fn deserialize_string_to_bitcoin_address<'de, D>(
937,730✔
176
    deserializer: D,
937,730✔
177
) -> Result<BitcoinAddress, D::Error>
937,730✔
178
where
937,730✔
179
    D: Deserializer<'de>,
937,730✔
180
{
181
    let addr_str: String = Deserialize::deserialize(deserializer)?;
937,730✔
182
    BitcoinAddress::from_string(&addr_str).ok_or(serde::de::Error::custom(
937,730✔
183
        "BitcoinAddress failed to create from string",
184
    ))
185
}
937,730✔
186

187
/// Deserializes a JSON string into [`Script`]
188
fn deserialize_string_to_script<'de, D>(deserializer: D) -> Result<Script, D::Error>
937,728✔
189
where
937,728✔
190
    D: Deserializer<'de>,
937,728✔
191
{
192
    let string: String = Deserialize::deserialize(deserializer)?;
937,728✔
193
    let bytes = hex_bytes(&string)
937,728✔
194
        .map_err(|e| serde::de::Error::custom(format!("invalid hex string for script: {e}")))?;
937,728✔
195
    Ok(bytes.into())
937,728✔
196
}
937,728✔
197

198
/// Deserializes a raw JSON value containing a BTC amount string into satoshis (`u64`).
199
///
200
/// First captures the value as unprocessed JSON to preserve exact formatting (e.g., float precision),
201
/// then convert the BTC string to its integer value in satoshis using [`convert_btc_string_to_sat`].
202
fn deserialize_btc_string_to_sat<'de, D>(deserializer: D) -> Result<u64, D::Error>
937,728✔
203
where
937,728✔
204
    D: Deserializer<'de>,
937,728✔
205
{
206
    let raw: Box<RawValue> = Deserialize::deserialize(deserializer)?;
937,728✔
207
    let raw_str = raw.get();
937,728✔
208
    let sat_amount = convert_btc_string_to_sat(raw_str).map_err(serde::de::Error::custom)?;
937,728✔
209
    Ok(sat_amount)
937,728✔
210
}
937,728✔
211

212
/// Converts a BTC amount string (e.g. "1.12345678") into satoshis (u64).
213
///
214
/// # Arguments
215
/// * `amount` - A string slice containing the BTC amount in decimal notation.
216
///              Expected format: `<integer>.<fractional>` with up to 8 decimal places.
217
///              Examples: "1.00000000", "0.00012345", "0.5", "1".
218
///
219
/// # Returns
220
/// On success return the equivalent amount in satoshis (as u64).
221
fn convert_btc_string_to_sat(amount: &str) -> Result<u64, String> {
937,738✔
222
    const BTC_TO_SAT: u64 = 100_000_000;
223
    const MAX_DECIMAL_COUNT: usize = 8;
224
    let comps: Vec<&str> = amount.split('.').collect();
937,738✔
225
    match comps[..] {
937,738✔
226
        [lhs, rhs] => {
937,735✔
227
            let rhs_len = rhs.len();
937,735✔
228
            if rhs_len > MAX_DECIMAL_COUNT {
937,735✔
229
                return Err(format!("Unexpected amount of decimals ({rhs_len}) in '{amount}'"));
1✔
230
            }
937,734✔
231

232
            match (lhs.parse::<u64>(), rhs.parse::<u64>()) {
937,734✔
233
                (Ok(integer), Ok(decimal)) => {
937,732✔
234
                    let mut sat_amount = integer * BTC_TO_SAT;
937,732✔
235
                    let base: u64 = 10;
937,732✔
236
                    let sat = decimal * base.pow((MAX_DECIMAL_COUNT - rhs.len()) as u32);
937,732✔
237
                    sat_amount += sat;
937,732✔
238
                    Ok(sat_amount)
937,732✔
239
                }
240
                (lhs, rhs) => {
2✔
241
                    return Err(format!("Cannot convert BTC '{amount}' to sat integer: {lhs:?} - fractional: {rhs:?}"));
2✔
242
                }
243
            }
244
        },
245
        [lhs] => match lhs.parse::<u64>() {
2✔
246
            Ok(btc) => Ok(btc * BTC_TO_SAT),
1✔
247
            Err(_) => Err(format!("Cannot convert BTC '{amount}' integer part to sat: '{lhs}'")),
1✔
248
        },
249

250
        _ => Err(format!("Invalid BTC amount format: '{amount}'. Expected '<integer>.<fractional>' with up to 8 decimals.")),
1✔
251
    }
252
}
937,738✔
253

254
/// Converts a satoshi amount (u64) into a BTC string with exactly 8 decimal places.
255
///
256
/// # Arguments
257
/// * `amount` - The amount in satoshis.
258
///
259
/// # Returns
260
/// * A `String` representing the BTC value in the format `<integer>.<fractional>`,
261
///   always padded to 8 decimal places (e.g. "1.00000000", "0.50000000").
262
fn convert_sat_to_btc_string(amount: u64) -> String {
8,969✔
263
    let base: u64 = 10;
8,969✔
264
    let int_part = amount / base.pow(8);
8,969✔
265
    let frac_part = amount % base.pow(8);
8,969✔
266
    let amount = format!("{int_part}.{frac_part:08}");
8,969✔
267
    amount
8,969✔
268
}
8,969✔
269

270
/// Represents an error message returned when importing descriptors fails.
271
#[derive(Debug, Clone, Deserialize, PartialEq)]
272
pub struct ImportDescriptorsErrorMessage {
273
    /// Numeric error code identifying the type of error.
274
    pub code: i64,
275
    /// Human-readable description of the error.
276
    pub message: String,
277
}
278

279
/// Response for `generatetoaddress` rpc, mainly used as deserialization wrapper for `BurnchainHeaderHash`
280
struct GenerateToAddressResponse(pub Vec<BurnchainHeaderHash>);
281

282
/// Deserializes a JSON string array into a vec of [`BurnchainHeaderHash`] and wrap it into [`GenerateToAddressResponse`]
283
impl<'de> Deserialize<'de> for GenerateToAddressResponse {
284
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
16,090✔
285
    where
16,090✔
286
        D: Deserializer<'de>,
16,090✔
287
    {
288
        let hash_strs: Vec<String> = Deserialize::deserialize(deserializer)?;
16,090✔
289
        let mut hashes = Vec::with_capacity(hash_strs.len());
16,090✔
290
        for (i, s) in hash_strs.into_iter().enumerate() {
62,326✔
291
            let hash = BurnchainHeaderHash::from_hex(&s).map_err(|e| {
62,326✔
292
                serde::de::Error::custom(format!(
1✔
293
                    "Invalid BurnchainHeaderHash at index {}: {}",
294
                    i, e
295
                ))
296
            })?;
1✔
297
            hashes.push(hash);
62,325✔
298
        }
299

300
        Ok(GenerateToAddressResponse(hashes))
16,089✔
301
    }
16,090✔
302
}
303

304
/// Response mainly used as deserialization wrapper for [`Txid`]
305
struct TxidWrapperResponse(pub Txid);
306

307
/// Deserializes a JSON string (hex-encoded in big-endian order) into [`Txid`] and wrap it into [`TxidWrapperResponse`]
308
impl<'de> Deserialize<'de> for TxidWrapperResponse {
309
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
8,702✔
310
    where
8,702✔
311
        D: Deserializer<'de>,
8,702✔
312
    {
313
        let hex_str: String = Deserialize::deserialize(deserializer)?;
8,702✔
314
        let txid = Txid::from_hex(&hex_str).map_err(serde::de::Error::custom)?;
8,702✔
315
        Ok(TxidWrapperResponse(txid))
8,701✔
316
    }
8,702✔
317
}
318

319
/// Response mainly used as deserialization wrapper for [`BurnchainHeaderHash`]
320
struct BurnchainHeaderHashWrapperResponse(pub BurnchainHeaderHash);
321

322
/// Deserializes a JSON string (hex-encoded, big-endian) into [`BurnchainHeaderHash`],
323
/// and wrap it into [`BurnchainHeaderHashWrapperResponse`]
324
impl<'de> Deserialize<'de> for BurnchainHeaderHashWrapperResponse {
325
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
8,999✔
326
    where
8,999✔
327
        D: Deserializer<'de>,
8,999✔
328
    {
329
        let hex_str: String = Deserialize::deserialize(deserializer)?;
8,999✔
330
        let bhh = BurnchainHeaderHash::from_hex(&hex_str).map_err(serde::de::Error::custom)?;
8,999✔
331
        Ok(BurnchainHeaderHashWrapperResponse(bhh))
8,999✔
332
    }
8,999✔
333
}
334

335
/// Client for interacting with a Bitcoin RPC service.
336
#[derive(Debug, Clone)]
337
pub struct BitcoinRpcClient {
338
    /// The client ID to identify the source of the requests.
339
    client_id: String,
340
    /// RPC endpoint used for api calls
341
    endpoint: RpcTransport,
342
}
343

344
/// Represents errors that can occur when using [`BitcoinRpcClient`].
345
#[derive(Debug, thiserror::Error)]
346
pub enum BitcoinRpcClientError {
347
    // Missing credential error
348
    #[error("Missing credential error")]
349
    MissingCredentials,
350
    // RPC Transport errors
351
    #[error("Rcp error: {0}")]
352
    Rpc(#[from] RpcError),
353
    // JSON serialization errors
354
    #[error("Serialization error: {0}")]
355
    Serialization(#[from] serde_json::Error),
356
    // Bitcoin serialization errors
357
    #[error("Bitcoin Serialization error: {0}")]
358
    BitcoinSerialization(#[from] bitcoin_serialize_error),
359
}
360

361
/// Alias for results returned from client operations.
362
pub type BitcoinRpcClientResult<T> = Result<T, BitcoinRpcClientError>;
363

364
impl BitcoinRpcClient {
365
    /// Create a [`BitcoinRpcClient`] from Stacks Configuration, mainly using [`stacks::config::BurnchainConfig`]
366
    ///
367
    /// # Notes
368
    /// `username` and `password` configuration are mandatory (`bitcoind` requires authentication for rpc calls),
369
    /// so a [`BitcoinRpcClientError::MissingCredentials`] is returned otherwise,
370
    pub fn from_stx_config(config: &Config) -> BitcoinRpcClientResult<Self> {
227,460✔
371
        let host = config.burnchain.peer_host.clone();
227,460✔
372
        let port = config.burnchain.rpc_port;
227,460✔
373
        let username_opt = &config.burnchain.username;
227,460✔
374
        let password_opt = &config.burnchain.password;
227,460✔
375
        let timeout = config.burnchain.timeout;
227,460✔
376
        let client_id = "stacks".to_string();
227,460✔
377

378
        let rpc_auth = match (username_opt, password_opt) {
227,460✔
379
            (Some(username), Some(password)) => RpcAuth::Basic {
227,457✔
380
                username: username.clone(),
227,457✔
381
                password: password.clone(),
227,457✔
382
            },
227,457✔
383
            _ => return Err(BitcoinRpcClientError::MissingCredentials),
3✔
384
        };
385

386
        Self::new(host, port, rpc_auth, timeout, client_id)
227,457✔
387
    }
227,460✔
388

389
    /// Creates a new instance of the Bitcoin RPC client with both global and wallet-specific endpoints.
390
    ///
391
    /// # Arguments
392
    ///
393
    /// * `host` - Hostname or IP address of the Bitcoin RPC server (e.g., `localhost`).
394
    /// * `port` - Port number the RPC server is listening on.
395
    /// * `auth` - RPC authentication credentials (`RpcAuth::None` or `RpcAuth::Basic`).
396
    /// * `timeout` - Timeout for RPC requests, in seconds.
397
    /// * `client_id` - Identifier used in the `id` field of JSON-RPC requests for traceability.
398
    ///
399
    /// # Returns
400
    ///
401
    /// A [`BitcoinRpcClient`] on success, or a [`BitcoinRpcClientError`] otherwise.
402
    pub fn new(
227,481✔
403
        host: String,
227,481✔
404
        port: u16,
227,481✔
405
        auth: RpcAuth,
227,481✔
406
        timeout: u64,
227,481✔
407
        client_id: String,
227,481✔
408
    ) -> BitcoinRpcClientResult<Self> {
227,481✔
409
        let rpc_url = format!("http://{host}:{port}");
227,481✔
410
        let rpc_auth = auth;
227,481✔
411

412
        let rpc_timeout = Duration::from_secs(timeout);
227,481✔
413

414
        let endpoint = RpcTransport::new(rpc_url, rpc_auth.clone(), Some(rpc_timeout))?;
227,481✔
415

416
        Ok(Self {
227,481✔
417
            client_id,
227,481✔
418
            endpoint,
227,481✔
419
        })
227,481✔
420
    }
227,481✔
421

422
    /// create a wallet rpc path based on the given wallet name.
423
    fn wallet_path(wallet: &str) -> String {
25,858✔
424
        format!("wallet/{wallet}")
25,858✔
425
    }
25,858✔
426

427
    /// Creates and loads a new wallet into the Bitcoin Core node.
428
    ///
429
    /// Wallet is stored in the `-walletdir` specified in the Bitcoin Core configuration (or the default data directory if not set).
430
    ///
431
    /// # Arguments
432
    /// * `wallet_name` - Name of the wallet to create.
433
    /// * `disable_private_keys` - If `Some(true)`, the wallet will not be able to hold private keys.
434
    ///   If `None`, this defaults to `false`, allowing private key import/use.
435
    ///
436
    /// # Returns
437
    /// Returns `Ok(())` if the wallet is created successfully.
438
    ///
439
    /// # Availability
440
    /// - **Since**: Bitcoin Core **v0.17.0**.
441
    ///
442
    /// # Notes
443
    /// This method supports a subset of available RPC arguments to match current usage.
444
    /// Additional parameters can be added in the future as needed.
445
    pub fn create_wallet(
265✔
446
        &self,
265✔
447
        wallet_name: &str,
265✔
448
        disable_private_keys: Option<bool>,
265✔
449
    ) -> BitcoinRpcClientResult<()> {
265✔
450
        let disable_private_keys = disable_private_keys.unwrap_or(false);
265✔
451

452
        self.endpoint.send::<Value>(
265✔
453
            &self.client_id,
265✔
454
            None,
265✔
455
            "createwallet",
265✔
456
            vec![wallet_name.into(), disable_private_keys.into()],
265✔
457
        )?;
×
458
        Ok(())
265✔
459
    }
265✔
460

461
    /// Returns a list of currently loaded wallets by the Bitcoin Core node.
462
    ///
463
    /// # Returns
464
    /// A vector of wallet names as strings.
465
    ///
466
    /// # Availability
467
    /// Available since Bitcoin Core **v0.15.0**.
468
    pub fn list_wallets(&self) -> BitcoinRpcClientResult<Vec<String>> {
776✔
469
        Ok(self
776✔
470
            .endpoint
776✔
471
            .send(&self.client_id, None, "listwallets", vec![])?)
776✔
472
    }
776✔
473

474
    /// Retrieve a list of unspent transaction outputs (UTXOs) that meet the specified criteria.
475
    ///
476
    /// # Arguments
477
    /// * `wallet` - The name of the wallet to query. This is used to construct the wallet-specific RPC endpoint.
478
    /// * `min_confirmations` - Minimum number of confirmations required for a UTXO to be included (Default: 0).
479
    /// * `max_confirmations` - Maximum number of confirmations allowed (Default: 9.999.999).
480
    /// * `addresses` - Optional list of addresses to filter UTXOs by (Default: no filtering).
481
    /// * `include_unsafe` - Whether to include UTXOs from unconfirmed unsafe transactions (Default: `true`).
482
    /// * `minimum_amount` - Minimum amount in satoshis (internally converted to BTC string to preserve full precision) a UTXO must have to be included (Default: 0).
483
    /// * `maximum_count` - Maximum number of UTXOs to return. Use `None` for effectively 'unlimited' (Default: 0).
484
    ///
485
    /// # Returns
486
    /// A Vec<[`ListUnspentResponse`]> containing the matching UTXOs.
487
    ///
488
    /// # Availability
489
    /// - **Since**: Bitcoin Core **v0.7.0**.
490
    ///
491
    /// # Notes
492
    /// This method supports a subset of available RPC arguments to match current usage.
493
    /// Additional parameters can be added in the future as needed.
494
    pub fn list_unspent(
8,966✔
495
        &self,
8,966✔
496
        wallet: &str,
8,966✔
497
        min_confirmations: Option<u64>,
8,966✔
498
        max_confirmations: Option<u64>,
8,966✔
499
        addresses: Option<&[&BitcoinAddress]>,
8,966✔
500
        include_unsafe: Option<bool>,
8,966✔
501
        minimum_amount: Option<u64>,
8,966✔
502
        maximum_count: Option<u64>,
8,966✔
503
    ) -> BitcoinRpcClientResult<Vec<ListUnspentResponse>> {
8,966✔
504
        let min_confirmations = min_confirmations.unwrap_or(0);
8,966✔
505
        let max_confirmations = max_confirmations.unwrap_or(9_999_999);
8,966✔
506
        let addresses = addresses.unwrap_or(&[]);
8,966✔
507
        let include_unsafe = include_unsafe.unwrap_or(true);
8,966✔
508
        let minimum_amount = minimum_amount.unwrap_or(0);
8,966✔
509
        let maximum_count = maximum_count.unwrap_or(0);
8,966✔
510

511
        let addr_as_strings: Vec<String> = addresses.iter().map(|addr| addr.to_string()).collect();
8,966✔
512
        let min_amount_btc_str = convert_sat_to_btc_string(minimum_amount);
8,966✔
513

514
        Ok(self.endpoint.send(
8,966✔
515
            &self.client_id,
8,966✔
516
            Some(&Self::wallet_path(wallet)),
8,966✔
517
            "listunspent",
8,966✔
518
            vec![
8,966✔
519
                min_confirmations.into(),
8,966✔
520
                max_confirmations.into(),
8,966✔
521
                addr_as_strings.into(),
8,966✔
522
                include_unsafe.into(),
8,966✔
523
                json!({
8,966✔
524
                    "minimumAmount": min_amount_btc_str,
8,966✔
525
                    "maximumCount": maximum_count
8,966✔
526
                }),
527
            ],
528
        )?)
×
529
    }
8,966✔
530

531
    /// Mines a specified number of blocks and sends the block rewards to a given address.
532
    ///
533
    /// # Arguments
534
    /// * `num_blocks` - The number of blocks to mine.
535
    /// * `address` - The [`BitcoinAddress`] to receive the block rewards.
536
    ///
537
    /// # Returns
538
    /// A vector of [`BurnchainHeaderHash`] corresponding to the newly generated blocks.
539
    ///
540
    /// # Availability
541
    /// - **Since**: Bitcoin Core **v0.17.0**.
542
    ///
543
    /// # Notes
544
    /// Typically used on `regtest` or test networks.
545
    pub fn generate_to_address(
16,090✔
546
        &self,
16,090✔
547
        num_blocks: u64,
16,090✔
548
        address: &BitcoinAddress,
16,090✔
549
    ) -> BitcoinRpcClientResult<Vec<BurnchainHeaderHash>> {
16,090✔
550
        let response = self.endpoint.send::<GenerateToAddressResponse>(
16,090✔
551
            &self.client_id,
16,090✔
552
            None,
16,090✔
553
            "generatetoaddress",
16,090✔
554
            vec![num_blocks.into(), address.to_string().into()],
16,090✔
555
        )?;
1✔
556
        Ok(response.0)
16,089✔
557
    }
16,090✔
558

559
    /// Retrieves detailed information about an in-wallet transaction.
560
    ///
561
    /// This method returns information such as amount, fee, confirmations, block hash,
562
    /// hex-encoded transaction, and other metadata for a transaction tracked by the wallet.
563
    ///
564
    /// # Arguments
565
    /// * `wallet` - The name of the wallet to query. This is used to construct the wallet-specific RPC endpoint.
566
    /// * `txid` - The transaction ID (as [`Txid`]) to query (in big-endian order).
567
    ///
568
    /// # Returns
569
    /// A [`GetTransactionResponse`] containing detailed metadata for the specified transaction.
570
    ///
571
    /// # Availability
572
    /// - **Since**: Bitcoin Core **v0.10.0**.
573
    pub fn get_transaction(
16,524✔
574
        &self,
16,524✔
575
        wallet: &str,
16,524✔
576
        txid: &Txid,
16,524✔
577
    ) -> BitcoinRpcClientResult<GetTransactionResponse> {
16,524✔
578
        Ok(self.endpoint.send(
16,524✔
579
            &self.client_id,
16,524✔
580
            Some(&Self::wallet_path(wallet)),
16,524✔
581
            "gettransaction",
16,524✔
582
            vec![txid.to_hex().into()],
16,524✔
583
        )?)
598✔
584
    }
16,524✔
585

586
    /// Broadcasts a raw transaction to the Bitcoin network.
587
    ///
588
    /// This method sends a hex-encoded raw Bitcoin transaction. It supports optional limits for the
589
    /// maximum fee rate and maximum burn amount to prevent accidental overspending.
590
    ///
591
    /// # Arguments
592
    ///
593
    /// * `tx` - A [`Transaction`], that will be hex-encoded, representing the raw transaction.
594
    /// * `max_fee_rate` - Optional maximum fee rate (in BTC/kvB). If `None`, defaults to `0.10` BTC/kvB.
595
    ///     - Bitcoin Core will reject transactions exceeding this rate unless explicitly overridden.
596
    ///     - Set to `0.0` to disable fee rate limiting entirely.
597
    /// * `max_burn_amount` - Optional maximum amount (in satoshis) that can be "burned" in the transaction.
598
    ///     - Introduced in Bitcoin Core v25 (https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-25.0.md#rpc-and-other-apis)
599
    ///     - If `None`, defaults to `0`, meaning burning is not allowed.
600
    ///
601
    /// # Returns
602
    /// A [`Txid`] as a transaction ID (in big-endian order)
603
    ///
604
    /// # Availability
605
    /// - **Since**: Bitcoin Core **v0.7.0**.
606
    /// - `maxburnamount` parameter is available starting from **v25.0**.
607
    pub fn send_raw_transaction(
8,702✔
608
        &self,
8,702✔
609
        tx: &Transaction,
8,702✔
610
        max_fee_rate: Option<f64>,
8,702✔
611
        max_burn_amount: Option<u64>,
8,702✔
612
    ) -> BitcoinRpcClientResult<Txid> {
8,702✔
613
        const DEFAULT_FEE_RATE_BTC_KVB: f64 = 0.10;
614
        let tx_hex = serialize_hex(tx)?;
8,702✔
615
        let max_fee_rate = max_fee_rate.unwrap_or(DEFAULT_FEE_RATE_BTC_KVB);
8,702✔
616
        let max_burn_amount = max_burn_amount.unwrap_or(0);
8,702✔
617

618
        let response = self.endpoint.send::<TxidWrapperResponse>(
8,702✔
619
            &self.client_id,
8,702✔
620
            None,
8,702✔
621
            "sendrawtransaction",
8,702✔
622
            vec![tx_hex.into(), max_fee_rate.into(), max_burn_amount.into()],
8,702✔
623
        )?;
2✔
624
        Ok(response.0)
8,700✔
625
    }
8,702✔
626

627
    /// Returns information about a descriptor, including its checksum.
628
    ///
629
    /// # Arguments
630
    /// * `descriptor` - The descriptor string to analyze.
631
    ///
632
    /// # Returns
633
    /// A [`DescriptorInfoResponse`] containing parsed descriptor information such as the checksum.
634
    ///
635
    /// # Availability
636
    /// - **Since**: Bitcoin Core **v0.18.0**.
637
    pub fn get_descriptor_info(
364✔
638
        &self,
364✔
639
        descriptor: &str,
364✔
640
    ) -> BitcoinRpcClientResult<DescriptorInfoResponse> {
364✔
641
        Ok(self.endpoint.send(
364✔
642
            &self.client_id,
364✔
643
            None,
364✔
644
            "getdescriptorinfo",
364✔
645
            vec![descriptor.into()],
364✔
646
        )?)
×
647
    }
364✔
648

649
    /// Imports one or more descriptors into the currently loaded wallet.
650
    ///
651
    /// # Arguments
652
    /// * `wallet` - The name of the wallet to query. This is used to construct the wallet-specific RPC endpoint.
653
    /// * `descriptors` – A slice of [`ImportDescriptorsRequest`] items. Each item defines a single
654
    ///   descriptor and optional metadata for how it should be imported.
655
    ///
656
    /// # Returns
657
    /// A vector of [`ImportDescriptorsResponse`] results, one for each descriptor import attempt.
658
    ///
659
    /// # Availability
660
    /// - **Since**: Bitcoin Core **v0.21.0**.
661
    pub fn import_descriptors(
364✔
662
        &self,
364✔
663
        wallet: &str,
364✔
664
        descriptors: &[&ImportDescriptorsRequest],
364✔
665
    ) -> BitcoinRpcClientResult<Vec<ImportDescriptorsResponse>> {
364✔
666
        let descriptor_values = descriptors
364✔
667
            .iter()
364✔
668
            .map(serde_json::to_value)
364✔
669
            .collect::<Result<Vec<_>, _>>()?;
364✔
670

671
        Ok(self.endpoint.send(
364✔
672
            &self.client_id,
364✔
673
            Some(&Self::wallet_path(wallet)),
364✔
674
            "importdescriptors",
364✔
675
            vec![descriptor_values.into()],
364✔
676
        )?)
×
677
    }
364✔
678

679
    /// Returns the hash of the block at the given height.
680
    ///
681
    /// # Arguments
682
    /// * `height` - The height (block number) of the block whose hash is requested.
683
    ///
684
    /// # Returns
685
    /// A [`BurnchainHeaderHash`] representing the block hash.
686
    ///
687
    /// # Availability
688
    /// - **Since**: Bitcoin Core **v0.9.0**.
689
    pub fn get_block_hash(&self, height: u64) -> BitcoinRpcClientResult<BurnchainHeaderHash> {
8,999✔
690
        let response = self.endpoint.send::<BurnchainHeaderHashWrapperResponse>(
8,999✔
691
            &self.client_id,
8,999✔
692
            None,
8,999✔
693
            "getblockhash",
8,999✔
694
            vec![height.into()],
8,999✔
695
        )?;
×
696
        Ok(response.0)
8,999✔
697
    }
8,999✔
698
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc