Skip to content

Implement scantxoutset method and test #164

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions client/src/client_sync/v17/blockchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,32 @@ macro_rules! impl_client_v17__save_mempool {
};
}

/// Implements Bitcoin Core JSON-RPC API method `scantxoutset`
#[macro_export]
macro_rules! impl_client_v17__scan_tx_out_set {
() => {
impl Client {
/// Aborts an ongoing `scantxoutset` scan.
pub fn scan_tx_out_set_abort(&self) -> Result<ScanTxOutSetAbort> {
self.call("scantxoutset", &[into_json("abort")?])
}

/// Starts a scan of the UTXO set for specified descriptors.
pub fn scan_tx_out_set_start(
&self,
scan_objects: &[&str],
) -> Result<ScanTxOutSetStart> {
self.call("scantxoutset", &[into_json("start")?, into_json(scan_objects)?])
}

/// Checks the status of an ongoing `scantxoutset` scan.
pub fn scan_tx_out_set_status(&self) -> Result<Option<ScanTxOutSetStatus>> {
self.call("scantxoutset", &[into_json("status")?])
}
}
};
}

/// Implements Bitcoin Core JSON-RPC API method `verifychain`
#[macro_export]
macro_rules! impl_client_v17__verify_chain {
Expand Down
1 change: 1 addition & 0 deletions client/src/client_sync/v17/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ crate::impl_client_v17__get_tx_out_set_info!();
crate::impl_client_v17__precious_block!();
crate::impl_client_v17__prune_blockchain!();
crate::impl_client_v17__save_mempool!();
crate::impl_client_v17__scan_tx_out_set!();
crate::impl_client_v17__verify_chain!();
crate::impl_client_v17__verify_tx_out_proof!();

Expand Down
1 change: 1 addition & 0 deletions client/src/client_sync/v18/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ crate::impl_client_v17__get_tx_out_set_info!();
crate::impl_client_v17__precious_block!();
crate::impl_client_v17__prune_blockchain!();
crate::impl_client_v17__save_mempool!();
crate::impl_client_v17__scan_tx_out_set!();
crate::impl_client_v17__verify_chain!();
crate::impl_client_v17__verify_tx_out_proof!();

Expand Down
1 change: 1 addition & 0 deletions client/src/client_sync/v19/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ crate::impl_client_v17__get_tx_out_set_info!();
crate::impl_client_v17__precious_block!();
crate::impl_client_v17__prune_blockchain!();
crate::impl_client_v17__save_mempool!();
crate::impl_client_v17__scan_tx_out_set!();
crate::impl_client_v17__verify_chain!();
crate::impl_client_v17__verify_tx_out_proof!();

Expand Down
1 change: 1 addition & 0 deletions client/src/client_sync/v20/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ crate::impl_client_v17__get_tx_out_set_info!();
crate::impl_client_v17__precious_block!();
crate::impl_client_v17__prune_blockchain!();
crate::impl_client_v17__save_mempool!();
crate::impl_client_v17__scan_tx_out_set!();
crate::impl_client_v17__verify_chain!();
crate::impl_client_v17__verify_tx_out_proof!();

Expand Down
1 change: 1 addition & 0 deletions client/src/client_sync/v21/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ crate::impl_client_v17__get_tx_out_set_info!();
crate::impl_client_v17__precious_block!();
crate::impl_client_v17__prune_blockchain!();
crate::impl_client_v17__save_mempool!();
crate::impl_client_v17__scan_tx_out_set!();
crate::impl_client_v17__verify_chain!();
crate::impl_client_v17__verify_tx_out_proof!();

Expand Down
1 change: 1 addition & 0 deletions client/src/client_sync/v22/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ crate::impl_client_v17__get_tx_out_set_info!();
crate::impl_client_v17__precious_block!();
crate::impl_client_v17__prune_blockchain!();
crate::impl_client_v17__save_mempool!();
crate::impl_client_v17__scan_tx_out_set!();
crate::impl_client_v17__verify_chain!();
crate::impl_client_v17__verify_tx_out_proof!();

Expand Down
1 change: 1 addition & 0 deletions client/src/client_sync/v23/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ crate::impl_client_v17__get_tx_out_set_info!();
crate::impl_client_v17__precious_block!();
crate::impl_client_v17__prune_blockchain!();
crate::impl_client_v23__save_mempool!();
crate::impl_client_v17__scan_tx_out_set!();
crate::impl_client_v17__verify_chain!();
crate::impl_client_v17__verify_tx_out_proof!();

Expand Down
1 change: 1 addition & 0 deletions client/src/client_sync/v24/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ crate::impl_client_v17__get_tx_out_set_info!();
crate::impl_client_v17__precious_block!();
crate::impl_client_v17__prune_blockchain!();
crate::impl_client_v23__save_mempool!();
crate::impl_client_v17__scan_tx_out_set!();
crate::impl_client_v17__verify_chain!();
crate::impl_client_v17__verify_tx_out_proof!();

Expand Down
1 change: 1 addition & 0 deletions client/src/client_sync/v25/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ crate::impl_client_v17__get_tx_out_set_info!();
crate::impl_client_v17__precious_block!();
crate::impl_client_v17__prune_blockchain!();
crate::impl_client_v23__save_mempool!();
crate::impl_client_v17__scan_tx_out_set!();
crate::impl_client_v17__verify_chain!();
crate::impl_client_v17__verify_tx_out_proof!();

Expand Down
1 change: 1 addition & 0 deletions client/src/client_sync/v26/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ crate::impl_client_v26__get_tx_out_set_info!();
crate::impl_client_v17__precious_block!();
crate::impl_client_v17__prune_blockchain!();
crate::impl_client_v23__save_mempool!();
crate::impl_client_v17__scan_tx_out_set!();
crate::impl_client_v17__verify_chain!();
crate::impl_client_v17__verify_tx_out_proof!();

Expand Down
1 change: 1 addition & 0 deletions client/src/client_sync/v27/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ crate::impl_client_v26__get_tx_out_set_info!();
crate::impl_client_v17__precious_block!();
crate::impl_client_v17__prune_blockchain!();
crate::impl_client_v23__save_mempool!();
crate::impl_client_v17__scan_tx_out_set!();
crate::impl_client_v17__verify_chain!();
crate::impl_client_v17__verify_tx_out_proof!();

Expand Down
1 change: 1 addition & 0 deletions client/src/client_sync/v28/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ crate::impl_client_v26__get_tx_out_set_info!();
crate::impl_client_v17__precious_block!();
crate::impl_client_v17__prune_blockchain!();
crate::impl_client_v23__save_mempool!();
crate::impl_client_v17__scan_tx_out_set!();
crate::impl_client_v17__verify_chain!();
crate::impl_client_v17__verify_tx_out_proof!();

Expand Down
4 changes: 3 additions & 1 deletion client/src/client_sync/v29/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ use crate::types::v29::*;

#[rustfmt::skip] // Keep public re-exports separate.
pub use crate::client_sync::{
v17::{AddNodeCommand, ImportMultiRequest, ImportMultiScriptPubKey, ImportMultiTimestamp, Input, Output, SetBanCommand, WalletCreateFundedPsbtInput,},
v17::{AddNodeCommand, ImportMultiRequest, ImportMultiScriptPubKey, ImportMultiTimestamp, Input, Output, SetBanCommand, WalletCreateFundedPsbtInput
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This change is unrelated and wrong. It's not fixed by cargo fmt due to the skip above the block.

NB. I'm ok with the random fix being in this PR if done correctly though.

},
v21::ImportDescriptorsRequest,
v23::AddressType,
};
Expand Down Expand Up @@ -51,6 +52,7 @@ crate::impl_client_v26__get_tx_out_set_info!();
crate::impl_client_v17__precious_block!();
crate::impl_client_v17__prune_blockchain!();
crate::impl_client_v23__save_mempool!();
crate::impl_client_v17__scan_tx_out_set!();
crate::impl_client_v17__verify_chain!();
crate::impl_client_v17__verify_tx_out_proof!();

Expand Down
22 changes: 22 additions & 0 deletions integration_test/tests/blockchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,28 @@ fn blockchain__savemempool() {
}
}

#[test]
fn blockchain__scan_tx_out_set_modelled() {
let node = match () {
#[cfg(feature = "v21_and_below")]
() => Node::with_wallet(Wallet::None, &[]),
#[cfg(not(feature = "v21_and_below"))]
() => Node::with_wallet(Wallet::None, &["-coinstatsindex=1"])
};

let dummy_pubkey_hex = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798";
let scan_desc = format!("pkh({})", dummy_pubkey_hex);

let json: ScanTxOutSetStart = node.client.scan_tx_out_set_start(&[&scan_desc]).expect("scantxoutset start");

let _: Option<ScanTxOutSetStatus> = node.client.scan_tx_out_set_status().expect("scantxoutset status");

let model: Result<mtype::ScanTxOutSetStart, ScanTxOutSetError> = json.into_model();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving this line to directly below let json makes more sense. I had to look twice and first thought the model for ..Status was being checked and not ..Start.

model.unwrap();

let _: ScanTxOutSetAbort = node.client.scan_tx_out_set_abort().expect("scantxoutset abort");
}

#[test]
fn blockchain__verify_tx_out_proof__modelled() {
let node = Node::with_wallet(Wallet::Default, &[]);
Expand Down
39 changes: 39 additions & 0 deletions types/src/model/blockchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -621,3 +621,42 @@ pub struct ReceiveActivity {
/// The ScriptPubKey, converted to `model::ScriptPubkey`.
pub output_spk: ScriptPubkey,
}

/// Models the result of the JSON-RPC method `scantxoutset` start.
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct ScanTxOutSetStart {
/// Whether the scan is completed. For v19 onwards.
pub success: Option<bool>,
/// The number of unspent transaction outputs scanned. For v19 onwards.
pub txouts: Option<u64>,
/// The current block height (index). For v19 onwards.
pub height: Option<u64>,
/// The hash of the block at the tip of the chain. For v19 onwards.
pub bestblock: Option<BlockHash>,
/// The unspents
pub unspents: Vec<ScanTxOutSetUnspent>,
/// The total amount of all found unspent outputs in BTC
pub total_amount: Amount,
}

#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
Copy link
Collaborator

@jamillambert jamillambert Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
/// Unspents item returned as part of `scantxoutset`
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]

Needs docs, e.g. above. Having scanned through the code a lot recently I find it useful if the original RPC is mentioned.

pub struct ScanTxOutSetUnspent {
/// The transaction id
pub txid: Txid,
/// The vout value
pub vout: u32,
/// The script key
pub script_pubkey: ScriptBuf,
/// An output descriptor. For v18 onwards.
pub desc: Option<String>,
/// The total amount in BTC of unspent output
pub amount: Amount,
/// Whether this is a coinbase output. For v25 onwards.
pub coinbase: Option<bool>,
/// Height of the unspent transaction output
pub height: u64,
/// Blockhash of the unspent transaction output. For v28 onwards.
pub blockhash: Option<BlockHash>,
/// Number of confirmations of the unspent transaction output when the scan was done. For v28 onwards.
pub confirmations: Option<u64>,
}
3 changes: 2 additions & 1 deletion types/src/model/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ pub use self::{
GetDescriptorActivity, GetDifficulty, GetMempoolAncestors, GetMempoolAncestorsVerbose,
GetMempoolDescendants, GetMempoolDescendantsVerbose, GetMempoolEntry, GetMempoolInfo,
GetRawMempool, GetRawMempoolVerbose, GetTxOut, GetTxOutSetInfo, MempoolEntry,
MempoolEntryFees, ReceiveActivity, Softfork, SoftforkType, SpendActivity, VerifyTxOutProof,
MempoolEntryFees, ReceiveActivity, ScanTxOutSetStart, ScanTxOutSetUnspent, Softfork,
SoftforkType, SpendActivity, VerifyTxOutProof,
},
generating::{Generate, GenerateBlock, GenerateToAddress, GenerateToDescriptor},
mining::{
Expand Down
57 changes: 57 additions & 0 deletions types/src/v17/blockchain/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -564,3 +564,60 @@ impl std::error::Error for GetTxOutSetInfoError {
impl From<NumericError> for GetTxOutSetInfoError {
fn from(e: NumericError) -> Self { Self::Numeric(e) }
}

/// Error that can occur when converting the result of the `scantxoutset`
/// RPC method into the strongly typed `ScanTxOutSet` model.
#[derive(Debug)]
pub enum ScanTxOutSetError {
/// Failed to parse the `bestblock` field (a block hash) from a hex string.
BestBlockHash(hex::HexToArrayError),
/// Failed to parse the `blockhash` field (per unspent) from a hex string.
BlockHash(hex::HexToArrayError),
/// Failed to parse the `txid` field from a hex string.
Txid(hex::HexToArrayError),
/// Failed to parse the `scriptPubKey` field from hex string into script bytes.
ScriptPubKey(hex::HexToBytesError),
/// Failed to convert the `total_amount` field to a valid Bitcoin amount.
TotalAmount(amount::ParseAmountError),
/// Failed to convert an `amount` field in an unspent output to a valid Bitcoin amount.
Amount(amount::ParseAmountError),
/// A numeric field could not be converted to the expected Rust numeric type.
Numeric(NumericError),
}

impl fmt::Display for ScanTxOutSetError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use ScanTxOutSetError::*;

match self {
BestBlockHash(e) => write_err!(f, "conversion of the `bestblock` field failed"; e),
BlockHash(e) => write_err!(f, "conversion of the `blockhash` field failed"; e),
Txid(e) => write_err!(f, "conversion of the `txid` field failed"; e),
ScriptPubKey(e) => write_err!(f, "conversion of the `scriptPubKey` field failed"; e),
TotalAmount(e) => write_err!(f, "conversion of the `total_amount` field failed"; e),
Amount(e) => write_err!(f, "conversion of the `amount` field failed"; e),
Numeric(e) => write_err!(f, "numeric"; e),
}
}
}

#[cfg(feature = "std")]
impl std::error::Error for ScanTxOutSetError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
use ScanTxOutSetError::*;

match self {
BestBlockHash(e) => Some(e),
BlockHash(e) => Some(e),
Txid(e) => Some(e),
ScriptPubKey(e) => Some(e),
TotalAmount(e) => Some(e),
Amount(e) => Some(e),
Numeric(e) => Some(e),
}
}
}

impl From<NumericError> for ScanTxOutSetError {
fn from(e: NumericError) -> Self { Self::Numeric(e) }
}
44 changes: 43 additions & 1 deletion types/src/v17/blockchain/into.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: CC0-1.0

use bitcoin::consensus::encode;
use bitcoin::{block, hex, Block, BlockHash, CompactTarget, Txid, Weight, Work};
use bitcoin::{block, hex, Block, BlockHash, CompactTarget, ScriptBuf, Txid, Weight, Work};

// TODO: Use explicit imports?
use super::*;
Expand Down Expand Up @@ -550,6 +550,48 @@ impl GetTxOutSetInfo {
}
}

impl ScanTxOutSetStart {
pub fn into_model(self) -> Result<model::ScanTxOutSetStart, ScanTxOutSetError> {
use ScanTxOutSetError as E;

let unspents =
self.unspents.into_iter().map(|u| u.into_model()).collect::<Result<Vec<_>, _>>()?;

let total_amount = Amount::from_btc(self.total_amount).map_err(E::TotalAmount)?;

Ok(model::ScanTxOutSetStart {
success: None,
txouts: None,
height: None,
bestblock: None,
unspents,
total_amount,
})
}
}

impl ScanTxOutSetUnspent {
pub fn into_model(self) -> Result<model::ScanTxOutSetUnspent, ScanTxOutSetError> {
use ScanTxOutSetError as E;

let txid = self.txid.parse::<Txid>().map_err(E::Txid)?;
let amount = Amount::from_btc(self.amount).map_err(E::Amount)?;
let script_pubkey = ScriptBuf::from_hex(&self.script_pubkey).map_err(E::ScriptPubKey)?;

Ok(model::ScanTxOutSetUnspent {
txid,
vout: self.vout,
script_pubkey,
desc: None,
amount,
coinbase: None,
height: self.height,
blockhash: None,
confirmations: None,
})
}
}

impl VerifyTxOutProof {
/// Converts version specific type to a version nonspecific, more strongly typed type.
pub fn into_model(self) -> Result<model::VerifyTxOutProof, hex::HexToArrayError> {
Expand Down
39 changes: 39 additions & 0 deletions types/src/v17/blockchain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,45 @@ pub struct PruneBlockchain(
pub i64,
);

/// Result of JSON-RPC method `scantxoutset`.
///
/// > scantxoutset "action" ( [scanobjects,...] )
/// >
/// > Arguments:
/// > 1. "action" (string, required) The action to execute
/// > 2. "scanobjects" (array, required) Array of scan objects
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct ScanTxOutSetStart {
/// The unspents
pub unspents: Vec<ScanTxOutSetUnspent>,
/// The total amount of all found unspent outputs in BTC
pub total_amount: f64,
}

#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct ScanTxOutSetAbort(pub bool);
Comment on lines +696 to +698
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs rustdocs


#[derive(Deserialize, Debug, Clone, PartialEq)]
pub struct ScanTxOutSetStatus {
/// Approximate percent complete
pub progress: f64,
}
Comment on lines +700 to +704
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[derive(Deserialize, Debug, Clone, PartialEq)]
pub struct ScanTxOutSetStatus {
/// Approximate percent complete
pub progress: f64,
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct ScanTxOutSetStatus(pub f64);

Also needs docs. Was there a reason serialize was not included that I have missed?


#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct ScanTxOutSetUnspent {
/// The transaction id
pub txid: String,
/// The vout value
pub vout: u32,
/// The script key
#[serde(rename = "scriptPubKey")]
pub script_pubkey: String,
/// The total amount in BTC of unspent output
pub amount: f64,
/// Height of the unspent transaction output
pub height: u64,
}

/// Result of JSON-RPC method `verifychain`.
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
Expand Down
Loading