From 866fd60b7a677b554ae922212d7149e2b74fbee2 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Wed, 1 Oct 2025 16:29:43 -0300 Subject: [PATCH 01/23] chore(deps): add corepc-types --- Cargo.lock | 1 + Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index b3faed4..e54fbcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -170,6 +170,7 @@ dependencies = [ "base64 0.22.1", "bitcoin", "corepc-node", + "corepc-types", "hex-conservative", "reqwest", "secp256k1", diff --git a/Cargo.toml b/Cargo.toml index 0b48ecf..57ab7ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ keywords = ["crypto", "bitcoin"] [dependencies] base64 = "0.22.1" bitcoin = { version = "0.32.6", features = ["serde", "base64"] } +corepc-types = "0.9.0" hex = { package = "hex-conservative", version = "0.2.1" } # for optimization keep in sync with bitcoin reqwest = { version = "0.12.22", default-features = false, features = [ "http2", From b1ff45418122a97d3ef9586c414c8a20dc74efe4 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Wed, 1 Oct 2025 16:30:17 -0300 Subject: [PATCH 02/23] refactor: use corepc-types ListTransactions --- src/client.rs | 15 ++++--- src/traits.rs | 11 ++--- src/types.rs | 119 +------------------------------------------------- 3 files changed, 16 insertions(+), 129 deletions(-) diff --git a/src/client.rs b/src/client.rs index cfb5c79..7e7ba76 100644 --- a/src/client.rs +++ b/src/client.rs @@ -15,6 +15,7 @@ use bitcoin::{ consensus::{self, encode::serialize_hex}, Address, Block, BlockHash, Network, Transaction, Txid, }; +use corepc_types::v28::ListTransactions; use reqwest::{ header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}, Client as ReqwestClient, @@ -36,10 +37,10 @@ use crate::{ GetAddressInfo, GetBlockVerbosityOne, GetBlockVerbosityZero, GetBlockchainInfo, GetMempoolInfo, GetNewAddress, GetRawMempoolVerbose, GetRawTransactionVerbosityOne, GetRawTransactionVerbosityZero, GetTransaction, GetTxOut, ImportDescriptor, - ImportDescriptorResult, ListDescriptors, ListTransactions, ListUnspent, - ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, - SighashType, SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, - WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, + ImportDescriptorResult, ListDescriptors, ListUnspent, ListUnspentQueryOptions, + PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, SighashType, + SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, WalletCreateFundedPsbt, + WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, }, }; @@ -448,8 +449,8 @@ impl Wallet for Client { Ok(resp) } - async fn list_transactions(&self, count: Option) -> ClientResult> { - self.call::>("listtransactions", &[to_value(count)?]) + async fn list_transactions(&self, count: Option) -> ClientResult { + self.call::("listtransactions", &[to_value(count)?]) .await } @@ -804,7 +805,7 @@ mod test { // list_transactions let got = client.list_transactions(None).await.unwrap(); - assert_eq!(got.len(), 10); + assert_eq!(got.0.len(), 10); // list_unspent // let's mine one more block diff --git a/src/traits.rs b/src/traits.rs index 5851ad6..55f6273 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,4 +1,5 @@ use bitcoin::{bip32::Xpriv, block::Header, Address, Block, BlockHash, Network, Transaction, Txid}; +use corepc_types::v28::ListTransactions; use std::future::Future; use crate::{ @@ -7,10 +8,10 @@ use crate::{ CreateRawTransaction, CreateRawTransactionInput, CreateRawTransactionOutput, GetAddressInfo, GetBlockchainInfo, GetMempoolInfo, GetRawMempoolVerbose, GetRawTransactionVerbosityOne, GetRawTransactionVerbosityZero, GetTransaction, GetTxOut, - ImportDescriptor, ImportDescriptorResult, ListTransactions, ListUnspent, - ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, - SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, WalletCreateFundedPsbt, - WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, + ImportDescriptor, ImportDescriptorResult, ListUnspent, ListUnspentQueryOptions, + PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, SignRawTransactionWithWallet, + SubmitPackage, TestMempoolAccept, WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, + WalletProcessPsbtResult, }, }; @@ -211,7 +212,7 @@ pub trait Wallet { fn list_transactions( &self, count: Option, - ) -> impl Future>> + Send; + ) -> impl Future> + Send; /// Lists all wallets in the underlying Bitcoin client. fn list_wallets(&self) -> impl Future>> + Send; diff --git a/src/types.rs b/src/types.rs index 1a1bee6..398439c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,14 +1,13 @@ use std::collections::BTreeMap; use bitcoin::{ - absolute::Height, address::{self, NetworkUnchecked}, block::Header, consensus::{self, encode}, Address, Amount, Block, BlockHash, FeeRate, Psbt, SignedAmount, Transaction, Txid, Wtxid, }; use serde::{ - de::{self, IntoDeserializer, Visitor}, + de::{self, Visitor}, Deserialize, Deserializer, Serialize, Serializer, }; use tracing::*; @@ -440,7 +439,7 @@ pub struct SubmitPackageTxResultFees { #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct GetTransactionDetail { pub address: String, - pub category: GetTransactionDetailCategory, + pub category: TransactionCategory, pub amount: f64, pub label: Option, pub vout: u32, @@ -448,17 +447,6 @@ pub struct GetTransactionDetail { pub abandoned: Option, } -/// Enum to represent the category of a transaction. -#[derive(Copy, Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] -#[serde(rename_all = "lowercase")] -pub enum GetTransactionDetailCategory { - Send, - Receive, - Generate, - Immature, - Orphan, -} - /// Result of the JSON-RPC method `getnewaddress`. /// /// # Note @@ -567,41 +555,6 @@ pub struct ListUnspent { pub safe: bool, } -/// Models the result of JSON-RPC method `listtransactions`. -/// -/// # Note -/// -/// This assumes that the transactions are present in the underlying Bitcoin -/// client's wallet. -/// -/// Careful with the amount field. It is a [`SignedAmount`], hence can be negative. -/// Negative amounts for the [`TransactionCategory::Send`], and is positive -/// for all other categories. -#[derive(Clone, Debug, PartialEq, Deserialize)] -pub struct ListTransactions { - /// The Bitcoin address. - #[serde(deserialize_with = "deserialize_address")] - pub address: Address, - /// Category of the transaction. - category: TransactionCategory, - /// The signed amount in BTC. - #[serde(deserialize_with = "deserialize_signed_bitcoin")] - pub amount: SignedAmount, - /// The label associated with the address, if any. - pub label: Option, - /// The number of confirmations. - pub confirmations: u32, - pub trusted: Option, - pub generated: Option, - pub blockhash: Option, - pub blockheight: Option, - pub blockindex: Option, - pub blocktime: Option, - /// The transaction id. - #[serde(deserialize_with = "deserialize_txid")] - pub txid: Txid, -} - /// Models the result of JSON-RPC method `testmempoolaccept`. #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct TestMempoolAccept { @@ -815,21 +768,6 @@ where deserializer.deserialize_any(SatVisitor) } -/// Deserializes the *signed* amount in BTC into proper [`SignedAmount`]s. -#[expect(dead_code)] -fn deserialize_signed_bitcoin_option<'d, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'d>, -{ - let f: Option = Option::deserialize(deserializer)?; - match f { - Some(v) => deserialize_signed_bitcoin(v.into_deserializer()).map(Some), - None => Ok(None), - } -} - /// Deserializes the transaction id string into proper [`Txid`]s. fn deserialize_txid<'d, D>(deserializer: D) -> Result where @@ -974,59 +912,6 @@ where deserializer.deserialize_any(AddressVisitor) } -/// Deserializes the blockhash string into proper [`BlockHash`]s. -#[expect(dead_code)] -fn deserialize_blockhash<'d, D>(deserializer: D) -> Result -where - D: Deserializer<'d>, -{ - struct BlockHashVisitor; - - impl Visitor<'_> for BlockHashVisitor { - type Value = BlockHash; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(formatter, "a blockhash string expected") - } - - fn visit_str(self, v: &str) -> Result - where - E: de::Error, - { - let blockhash = consensus::encode::deserialize_hex::(v) - .expect("BlockHash deserialization failed"); - Ok(blockhash) - } - } - deserializer.deserialize_any(BlockHashVisitor) -} - -/// Deserializes the height string into proper [`Height`]s. -#[expect(dead_code)] -fn deserialize_height<'d, D>(deserializer: D) -> Result -where - D: Deserializer<'d>, -{ - struct HeightVisitor; - - impl Visitor<'_> for HeightVisitor { - type Value = Height; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(formatter, "a height u32 string expected") - } - - fn visit_u32(self, v: u32) -> Result - where - E: de::Error, - { - let height = Height::from_consensus(v).expect("Height deserialization failed"); - Ok(height) - } - } - deserializer.deserialize_any(HeightVisitor) -} - /// Signature hash types for Bitcoin transactions. /// /// These types specify which parts of a transaction are included in the signature From aeeb711f269f4174fe9e8061b6a31d98f8b1e807 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Wed, 1 Oct 2025 17:09:01 -0300 Subject: [PATCH 03/23] refactor: use corepc-types GetBlockchainInfo --- src/client.rs | 36 +++++++++++++++++++----------------- src/traits.rs | 12 ++++++------ src/types.rs | 42 ------------------------------------------ 3 files changed, 25 insertions(+), 65 deletions(-) diff --git a/src/client.rs b/src/client.rs index 7e7ba76..25c6e3c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -15,7 +15,8 @@ use bitcoin::{ consensus::{self, encode::serialize_hex}, Address, Block, BlockHash, Network, Transaction, Txid, }; -use corepc_types::v28::ListTransactions; +use corepc_types::model; +use corepc_types::v29::{GetBlockHeaderVerbose, GetBlockchainInfo, ListTransactions}; use reqwest::{ header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}, Client as ReqwestClient, @@ -28,19 +29,18 @@ use serde_json::{ use tokio::time::sleep; use tracing::*; -use super::types::GetBlockHeaderVerbosityZero; use crate::{ error::{BitcoinRpcError, ClientError}, traits::{Broadcaster, Reader, Signer, Wallet}, types::{ CreateRawTransaction, CreateRawTransactionInput, CreateRawTransactionOutput, CreateWallet, - GetAddressInfo, GetBlockVerbosityOne, GetBlockVerbosityZero, GetBlockchainInfo, - GetMempoolInfo, GetNewAddress, GetRawMempoolVerbose, GetRawTransactionVerbosityOne, - GetRawTransactionVerbosityZero, GetTransaction, GetTxOut, ImportDescriptor, - ImportDescriptorResult, ListDescriptors, ListUnspent, ListUnspentQueryOptions, - PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, SighashType, - SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, WalletCreateFundedPsbt, - WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, + GetAddressInfo, GetBlockVerbosityOne, GetBlockVerbosityZero, GetMempoolInfo, GetNewAddress, + GetRawMempoolVerbose, GetRawTransactionVerbosityOne, GetRawTransactionVerbosityZero, + GetTransaction, GetTxOut, ImportDescriptor, ImportDescriptorResult, ListDescriptors, + ListUnspent, ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, + PsbtBumpFeeOptions, SighashType, SignRawTransactionWithWallet, SubmitPackage, + TestMempoolAccept, WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, + WalletProcessPsbtResult, }, }; @@ -271,13 +271,13 @@ impl Reader for Client { async fn get_block_header(&self, hash: &BlockHash) -> ClientResult
{ let get_block_header = self - .call::( + .call::( "getblockheader", &[to_value(hash.to_string())?, to_value(false)?], ) .await?; let header = get_block_header - .header() + .block_header() .map_err(|err| ClientError::Other(format!("header decode: {err}")))?; Ok(header) } @@ -320,9 +320,10 @@ impl Reader for Client { .await } - async fn get_blockchain_info(&self) -> ClientResult { - self.call::("getblockchaininfo", &[]) - .await + async fn get_blockchain_info(&self) -> ClientResult { + let res = self.call::("getblockchaininfo", &[]) + .await?; + res.into_model().map_err(|e| ClientError::Parse(e.to_string())) } async fn get_current_timestamp(&self) -> ClientResult { @@ -449,9 +450,10 @@ impl Wallet for Client { Ok(resp) } - async fn list_transactions(&self, count: Option) -> ClientResult { - self.call::("listtransactions", &[to_value(count)?]) - .await + async fn list_transactions(&self, count: Option) -> ClientResult { + let resp = self.call::("listtransactions", &[to_value(count)?]) + .await?; + resp.into_model().map_err(|e| ClientError::Parse(e.to_string())) } async fn list_wallets(&self) -> ClientResult> { diff --git a/src/traits.rs b/src/traits.rs index 55f6273..4a1bff3 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,16 +1,16 @@ use bitcoin::{bip32::Xpriv, block::Header, Address, Block, BlockHash, Network, Transaction, Txid}; -use corepc_types::v28::ListTransactions; +use corepc_types::model::{GetBlockchainInfo, ListTransactions}; use std::future::Future; use crate::{ client::ClientResult, types::{ CreateRawTransaction, CreateRawTransactionInput, CreateRawTransactionOutput, - GetAddressInfo, GetBlockchainInfo, GetMempoolInfo, GetRawMempoolVerbose, - GetRawTransactionVerbosityOne, GetRawTransactionVerbosityZero, GetTransaction, GetTxOut, - ImportDescriptor, ImportDescriptorResult, ListUnspent, ListUnspentQueryOptions, - PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, SignRawTransactionWithWallet, - SubmitPackage, TestMempoolAccept, WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, + GetAddressInfo, GetMempoolInfo, GetRawMempoolVerbose, GetRawTransactionVerbosityOne, + GetRawTransactionVerbosityZero, GetTransaction, GetTxOut, ImportDescriptor, + ImportDescriptorResult, ListUnspent, ListUnspentQueryOptions, PreviousTransactionOutput, + PsbtBumpFee, PsbtBumpFeeOptions, SignRawTransactionWithWallet, SubmitPackage, + TestMempoolAccept, WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, }, }; diff --git a/src/types.rs b/src/types.rs index 398439c..7e727b4 100644 --- a/src/types.rs +++ b/src/types.rs @@ -38,48 +38,6 @@ pub enum TransactionCategory { Orphan, } -/// Result of JSON-RPC method `getblockchaininfo`. -/// -/// Method call: `getblockchaininfo` -/// -/// > Returns an object containing various state info regarding blockchain processing. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct GetBlockchainInfo { - /// Current network name as defined in BIP70 (main, test, signet, regtest). - pub chain: String, - /// The current number of blocks processed in the server. - pub blocks: u64, - /// The current number of headers we have validated. - pub headers: u64, - /// The hash of the currently best block. - #[serde(rename = "bestblockhash")] - pub best_block_hash: String, - /// The current difficulty. - pub difficulty: f64, - /// Median time for the current best block. - #[serde(rename = "mediantime")] - pub median_time: u64, - /// Estimate of verification progress (between 0 and 1). - #[serde(rename = "verificationprogress")] - pub verification_progress: f64, - /// Estimate of whether this node is in Initial Block Download (IBD) mode. - #[serde(rename = "initialblockdownload")] - pub initial_block_download: bool, - /// Total amount of work in active chain, in hexadecimal. - #[serde(rename = "chainwork")] - pub chain_work: String, - /// The estimated size of the block and undo files on disk. - pub size_on_disk: u64, - /// If the blocks are subject to pruning. - pub pruned: bool, - /// Lowest-height complete block stored (only present if pruning is enabled). - #[serde(rename = "pruneheight")] - pub prune_height: Option, - /// Whether automatic pruning is enabled (only present if pruning is enabled). - pub automatic_pruning: Option, - /// The target size used by pruning (only present if automatic pruning is enabled). - pub prune_target_size: Option, -} /// Result of JSON-RPC method `getblockheader` with verbosity set to 0. /// From deb995aabc839eb749496f9683bd196d39ecff30 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Wed, 1 Oct 2025 17:18:47 -0300 Subject: [PATCH 04/23] refactor: use corepc-types GetTransaction --- src/client.rs | 13 +++--- src/traits.rs | 11 ++--- src/types.rs | 124 +------------------------------------------------- 3 files changed, 13 insertions(+), 135 deletions(-) diff --git a/src/client.rs b/src/client.rs index 25c6e3c..c8bfa3f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -16,7 +16,7 @@ use bitcoin::{ Address, Block, BlockHash, Network, Transaction, Txid, }; use corepc_types::model; -use corepc_types::v29::{GetBlockHeaderVerbose, GetBlockchainInfo, ListTransactions}; +use corepc_types::v29::{GetBlockHeaderVerbose, GetBlockchainInfo, ListTransactions, GetTransaction}; use reqwest::{ header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}, Client as ReqwestClient, @@ -36,7 +36,7 @@ use crate::{ CreateRawTransaction, CreateRawTransactionInput, CreateRawTransactionOutput, CreateWallet, GetAddressInfo, GetBlockVerbosityOne, GetBlockVerbosityZero, GetMempoolInfo, GetNewAddress, GetRawMempoolVerbose, GetRawTransactionVerbosityOne, GetRawTransactionVerbosityZero, - GetTransaction, GetTxOut, ImportDescriptor, ImportDescriptorResult, ListDescriptors, + GetTxOut, ImportDescriptor, ImportDescriptorResult, ListDescriptors, ListUnspent, ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, SighashType, SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, @@ -439,9 +439,10 @@ impl Wallet for Client { .assume_checked(); Ok(address_unchecked) } - async fn get_transaction(&self, txid: &Txid) -> ClientResult { - self.call::("gettransaction", &[to_value(txid.to_string())?]) - .await + async fn get_transaction(&self, txid: &Txid) -> ClientResult { + let resp = self.call::("gettransaction", &[to_value(txid.to_string())?]) + .await?; + resp.into_model().map_err(|e| ClientError::Parse(e.to_string())) } async fn get_utxos(&self) -> ClientResult> { @@ -737,7 +738,7 @@ mod test { .unwrap(); // get_transaction - let tx = client.get_transaction(&txid).await.unwrap().hex; + let tx = client.get_transaction(&txid).await.unwrap().tx; let got = client.send_raw_transaction(&tx).await.unwrap(); let expected = txid; // Don't touch this! assert_eq!(expected, got); diff --git a/src/traits.rs b/src/traits.rs index 4a1bff3..46a71d4 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,5 +1,5 @@ use bitcoin::{bip32::Xpriv, block::Header, Address, Block, BlockHash, Network, Transaction, Txid}; -use corepc_types::model::{GetBlockchainInfo, ListTransactions}; +use corepc_types::model::{GetBlockchainInfo, GetTransaction, ListTransactions}; use std::future::Future; use crate::{ @@ -7,11 +7,10 @@ use crate::{ types::{ CreateRawTransaction, CreateRawTransactionInput, CreateRawTransactionOutput, GetAddressInfo, GetMempoolInfo, GetRawMempoolVerbose, GetRawTransactionVerbosityOne, - GetRawTransactionVerbosityZero, GetTransaction, GetTxOut, ImportDescriptor, - ImportDescriptorResult, ListUnspent, ListUnspentQueryOptions, PreviousTransactionOutput, - PsbtBumpFee, PsbtBumpFeeOptions, SignRawTransactionWithWallet, SubmitPackage, - TestMempoolAccept, WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, - WalletProcessPsbtResult, + GetRawTransactionVerbosityZero, GetTxOut, ImportDescriptor, ImportDescriptorResult, + ListUnspent, ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, + PsbtBumpFeeOptions, SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, + WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, }, }; diff --git a/src/types.rs b/src/types.rs index 7e727b4..addb71c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -4,41 +4,15 @@ use bitcoin::{ address::{self, NetworkUnchecked}, block::Header, consensus::{self, encode}, - Address, Amount, Block, BlockHash, FeeRate, Psbt, SignedAmount, Transaction, Txid, Wtxid, + Address, Amount, Block, BlockHash, FeeRate, Psbt, Transaction, Txid, Wtxid, }; use serde::{ de::{self, Visitor}, Deserialize, Deserializer, Serialize, Serializer, }; -use tracing::*; use crate::error::SignRawTransactionWithWalletError; -/// The category of a transaction. -/// -/// This is one of the results of `listtransactions` RPC method. -/// -/// # Note -/// -/// This is a subset of the categories available in Bitcoin Core. -/// It also assumes that the transactions are present in the underlying Bitcoin -/// client's wallet. -#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)] -#[serde(rename_all = "lowercase")] -pub enum TransactionCategory { - /// Transactions sent. - Send, - /// Non-coinbase transactions received. - Receive, - /// Coinbase transactions received with more than 100 confirmations. - Generate, - /// Coinbase transactions received with 100 or less confirmations. - Immature, - /// Orphaned coinbase transactions received. - Orphan, -} - - /// Result of JSON-RPC method `getblockheader` with verbosity set to 0. /// /// A string that is serialized, hex-encoded data for block 'hash'. @@ -388,23 +362,6 @@ pub struct SubmitPackageTxResultFees { pub effective_includes: Option>, } -/// Result of JSON-RPC method `gettxout`. -/// -/// # Note -/// -/// This assumes that the UTXOs are present in the underlying Bitcoin -/// client's wallet. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct GetTransactionDetail { - pub address: String, - pub category: TransactionCategory, - pub amount: f64, - pub label: Option, - pub vout: u32, - pub fee: Option, - pub abandoned: Option, -} - /// Result of the JSON-RPC method `getnewaddress`. /// /// # Note @@ -422,60 +379,6 @@ impl GetNewAddress { } } -/// Models the result of JSON-RPC method `listunspent`. -/// -/// # Note -/// -/// This assumes that the UTXOs are present in the underlying Bitcoin -/// client's wallet. -/// -/// Careful with the amount field. It is a [`SignedAmount`], hence can be negative. -/// Negative amounts for the [`TransactionCategory::Send`], and is positive -/// for all other categories. -#[derive(Clone, Debug, PartialEq, Deserialize)] -pub struct GetTransaction { - /// The signed amount in BTC. - #[serde(deserialize_with = "deserialize_signed_bitcoin")] - pub amount: SignedAmount, - /// The signed fee in BTC. - pub confirmations: u64, - pub generated: Option, - pub trusted: Option, - pub blockhash: Option, - pub blockheight: Option, - pub blockindex: Option, - pub blocktime: Option, - /// The transaction id. - #[serde(deserialize_with = "deserialize_txid")] - pub txid: Txid, - pub wtxid: String, - pub walletconflicts: Vec, - pub replaced_by_txid: Option, - pub replaces_txid: Option, - pub comment: Option, - pub to: Option, - pub time: u64, - pub timereceived: u64, - #[serde(rename = "bip125-replaceable")] - pub bip125_replaceable: String, - pub details: Vec, - /// The transaction itself. - #[serde(deserialize_with = "deserialize_tx")] - pub hex: Transaction, -} - -impl GetTransaction { - pub fn block_height(&self) -> u64 { - if self.confirmations == 0 { - return 0; - } - self.blockheight.unwrap_or_else(|| { - warn!("Txn confirmed but did not obtain blockheight. Setting height to zero"); - 0 - }) - } -} - /// Models the result of JSON-RPC method `listunspent`. /// /// # Note @@ -701,31 +604,6 @@ where deserializer.deserialize_any(FeeRateVisitor) } -/// Deserializes the *signed* amount in BTC into proper [`SignedAmount`]s. -fn deserialize_signed_bitcoin<'d, D>(deserializer: D) -> Result -where - D: Deserializer<'d>, -{ - struct SatVisitor; - - impl Visitor<'_> for SatVisitor { - type Value = SignedAmount; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(formatter, "a float representation of btc values expected") - } - - fn visit_f64(self, v: f64) -> Result - where - E: de::Error, - { - let signed_amount = SignedAmount::from_btc(v).expect("Amount deserialization failed"); - Ok(signed_amount) - } - } - deserializer.deserialize_any(SatVisitor) -} - /// Deserializes the transaction id string into proper [`Txid`]s. fn deserialize_txid<'d, D>(deserializer: D) -> Result where From 5ec0ebc8db5df04034a94ec20ff1bb54d3180915 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Thu, 2 Oct 2025 09:24:02 -0300 Subject: [PATCH 05/23] refactor: use corepc-types ListUnspent --- src/client.rs | 90 ++++++++++++++++++++++++++++++++++++--------------- src/traits.rs | 18 +++-------- src/types.rs | 37 --------------------- 3 files changed, 69 insertions(+), 76 deletions(-) diff --git a/src/client.rs b/src/client.rs index c8bfa3f..7ff51e6 100644 --- a/src/client.rs +++ b/src/client.rs @@ -16,7 +16,9 @@ use bitcoin::{ Address, Block, BlockHash, Network, Transaction, Txid, }; use corepc_types::model; -use corepc_types::v29::{GetBlockHeaderVerbose, GetBlockchainInfo, ListTransactions, GetTransaction}; +use corepc_types::v29::{ + GetBlockHeaderVerbose, GetBlockchainInfo, GetTransaction, ListTransactions, +}; use reqwest::{ header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}, Client as ReqwestClient, @@ -37,10 +39,9 @@ use crate::{ GetAddressInfo, GetBlockVerbosityOne, GetBlockVerbosityZero, GetMempoolInfo, GetNewAddress, GetRawMempoolVerbose, GetRawTransactionVerbosityOne, GetRawTransactionVerbosityZero, GetTxOut, ImportDescriptor, ImportDescriptorResult, ListDescriptors, - ListUnspent, ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, - PsbtBumpFeeOptions, SighashType, SignRawTransactionWithWallet, SubmitPackage, - TestMempoolAccept, WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, - WalletProcessPsbtResult, + ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, + SighashType, SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, + WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, }, }; @@ -321,9 +322,11 @@ impl Reader for Client { } async fn get_blockchain_info(&self) -> ClientResult { - let res = self.call::("getblockchaininfo", &[]) + let res = self + .call::("getblockchaininfo", &[]) .await?; - res.into_model().map_err(|e| ClientError::Parse(e.to_string())) + res.into_model() + .map_err(|e| ClientError::Parse(e.to_string())) } async fn get_current_timestamp(&self) -> ClientResult { @@ -440,21 +443,22 @@ impl Wallet for Client { Ok(address_unchecked) } async fn get_transaction(&self, txid: &Txid) -> ClientResult { - let resp = self.call::("gettransaction", &[to_value(txid.to_string())?]) + let resp = self + .call::("gettransaction", &[to_value(txid.to_string())?]) .await?; - resp.into_model().map_err(|e| ClientError::Parse(e.to_string())) - } - - async fn get_utxos(&self) -> ClientResult> { - let resp = self.call::>("listunspent", &[]).await?; - trace!(?resp, "Got UTXOs"); - Ok(resp) + resp.into_model() + .map_err(|e| ClientError::Parse(e.to_string())) } - async fn list_transactions(&self, count: Option) -> ClientResult { - let resp = self.call::("listtransactions", &[to_value(count)?]) + async fn list_transactions( + &self, + count: Option, + ) -> ClientResult { + let resp = self + .call::("listtransactions", &[to_value(count)?]) .await?; - resp.into_model().map_err(|e| ClientError::Parse(e.to_string())) + resp.into_model() + .map_err(|e| ClientError::Parse(e.to_string())) } async fn list_wallets(&self) -> ClientResult> { @@ -510,7 +514,7 @@ impl Wallet for Client { addresses: Option<&[Address]>, include_unsafe: Option, query_options: Option, - ) -> ClientResult> { + ) -> ClientResult { let addr_strings: Vec = addresses .map(|addrs| addrs.iter().map(|a| a.to_string()).collect()) .unwrap_or_default(); @@ -526,7 +530,41 @@ impl Wallet for Client { params.push(to_value(query_options)?); } - self.call::>("listunspent", ¶ms).await + let resp = self + .call::("listunspent", ¶ms) + .await?; + trace!(?resp, "Got UTXOs"); + let mut utxos: Vec = + serde_json::from_value(resp).map_err(|e| ClientError::Parse(e.to_string()))?; + + // FIXME(corepc-types): Transform field names in each UTXO + for utxo in &mut utxos { + if let Some(utxo_map) = utxo.as_object_mut() { + // Rename scriptPubKey to script_pubkey + if let Some(script_pubkey) = utxo_map.remove("scriptPubKey") { + utxo_map.insert("script_pubkey".to_string(), script_pubkey); + } + + // Rename desc to descriptor + if let Some(desc) = utxo_map.remove("desc") { + utxo_map.insert("descriptor".to_string(), desc); + } + + // Add missing label field if not present + if !utxo_map.contains_key("label") { + utxo_map.insert( + "label".to_string(), + serde_json::Value::String(String::new()), + ); + } + } + } + + let list_unspent: model::ListUnspent = + serde_json::from_value(serde_json::Value::Array(utxos)) + .map_err(|e| ClientError::Parse(e.to_string()))?; + + Ok(list_unspent) } } @@ -817,7 +855,7 @@ mod test { .list_unspent(None, None, None, None, None) .await .unwrap(); - assert_eq!(got.len(), 3); + assert_eq!(got.0.len(), 3); // listdescriptors let got = client.get_xpriv().await.unwrap().unwrap().network; @@ -900,7 +938,7 @@ mod test { .list_unspent(Some(1), Some(9_999_999), None, Some(true), None) .await .unwrap(); - assert!(!utxos.is_empty()); + assert!(!utxos.0.is_empty()); let utxos_filtered = client .list_unspent( @@ -912,8 +950,8 @@ mod test { ) .await .unwrap(); - assert!(!utxos_filtered.is_empty()); - let found_utxo = utxos_filtered.iter().any(|utxo| { + assert!(!utxos_filtered.0.is_empty()); + let found_utxo = utxos_filtered.0.iter().any(|utxo| { utxo.txid.to_string() == unspent_txid && utxo.address.clone().assume_checked().to_string() == unspent_address.to_string() }); @@ -934,8 +972,8 @@ mod test { ) .await .unwrap(); - assert!(!utxos_with_query.is_empty()); - for utxo in &utxos_with_query { + assert!(!utxos_with_query.0.is_empty()); + for utxo in &utxos_with_query.0 { let amount_btc = utxo.amount.to_btc(); assert!((0.5..=2.0).contains(&amount_btc)); } diff --git a/src/traits.rs b/src/traits.rs index 46a71d4..8c63cbd 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,5 +1,5 @@ use bitcoin::{bip32::Xpriv, block::Header, Address, Block, BlockHash, Network, Transaction, Txid}; -use corepc_types::model::{GetBlockchainInfo, GetTransaction, ListTransactions}; +use corepc_types::model::{GetBlockchainInfo, GetTransaction, ListTransactions, ListUnspent}; use std::future::Future; use crate::{ @@ -8,9 +8,9 @@ use crate::{ CreateRawTransaction, CreateRawTransactionInput, CreateRawTransactionOutput, GetAddressInfo, GetMempoolInfo, GetRawMempoolVerbose, GetRawTransactionVerbosityOne, GetRawTransactionVerbosityZero, GetTxOut, ImportDescriptor, ImportDescriptorResult, - ListUnspent, ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, - PsbtBumpFeeOptions, SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, - WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, + ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, + SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, WalletCreateFundedPsbt, + WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, }, }; @@ -194,14 +194,6 @@ pub trait Wallet { txid: &Txid, ) -> impl Future> + Send; - /// Gets all Unspent Transaction Outputs (UTXOs) for the underlying Bitcoin - /// client's wallet. - #[deprecated( - since = "0.4.0", - note = "Does not adhere with bitcoin core RPC naming. Use `list_unspent` instead" - )] - fn get_utxos(&self) -> impl Future>> + Send; - /// Lists transactions in the underlying Bitcoin client's wallet. /// /// # Parameters @@ -318,7 +310,7 @@ pub trait Wallet { addresses: Option<&[Address]>, include_unsafe: Option, query_options: Option, - ) -> impl Future>> + Send; + ) -> impl Future> + Send; } /// Signing functionality that any Bitcoin client **with private keys** that diff --git a/src/types.rs b/src/types.rs index addb71c..a358545 100644 --- a/src/types.rs +++ b/src/types.rs @@ -379,43 +379,6 @@ impl GetNewAddress { } } -/// Models the result of JSON-RPC method `listunspent`. -/// -/// # Note -/// -/// This assumes that the UTXOs are present in the underlying Bitcoin -/// client's wallet. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct ListUnspent { - /// The transaction id. - #[serde(deserialize_with = "deserialize_txid")] - pub txid: Txid, - /// The vout value. - pub vout: u32, - /// The Bitcoin address. - #[serde(deserialize_with = "deserialize_address")] - pub address: Address, - // The associated label, if any. - pub label: Option, - /// The script pubkey. - #[serde(rename = "scriptPubKey")] - pub script_pubkey: String, - /// The transaction output amount in BTC. - #[serde(deserialize_with = "deserialize_bitcoin")] - pub amount: Amount, - /// The number of confirmations. - pub confirmations: u32, - /// Whether we have the private keys to spend this output. - pub spendable: bool, - /// Whether we know how to spend this output, ignoring the lack of keys. - pub solvable: bool, - /// Whether this output is considered safe to spend. - /// Unconfirmed transactions from outside keys and unconfirmed replacement - /// transactions are considered unsafe and are not eligible for spending by - /// `fundrawtransaction` and `sendtoaddress`. - pub safe: bool, -} - /// Models the result of JSON-RPC method `testmempoolaccept`. #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct TestMempoolAccept { From f0c4a2e49026ede972194353fa28b7ef5244cbb3 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Thu, 2 Oct 2025 09:29:46 -0300 Subject: [PATCH 06/23] refactor: use corepc-types GetBlockVerbose{Zero,One} --- src/client.rs | 24 +++++++------- src/types.rs | 86 +-------------------------------------------------- 2 files changed, 14 insertions(+), 96 deletions(-) diff --git a/src/client.rs b/src/client.rs index 7ff51e6..66884e2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -17,7 +17,8 @@ use bitcoin::{ }; use corepc_types::model; use corepc_types::v29::{ - GetBlockHeaderVerbose, GetBlockchainInfo, GetTransaction, ListTransactions, + GetBlockHeaderVerbose, GetBlockVerboseOne, GetBlockVerboseZero, GetBlockchainInfo, + GetTransaction, ListTransactions, }; use reqwest::{ header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}, @@ -36,12 +37,12 @@ use crate::{ traits::{Broadcaster, Reader, Signer, Wallet}, types::{ CreateRawTransaction, CreateRawTransactionInput, CreateRawTransactionOutput, CreateWallet, - GetAddressInfo, GetBlockVerbosityOne, GetBlockVerbosityZero, GetMempoolInfo, GetNewAddress, - GetRawMempoolVerbose, GetRawTransactionVerbosityOne, GetRawTransactionVerbosityZero, - GetTxOut, ImportDescriptor, ImportDescriptorResult, ListDescriptors, - ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, - SighashType, SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, - WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, + GetAddressInfo, GetMempoolInfo, GetNewAddress, GetRawMempoolVerbose, + GetRawTransactionVerbosityOne, GetRawTransactionVerbosityZero, GetTxOut, ImportDescriptor, + ImportDescriptorResult, ListDescriptors, ListUnspentQueryOptions, + PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, SighashType, + SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, WalletCreateFundedPsbt, + WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, }, }; @@ -285,17 +286,18 @@ impl Reader for Client { async fn get_block(&self, hash: &BlockHash) -> ClientResult { let get_block = self - .call::("getblock", &[to_value(hash.to_string())?, to_value(0)?]) + .call::("getblock", &[to_value(hash.to_string())?, to_value(0)?]) .await?; let block = get_block - .block() - .map_err(|err| ClientError::Other(format!("block decode: {err}")))?; + .into_model() + .map_err(|e| ClientError::Parse(e.to_string()))? + .0; Ok(block) } async fn get_block_height(&self, hash: &BlockHash) -> ClientResult { let block_verobose = self - .call::("getblock", &[to_value(hash.to_string())?]) + .call::("getblock", &[to_value(hash.to_string())?]) .await?; let block_height = block_verobose.height as u64; diff --git a/src/types.rs b/src/types.rs index a358545..c8e172f 100644 --- a/src/types.rs +++ b/src/types.rs @@ -2,9 +2,8 @@ use std::collections::BTreeMap; use bitcoin::{ address::{self, NetworkUnchecked}, - block::Header, consensus::{self, encode}, - Address, Amount, Block, BlockHash, FeeRate, Psbt, Transaction, Txid, Wtxid, + Address, Amount, BlockHash, FeeRate, Psbt, Transaction, Txid, Wtxid, }; use serde::{ de::{self, Visitor}, @@ -13,89 +12,6 @@ use serde::{ use crate::error::SignRawTransactionWithWalletError; -/// Result of JSON-RPC method `getblockheader` with verbosity set to 0. -/// -/// A string that is serialized, hex-encoded data for block 'hash'. -/// -/// Method call: `getblockheader "blockhash" ( verbosity )` -#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] -pub struct GetBlockHeaderVerbosityZero(pub String); - -impl GetBlockHeaderVerbosityZero { - /// Converts json straight to a [`Header`]. - pub fn header(self) -> Result { - let header: Header = encode::deserialize_hex(&self.0)?; - Ok(header) - } -} - -/// Result of JSON-RPC method `getblock` with verbosity set to 0. -/// -/// A string that is serialized, hex-encoded data for block 'hash'. -/// -/// Method call: `getblock "blockhash" ( verbosity )` -#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] -pub struct GetBlockVerbosityZero(pub String); - -impl GetBlockVerbosityZero { - /// Converts json straight to a [`Block`]. - pub fn block(self) -> Result { - let block: Block = encode::deserialize_hex(&self.0)?; - Ok(block) - } -} - -/// Result of JSON-RPC method `getblock` with verbosity set to 1. -#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] -pub struct GetBlockVerbosityOne { - /// The block hash (same as provided) in RPC call. - pub hash: String, - /// The number of confirmations, or -1 if the block is not on the main chain. - pub confirmations: i32, - /// The block size. - pub size: usize, - /// The block size excluding witness data. - #[serde(rename = "strippedsize")] - pub stripped_size: Option, - /// The block weight as defined in BIP-141. - pub weight: u64, - /// The block height or index. - pub height: usize, - /// The block version. - pub version: i32, - /// The block version formatted in hexadecimal. - #[serde(rename = "versionHex")] - pub version_hex: String, - /// The merkle root - #[serde(rename = "merkleroot")] - pub merkle_root: String, - /// The transaction ids - pub tx: Vec, - /// The block time expressed in UNIX epoch time. - pub time: usize, - /// The median block time expressed in UNIX epoch time. - #[serde(rename = "mediantime")] - pub median_time: Option, - /// The nonce - pub nonce: u32, - /// The bits. - pub bits: String, - /// The difficulty. - pub difficulty: f64, - /// Expected number of hashes required to produce the chain up to this block (in hex). - #[serde(rename = "chainwork")] - pub chain_work: String, - /// The number of transactions in the block. - #[serde(rename = "nTx")] - pub n_tx: u32, - /// The hash of the previous block (if available). - #[serde(rename = "previousblockhash")] - pub previous_block_hash: Option, - /// The hash of the next block (if available). - #[serde(rename = "nextblockhash")] - pub next_block_hash: Option, -} - /// Result of JSON-RPC method `getrawtransaction` with verbosity set to 0. /// /// A string that is serialized, hex-encoded data for transaction. From 2a475308ebab72caf21e9ed75d3cd98f8cf43dd6 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Thu, 2 Oct 2025 09:41:03 -0300 Subject: [PATCH 07/23] refactor: use corepc-types GetRawTransaction{,Verbose} --- src/client.rs | 45 +++++++++++++++++++--------------- src/traits.rs | 18 ++++++++------ src/types.rs | 67 ++------------------------------------------------- 3 files changed, 38 insertions(+), 92 deletions(-) diff --git a/src/client.rs b/src/client.rs index 66884e2..67718a2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -18,7 +18,7 @@ use bitcoin::{ use corepc_types::model; use corepc_types::v29::{ GetBlockHeaderVerbose, GetBlockVerboseOne, GetBlockVerboseZero, GetBlockchainInfo, - GetTransaction, ListTransactions, + GetRawTransaction, GetRawTransactionVerbose, GetTransaction, ListTransactions, }; use reqwest::{ header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}, @@ -37,9 +37,8 @@ use crate::{ traits::{Broadcaster, Reader, Signer, Wallet}, types::{ CreateRawTransaction, CreateRawTransactionInput, CreateRawTransactionOutput, CreateWallet, - GetAddressInfo, GetMempoolInfo, GetNewAddress, GetRawMempoolVerbose, - GetRawTransactionVerbosityOne, GetRawTransactionVerbosityZero, GetTxOut, ImportDescriptor, - ImportDescriptorResult, ListDescriptors, ListUnspentQueryOptions, + GetAddressInfo, GetMempoolInfo, GetNewAddress, GetRawMempoolVerbose, GetTxOut, + ImportDescriptor, ImportDescriptorResult, ListDescriptors, ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, SighashType, SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, @@ -353,23 +352,31 @@ impl Reader for Client { async fn get_raw_transaction_verbosity_zero( &self, txid: &Txid, - ) -> ClientResult { - self.call::( - "getrawtransaction", - &[to_value(txid.to_string())?, to_value(0)?], - ) - .await + ) -> ClientResult { + let resp = self + .call::( + "getrawtransaction", + &[to_value(txid.to_string())?, to_value(0)?], + ) + .await + .map_err(|e| ClientError::Parse(e.to_string()))?; + resp.into_model() + .map_err(|e| ClientError::Parse(e.to_string())) } async fn get_raw_transaction_verbosity_one( &self, txid: &Txid, - ) -> ClientResult { - self.call::( - "getrawtransaction", - &[to_value(txid.to_string())?, to_value(1)?], - ) - .await + ) -> ClientResult { + let resp = self + .call::( + "getrawtransaction", + &[to_value(txid.to_string())?, to_value(1)?], + ) + .await + .map_err(|e| ClientError::Parse(e.to_string()))?; + resp.into_model() + .map_err(|e| ClientError::Parse(e.to_string())) } async fn get_tx_out( @@ -688,7 +695,7 @@ mod test { use std::sync::Once; use bitcoin::{ - consensus::{self, encode::deserialize_hex}, + consensus::{self}, hashes::Hash, transaction, Amount, FeeRate, NetworkKind, }; @@ -788,8 +795,8 @@ mod test { .get_raw_transaction_verbosity_zero(&txid) .await .unwrap() - .0; - let got = deserialize_hex::(&got).unwrap().compute_txid(); + .0 + .compute_txid(); assert_eq!(expected, got); // get_raw_transaction_verbosity_one diff --git a/src/traits.rs b/src/traits.rs index 8c63cbd..3fc65ef 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,16 +1,18 @@ use bitcoin::{bip32::Xpriv, block::Header, Address, Block, BlockHash, Network, Transaction, Txid}; -use corepc_types::model::{GetBlockchainInfo, GetTransaction, ListTransactions, ListUnspent}; +use corepc_types::model::{ + GetBlockchainInfo, GetRawTransaction, GetRawTransactionVerbose, GetTransaction, + ListTransactions, ListUnspent, +}; use std::future::Future; use crate::{ client::ClientResult, types::{ CreateRawTransaction, CreateRawTransactionInput, CreateRawTransactionOutput, - GetAddressInfo, GetMempoolInfo, GetRawMempoolVerbose, GetRawTransactionVerbosityOne, - GetRawTransactionVerbosityZero, GetTxOut, ImportDescriptor, ImportDescriptorResult, - ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, - SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, WalletCreateFundedPsbt, - WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, + GetAddressInfo, GetMempoolInfo, GetRawMempoolVerbose, GetTxOut, ImportDescriptor, + ImportDescriptorResult, ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, + PsbtBumpFeeOptions, SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, + WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, }, }; @@ -100,13 +102,13 @@ pub trait Reader { fn get_raw_transaction_verbosity_zero( &self, txid: &Txid, - ) -> impl Future> + Send; + ) -> impl Future> + Send; /// Gets a raw transaction by its [`Txid`]. fn get_raw_transaction_verbosity_one( &self, txid: &Txid, - ) -> impl Future> + Send; + ) -> impl Future> + Send; /// Returns details about an unspent transaction output. fn get_tx_out( diff --git a/src/types.rs b/src/types.rs index c8e172f..01937dc 100644 --- a/src/types.rs +++ b/src/types.rs @@ -2,8 +2,8 @@ use std::collections::BTreeMap; use bitcoin::{ address::{self, NetworkUnchecked}, - consensus::{self, encode}, - Address, Amount, BlockHash, FeeRate, Psbt, Transaction, Txid, Wtxid, + consensus::{self}, + Address, Amount, FeeRate, Psbt, Transaction, Txid, }; use serde::{ de::{self, Visitor}, @@ -12,22 +12,6 @@ use serde::{ use crate::error::SignRawTransactionWithWalletError; -/// Result of JSON-RPC method `getrawtransaction` with verbosity set to 0. -/// -/// A string that is serialized, hex-encoded data for transaction. -/// -/// Method call: `getrawtransaction "txid" ( verbosity )` -#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] -pub struct GetRawTransactionVerbosityZero(pub String); - -impl GetRawTransactionVerbosityZero { - /// Converts json straight to a [`Transaction`]. - pub fn transaction(self) -> Result { - let transaction: Transaction = encode::deserialize_hex(&self.0)?; - Ok(transaction) - } -} - /// Result of JSON-RPC method `getmempoolinfo`. /// /// Method call: `getmempoolinfo` @@ -80,27 +64,6 @@ pub struct MempoolFeeBreakdown { pub descendant: Amount, } -/// Result of JSON-RPC method `getrawtransaction` with verbosity set to 1. -/// -/// Method call: `getrawtransaction "txid" ( verbosity )` -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct GetRawTransactionVerbosityOne { - pub in_active_chain: Option, - #[serde(deserialize_with = "deserialize_tx")] - #[serde(rename = "hex")] - pub transaction: Transaction, - pub txid: Txid, - pub hash: Wtxid, - pub size: usize, - pub vsize: usize, - pub version: u32, - pub locktime: u32, - pub blockhash: Option, - pub confirmations: Option, - pub time: Option, - pub blocktime: Option, -} - /// Result of JSON-RPC method `gettxout`. /// /// > gettxout "txid" n ( include_mempool ) @@ -509,32 +472,6 @@ where deserializer.deserialize_any(TxidVisitor) } -/// Deserializes the transaction hex string into proper [`Transaction`]s. -fn deserialize_tx<'d, D>(deserializer: D) -> Result -where - D: Deserializer<'d>, -{ - struct TxVisitor; - - impl Visitor<'_> for TxVisitor { - type Value = Transaction; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(formatter, "a transaction hex string expected") - } - - fn visit_str(self, v: &str) -> Result - where - E: de::Error, - { - let tx = consensus::encode::deserialize_hex::(v) - .expect("failed to deserialize tx hex"); - Ok(tx) - } - } - deserializer.deserialize_any(TxVisitor) -} - /// Deserializes a base64-encoded PSBT string into proper [`Psbt`]s. /// /// # Note From 139609926aebbdd4aca9857a5060295ff44cf2fd Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Thu, 2 Oct 2025 10:10:59 -0300 Subject: [PATCH 08/23] refactor: use corepc-types mempool stuff --- src/client.rs | 90 ++++++++++++++++++++++++++++++++++++++++----------- src/traits.rs | 14 ++++---- src/types.rs | 52 ----------------------------- 3 files changed, 78 insertions(+), 78 deletions(-) diff --git a/src/client.rs b/src/client.rs index 67718a2..fd32138 100644 --- a/src/client.rs +++ b/src/client.rs @@ -18,7 +18,8 @@ use bitcoin::{ use corepc_types::model; use corepc_types::v29::{ GetBlockHeaderVerbose, GetBlockVerboseOne, GetBlockVerboseZero, GetBlockchainInfo, - GetRawTransaction, GetRawTransactionVerbose, GetTransaction, ListTransactions, + GetMempoolInfo, GetRawMempool, GetRawMempoolVerbose, GetRawTransaction, + GetRawTransactionVerbose, GetTransaction, ListTransactions, }; use reqwest::{ header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}, @@ -37,11 +38,11 @@ use crate::{ traits::{Broadcaster, Reader, Signer, Wallet}, types::{ CreateRawTransaction, CreateRawTransactionInput, CreateRawTransactionOutput, CreateWallet, - GetAddressInfo, GetMempoolInfo, GetNewAddress, GetRawMempoolVerbose, GetTxOut, - ImportDescriptor, ImportDescriptorResult, ListDescriptors, ListUnspentQueryOptions, - PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, SighashType, - SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, WalletCreateFundedPsbt, - WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, + GetAddressInfo, GetNewAddress, GetTxOut, ImportDescriptor, ImportDescriptorResult, + ListDescriptors, ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, + PsbtBumpFeeOptions, SighashType, SignRawTransactionWithWallet, SubmitPackage, + TestMempoolAccept, WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, + WalletProcessPsbtResult, }, }; @@ -336,17 +337,68 @@ impl Reader for Client { Ok(block.header.time) } - async fn get_raw_mempool(&self) -> ClientResult> { - self.call::>("getrawmempool", &[]).await + async fn get_raw_mempool(&self) -> ClientResult { + let resp = self.call::("getrawmempool", &[]).await?; + resp.into_model() + .map_err(|e| ClientError::Parse(e.to_string())) } - async fn get_raw_mempool_verbose(&self) -> ClientResult { - self.call::("getrawmempool", &[to_value(true)?]) - .await + async fn get_raw_mempool_verbose(&self) -> ClientResult { + let resp = self + .call::("getrawmempool", &[to_value(true)?]) + .await?; + trace!(?resp, "Got raw mempool verbose"); + + let mut mempool_map: serde_json::Map = + serde_json::from_value(resp).map_err(|e| ClientError::Parse(e.to_string()))?; + + // FIXME(corepc-types): Transform field names in each mempool entry + for (_txid, entry) in &mut mempool_map { + if let Some(entry_map) = entry.as_object_mut() { + // Rename vsize to size + if let Some(vsize) = entry_map.remove("vsize") { + entry_map.insert("size".to_string(), vsize); + } + + // Flatten fees object: fees.base -> fee, fees.modified -> modifiedfee, etc. + // Keep the fees object too, as model might need both + if let Some(fees_obj) = entry_map.get("fees").cloned() { + if let Some(fees_map) = fees_obj.as_object() { + if let Some(base) = fees_map.get("base") { + entry_map.insert("fee".to_string(), base.clone()); + } + if let Some(modified) = fees_map.get("modified") { + entry_map.insert("modifiedfee".to_string(), modified.clone()); + } + if let Some(ancestor) = fees_map.get("ancestor") { + entry_map.insert("ancestorfees".to_string(), ancestor.clone()); + } + if let Some(descendant) = fees_map.get("descendant") { + entry_map.insert("descendantfees".to_string(), descendant.clone()); + } + } + } + + // Remove fields not expected by model + entry_map.remove("bip125-replaceable"); + entry_map.remove("unbroadcast"); + entry_map.remove("weight"); + } + } + + let mempool_verbose: GetRawMempoolVerbose = + serde_json::from_value(serde_json::Value::Object(mempool_map)) + .map_err(|e| ClientError::Parse(e.to_string()))?; + + mempool_verbose + .into_model() + .map_err(|e| ClientError::Parse(e.to_string())) } - async fn get_mempool_info(&self) -> ClientResult { - self.call::("getmempoolinfo", &[]).await + async fn get_mempool_info(&self) -> ClientResult { + let resp = self.call::("getmempoolinfo", &[]).await?; + resp.into_model() + .map_err(|e| ClientError::Parse(e.to_string())) } async fn get_raw_transaction_verbosity_zero( @@ -811,18 +863,18 @@ mod test { // get_raw_mempool let got = client.get_raw_mempool().await.unwrap(); let expected = vec![txid]; - assert_eq!(expected, got); + assert_eq!(expected, got.0); // get_raw_mempool_verbose let got = client.get_raw_mempool_verbose().await.unwrap(); - assert_eq!(got.len(), 1); - assert_eq!(got.get(&txid).unwrap().height, 101); + assert_eq!(got.0.len(), 1); + assert_eq!(got.0.get(&txid).unwrap().height, 101); // get_mempool_info let got = client.get_mempool_info().await.unwrap(); - assert!(got.loaded); + assert!(got.loaded.unwrap_or(false)); assert_eq!(got.size, 1); - assert_eq!(got.unbroadcastcount, 1); + assert_eq!(got.unbroadcast_count, Some(1)); // estimate_smart_fee let got = client.estimate_smart_fee(1).await.unwrap(); @@ -1281,7 +1333,7 @@ mod test { // Verify transaction is in mempool (unconfirmed) let mempool = client.get_raw_mempool().await.unwrap(); assert!( - mempool.contains(&txid), + mempool.0.contains(&txid), "Transaction should be in mempool for RBF" ); diff --git a/src/traits.rs b/src/traits.rs index 3fc65ef..b90aeb1 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,7 +1,7 @@ use bitcoin::{bip32::Xpriv, block::Header, Address, Block, BlockHash, Network, Transaction, Txid}; use corepc_types::model::{ - GetBlockchainInfo, GetRawTransaction, GetRawTransactionVerbose, GetTransaction, - ListTransactions, ListUnspent, + GetBlockchainInfo, GetMempoolInfo, GetRawMempool, GetRawMempoolVerbose, GetRawTransaction, + GetRawTransactionVerbose, GetTransaction, ListTransactions, ListUnspent, }; use std::future::Future; @@ -9,10 +9,10 @@ use crate::{ client::ClientResult, types::{ CreateRawTransaction, CreateRawTransactionInput, CreateRawTransactionOutput, - GetAddressInfo, GetMempoolInfo, GetRawMempoolVerbose, GetTxOut, ImportDescriptor, - ImportDescriptorResult, ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, - PsbtBumpFeeOptions, SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, - WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, + GetAddressInfo, GetTxOut, ImportDescriptor, ImportDescriptorResult, + ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, + SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, WalletCreateFundedPsbt, + WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, }, }; @@ -88,7 +88,7 @@ pub trait Reader { fn get_current_timestamp(&self) -> impl Future> + Send; /// Gets all transaction ids in mempool. - fn get_raw_mempool(&self) -> impl Future>> + Send; + fn get_raw_mempool(&self) -> impl Future> + Send; /// Gets verbose representation of transactions in mempool. fn get_raw_mempool_verbose( diff --git a/src/types.rs b/src/types.rs index 01937dc..88c8fcc 100644 --- a/src/types.rs +++ b/src/types.rs @@ -12,58 +12,6 @@ use serde::{ use crate::error::SignRawTransactionWithWalletError; -/// Result of JSON-RPC method `getmempoolinfo`. -/// -/// Method call: `getmempoolinfo` -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct GetMempoolInfo { - pub loaded: bool, - pub size: usize, - pub bytes: usize, - pub usage: usize, - pub maxmempool: usize, - pub mempoolminfee: f64, - pub minrelaytxfee: f64, - pub unbroadcastcount: usize, -} - -/// Response from `getrawmempool` with `verbose=true`. -/// -/// The top-level map key is the txid, and the value contains detailed mempool info per tx. -pub type GetRawMempoolVerbose = BTreeMap; - -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct MempoolEntry { - pub vsize: usize, - pub weight: usize, - pub time: u64, - pub height: usize, - pub descendantcount: usize, - pub descendantsize: usize, - #[serde(default)] - pub ancestorcount: usize, - pub ancestorsize: usize, - pub wtxid: String, - pub fees: Option, - pub depends: Vec, - pub spentby: Vec, - #[serde(rename = "bip125-replaceable")] - pub bip125_replaceable: bool, - pub unbroadcast: bool, -} - -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct MempoolFeeBreakdown { - #[serde(deserialize_with = "deserialize_bitcoin")] - pub base: Amount, - #[serde(deserialize_with = "deserialize_bitcoin")] - pub modified: Amount, - #[serde(deserialize_with = "deserialize_bitcoin")] - pub ancestor: Amount, - #[serde(deserialize_with = "deserialize_bitcoin")] - pub descendant: Amount, -} - /// Result of JSON-RPC method `gettxout`. /// /// > gettxout "txid" n ( include_mempool ) From f0323f55cd60fc7a7e1611ffe2f6c5f66e19a10d Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Thu, 2 Oct 2025 10:16:12 -0300 Subject: [PATCH 09/23] refactor: use corepc-types GetTxOut --- src/client.rs | 37 ++++++++++++++++++++----------------- src/traits.rs | 10 +++++----- src/types.rs | 41 ----------------------------------------- 3 files changed, 25 insertions(+), 63 deletions(-) diff --git a/src/client.rs b/src/client.rs index fd32138..7f7534f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -19,7 +19,7 @@ use corepc_types::model; use corepc_types::v29::{ GetBlockHeaderVerbose, GetBlockVerboseOne, GetBlockVerboseZero, GetBlockchainInfo, GetMempoolInfo, GetRawMempool, GetRawMempoolVerbose, GetRawTransaction, - GetRawTransactionVerbose, GetTransaction, ListTransactions, + GetRawTransactionVerbose, GetTransaction, GetTxOut, ListTransactions, }; use reqwest::{ header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}, @@ -38,11 +38,10 @@ use crate::{ traits::{Broadcaster, Reader, Signer, Wallet}, types::{ CreateRawTransaction, CreateRawTransactionInput, CreateRawTransactionOutput, CreateWallet, - GetAddressInfo, GetNewAddress, GetTxOut, ImportDescriptor, ImportDescriptorResult, - ListDescriptors, ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, - PsbtBumpFeeOptions, SighashType, SignRawTransactionWithWallet, SubmitPackage, - TestMempoolAccept, WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, - WalletProcessPsbtResult, + GetAddressInfo, GetNewAddress, ImportDescriptor, ImportDescriptorResult, ListDescriptors, + ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, + SighashType, SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, + WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, }, }; @@ -436,16 +435,20 @@ impl Reader for Client { txid: &Txid, vout: u32, include_mempool: bool, - ) -> ClientResult { - self.call::( - "gettxout", - &[ - to_value(txid.to_string())?, - to_value(vout)?, - to_value(include_mempool)?, - ], - ) - .await + ) -> ClientResult { + let resp = self + .call::( + "gettxout", + &[ + to_value(txid.to_string())?, + to_value(vout)?, + to_value(include_mempool)?, + ], + ) + .await + .map_err(|e| ClientError::Parse(e.to_string()))?; + resp.into_model() + .map_err(|e| ClientError::Parse(e.to_string())) } async fn network(&self) -> ClientResult { @@ -1065,7 +1068,7 @@ mod test { .get_tx_out(&coinbase_tx.compute_txid(), 0, true) .await .unwrap(); - assert_eq!(got.value, COINBASE_AMOUNT.to_btc()); + assert_eq!(got.tx_out.value, COINBASE_AMOUNT); // gettxout should fail with a spent UTXO. let new_address = bitcoind.client.new_address().unwrap(); diff --git a/src/traits.rs b/src/traits.rs index b90aeb1..edc5901 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,7 +1,7 @@ use bitcoin::{bip32::Xpriv, block::Header, Address, Block, BlockHash, Network, Transaction, Txid}; use corepc_types::model::{ GetBlockchainInfo, GetMempoolInfo, GetRawMempool, GetRawMempoolVerbose, GetRawTransaction, - GetRawTransactionVerbose, GetTransaction, ListTransactions, ListUnspent, + GetRawTransactionVerbose, GetTransaction, GetTxOut, ListTransactions, ListUnspent, }; use std::future::Future; @@ -9,10 +9,10 @@ use crate::{ client::ClientResult, types::{ CreateRawTransaction, CreateRawTransactionInput, CreateRawTransactionOutput, - GetAddressInfo, GetTxOut, ImportDescriptor, ImportDescriptorResult, - ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, - SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, WalletCreateFundedPsbt, - WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, + GetAddressInfo, ImportDescriptor, ImportDescriptorResult, ListUnspentQueryOptions, + PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, SignRawTransactionWithWallet, + SubmitPackage, TestMempoolAccept, WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, + WalletProcessPsbtResult, }, }; diff --git a/src/types.rs b/src/types.rs index 88c8fcc..b63e32a 100644 --- a/src/types.rs +++ b/src/types.rs @@ -12,47 +12,6 @@ use serde::{ use crate::error::SignRawTransactionWithWalletError; -/// Result of JSON-RPC method `gettxout`. -/// -/// > gettxout "txid" n ( include_mempool ) -/// > -/// > Returns details about an unspent transaction output. -/// > -/// > Arguments: -/// > 1. txid (string, required) The transaction id -/// > 2. n (numeric, required) vout number -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct GetTxOut { - /// The hash of the block at the tip of the chain. - #[serde(rename = "bestblock")] - pub best_block: String, - /// The number of confirmations. - pub confirmations: u32, // TODO: Change this to an i64. - /// The transaction value in BTC. - pub value: f64, - /// The script pubkey. - #[serde(rename = "scriptPubkey")] - pub script_pubkey: Option, - /// Coinbase or not. - pub coinbase: bool, -} - -/// A script pubkey. -#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] -pub struct ScriptPubkey { - /// Script assembly. - pub asm: String, - /// Script hex. - pub hex: String, - #[serde(rename = "reqSigs")] - pub req_sigs: i64, - /// The type, eg pubkeyhash. - #[serde(rename = "type")] - pub type_: String, - /// Bitcoin address. - pub address: Option, -} - /// Models the arguments of JSON-RPC method `createrawtransaction`. /// /// # Note From d49e3010af6152e7147454ff9de51a52ead13b47 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Thu, 2 Oct 2025 10:36:49 -0300 Subject: [PATCH 10/23] refactor: rename CreateRawTransaction to CreateRawTransactionArguments --- src/client.rs | 12 ++++++------ src/traits.rs | 4 ++-- src/types.rs | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/client.rs b/src/client.rs index 7f7534f..2e984fd 100644 --- a/src/client.rs +++ b/src/client.rs @@ -37,10 +37,10 @@ use crate::{ error::{BitcoinRpcError, ClientError}, traits::{Broadcaster, Reader, Signer, Wallet}, types::{ - CreateRawTransaction, CreateRawTransactionInput, CreateRawTransactionOutput, CreateWallet, GetAddressInfo, GetNewAddress, ImportDescriptor, ImportDescriptorResult, ListDescriptors, ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, SighashType, SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, + CreateRawTransactionArguments, CreateRawTransactionInput, CreateRawTransactionOutput, WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, }, }; @@ -531,7 +531,7 @@ impl Wallet for Client { async fn create_raw_transaction( &self, - raw_tx: CreateRawTransaction, + raw_tx: CreateRawTransactionArguments, ) -> ClientResult { let raw_tx = self .call::( @@ -1115,7 +1115,7 @@ mod test { let amount_minus_fees = Amount::from_sat(amount.to_sat() - 2_000); let send_back_address = client.get_new_address().await.unwrap(); - let parent_raw_tx = CreateRawTransaction { + let parent_raw_tx = CreateRawTransactionArguments { inputs: vec![CreateRawTransactionInput { txid: coinbase_tx.compute_txid().to_string(), vout: 0, @@ -1147,7 +1147,7 @@ mod test { // sanity check let parent_submitted = client.send_raw_transaction(&signed_parent).await.unwrap(); - let child_raw_tx = CreateRawTransaction { + let child_raw_tx = CreateRawTransactionArguments { inputs: vec![CreateRawTransactionInput { txid: parent_submitted.to_string(), vout: 0, @@ -1202,7 +1202,7 @@ mod test { let last_block = client.get_block(blocks.first().unwrap()).await.unwrap(); let coinbase_tx = last_block.coinbase().unwrap(); - let parent_raw_tx = CreateRawTransaction { + let parent_raw_tx = CreateRawTransactionArguments { inputs: vec![CreateRawTransactionInput { txid: coinbase_tx.compute_txid().to_string(), vout: 0, @@ -1233,7 +1233,7 @@ mod test { // 5k sats as fees. let amount_minus_fees = Amount::from_sat(COINBASE_AMOUNT.to_sat() - 43_000); - let child_raw_tx = CreateRawTransaction { + let child_raw_tx = CreateRawTransactionArguments { inputs: vec![CreateRawTransactionInput { txid: signed_parent.compute_txid().to_string(), vout: 0, diff --git a/src/traits.rs b/src/traits.rs index edc5901..4a4aa74 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -8,7 +8,7 @@ use std::future::Future; use crate::{ client::ClientResult, types::{ - CreateRawTransaction, CreateRawTransactionInput, CreateRawTransactionOutput, + CreateRawTransactionArguments, CreateRawTransactionInput, CreateRawTransactionOutput, GetAddressInfo, ImportDescriptor, ImportDescriptorResult, ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, @@ -213,7 +213,7 @@ pub trait Wallet { /// Creates a raw transaction. fn create_raw_transaction( &self, - raw_tx: CreateRawTransaction, + raw_tx: CreateRawTransactionArguments, ) -> impl Future> + Send; /// Creates and funds a PSBT with inputs and outputs from the wallet. diff --git a/src/types.rs b/src/types.rs index b63e32a..6d55139 100644 --- a/src/types.rs +++ b/src/types.rs @@ -18,7 +18,7 @@ use crate::error::SignRawTransactionWithWalletError; /// /// Assumes that the transaction is always "replaceable" by default and has a locktime of 0. #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct CreateRawTransaction { +pub struct CreateRawTransactionArguments { pub inputs: Vec, pub outputs: Vec, } From 4a9a89690eb0b7cc071a62bb3d695bef5e65839d Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Thu, 2 Oct 2025 10:37:03 -0300 Subject: [PATCH 11/23] refactor: use corepc-types SubmitPackage --- src/client.rs | 58 ++++++++++++++++++++++++++++++++++----- src/traits.rs | 3 ++- src/types.rs | 75 --------------------------------------------------- 3 files changed, 53 insertions(+), 83 deletions(-) diff --git a/src/client.rs b/src/client.rs index 2e984fd..375f629 100644 --- a/src/client.rs +++ b/src/client.rs @@ -19,7 +19,7 @@ use corepc_types::model; use corepc_types::v29::{ GetBlockHeaderVerbose, GetBlockVerboseOne, GetBlockVerboseZero, GetBlockchainInfo, GetMempoolInfo, GetRawMempool, GetRawMempoolVerbose, GetRawTransaction, - GetRawTransactionVerbose, GetTransaction, GetTxOut, ListTransactions, + GetRawTransactionVerbose, GetTransaction, GetTxOut, ListTransactions, SubmitPackage, }; use reqwest::{ header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}, @@ -37,10 +37,10 @@ use crate::{ error::{BitcoinRpcError, ClientError}, traits::{Broadcaster, Reader, Signer, Wallet}, types::{ - GetAddressInfo, GetNewAddress, ImportDescriptor, ImportDescriptorResult, ListDescriptors, - ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, - SighashType, SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, CreateRawTransactionArguments, CreateRawTransactionInput, CreateRawTransactionOutput, + CreateWallet, GetAddressInfo, GetNewAddress, ImportDescriptor, ImportDescriptorResult, + ListDescriptors, ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, + PsbtBumpFeeOptions, SighashType, SignRawTransactionWithWallet, TestMempoolAccept, WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, }, }; @@ -488,10 +488,54 @@ impl Broadcaster for Client { .await } - async fn submit_package(&self, txs: &[Transaction]) -> ClientResult { + async fn submit_package(&self, txs: &[Transaction]) -> ClientResult { let txstrs: Vec = txs.iter().map(serialize_hex).collect(); - self.call::("submitpackage", &[to_value(txstrs)?]) - .await + let resp = self + .call::("submitpackage", &[to_value(txstrs)?]) + .await?; + trace!(?resp, "Got submit package response"); + + let mut package_map: serde_json::Map = + serde_json::from_value(resp).map_err(|e| ClientError::Parse(e.to_string()))?; + + // FIXME(corepc-types): Add missing effective-includes field to tx-results + // bitcoind only returns effective-includes for some transactions (child txs with fee bumping) + // but the model expects it to be present in all transactions + if let Some(tx_results) = package_map.get("tx-results").cloned() { + if let Some(mut tx_results_map) = tx_results.as_object().cloned() { + for (_wtxid, tx_result) in &mut tx_results_map { + if let Some(tx_result_map) = tx_result.as_object_mut() { + if let Some(fees) = tx_result_map.get("fees").cloned() { + if let Some(mut fees_map) = fees.as_object().cloned() { + // Add empty effective-includes if not present + if !fees_map.contains_key("effective-includes") { + fees_map.insert( + "effective-includes".to_string(), + serde_json::Value::Array(vec![]), + ); + } + tx_result_map.insert( + "fees".to_string(), + serde_json::Value::Object(fees_map), + ); + } + } + } + } + package_map.insert( + "tx-results".to_string(), + serde_json::Value::Object(tx_results_map), + ); + } + } + + let submit_package: SubmitPackage = + serde_json::from_value(serde_json::Value::Object(package_map)) + .map_err(|e| ClientError::Parse(e.to_string()))?; + + submit_package + .into_model() + .map_err(|e| ClientError::Parse(e.to_string())) } } diff --git a/src/traits.rs b/src/traits.rs index 4a4aa74..5f65a9b 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -2,6 +2,7 @@ use bitcoin::{bip32::Xpriv, block::Header, Address, Block, BlockHash, Network, T use corepc_types::model::{ GetBlockchainInfo, GetMempoolInfo, GetRawMempool, GetRawMempoolVerbose, GetRawTransaction, GetRawTransactionVerbose, GetTransaction, GetTxOut, ListTransactions, ListUnspent, + SubmitPackage, }; use std::future::Future; @@ -11,7 +12,7 @@ use crate::{ CreateRawTransactionArguments, CreateRawTransactionInput, CreateRawTransactionOutput, GetAddressInfo, ImportDescriptor, ImportDescriptorResult, ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, SignRawTransactionWithWallet, - SubmitPackage, TestMempoolAccept, WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, + TestMempoolAccept, WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, }, }; diff --git a/src/types.rs b/src/types.rs index 6d55139..fc9c387 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,5 +1,3 @@ -use std::collections::BTreeMap; - use bitcoin::{ address::{self, NetworkUnchecked}, consensus::{self}, @@ -75,79 +73,6 @@ impl Serialize for CreateRawTransactionOutput { } } -/// Result of JSON-RPC method `submitpackage`. -/// -/// > submitpackage ["rawtx",...] ( maxfeerate maxburnamount ) -/// > -/// > Submit a package of raw transactions (serialized, hex-encoded) to local node. -/// > The package will be validated according to consensus and mempool policy rules. If any -/// > transaction passes, it will be accepted to mempool. -/// > This RPC is experimental and the interface may be unstable. Refer to doc/policy/packages.md -/// > for documentation on package policies. -/// > Warning: successful submission does not mean the transactions will propagate throughout the -/// > network. -/// > -/// > Arguments: -/// > 1. package (json array, required) An array of raw transactions. -/// > The package must solely consist of a child and its parents. None of the parents may depend on -/// > each other. -/// > The package must be topologically sorted, with the child being the last element in the array. -/// > [ -/// > "rawtx", (string) -/// > ... -/// > ] -#[allow(clippy::doc_lazy_continuation)] -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct SubmitPackage { - /// The transaction package result message. - /// - /// "success" indicates all transactions were accepted into or are already in the mempool. - pub package_msg: String, - /// Transaction results keyed by wtxid. - #[serde(rename = "tx-results")] - pub tx_results: BTreeMap, - /// List of txids of replaced transactions. - #[serde(rename = "replaced-transactions")] - pub replaced_transactions: Vec, -} - -/// Models the per-transaction result included in the JSON-RPC method `submitpackage`. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct SubmitPackageTxResult { - /// The transaction id. - pub txid: String, - /// The wtxid of a different transaction with the same txid but different witness found in the - /// mempool. - /// - /// If set, this means the submitted transaction was ignored. - #[serde(rename = "other-wtxid")] - pub other_wtxid: Option, - /// Sigops-adjusted virtual transaction size. - pub vsize: i64, - /// Transaction fees. - pub fees: Option, - /// The transaction error string, if it was rejected by the mempool - pub error: Option, -} - -/// Models the fees included in the per-transaction result of the JSON-RPC method `submitpackage`. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct SubmitPackageTxResultFees { - /// Transaction fee. - #[serde(rename = "base")] - pub base_fee: f64, - /// The effective feerate. - /// - /// Will be `None` if the transaction was already in the mempool. For example, the package - /// feerate and/or feerate with modified fees from the `prioritisetransaction` JSON-RPC method. - #[serde(rename = "effective-feerate")] - pub effective_fee_rate: Option, - /// If [`Self::effective_fee_rate`] is provided, this holds the wtxid's of the transactions - /// whose fees and vsizes are included in effective-feerate. - #[serde(rename = "effective-includes")] - pub effective_includes: Option>, -} - /// Result of the JSON-RPC method `getnewaddress`. /// /// # Note From f9383b779b7dc56466f27d79131f6d64ab7930a4 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Thu, 2 Oct 2025 10:42:45 -0300 Subject: [PATCH 12/23] refactor: use corepc-types GetAddressInfo --- src/client.rs | 27 +++++++++------- src/traits.rs | 8 ++--- src/types.rs | 90 +-------------------------------------------------- 3 files changed, 21 insertions(+), 104 deletions(-) diff --git a/src/client.rs b/src/client.rs index 375f629..de93e43 100644 --- a/src/client.rs +++ b/src/client.rs @@ -17,9 +17,10 @@ use bitcoin::{ }; use corepc_types::model; use corepc_types::v29::{ - GetBlockHeaderVerbose, GetBlockVerboseOne, GetBlockVerboseZero, GetBlockchainInfo, - GetMempoolInfo, GetRawMempool, GetRawMempoolVerbose, GetRawTransaction, - GetRawTransactionVerbose, GetTransaction, GetTxOut, ListTransactions, SubmitPackage, + GetAddressInfo, GetBlockHeaderVerbose, GetBlockVerboseOne, GetBlockVerboseZero, + GetBlockchainInfo, GetMempoolInfo, GetNewAddress, GetRawMempool, GetRawMempoolVerbose, + GetRawTransaction, GetRawTransactionVerbose, GetTransaction, GetTxOut, ListTransactions, + SubmitPackage, }; use reqwest::{ header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}, @@ -38,10 +39,10 @@ use crate::{ traits::{Broadcaster, Reader, Signer, Wallet}, types::{ CreateRawTransactionArguments, CreateRawTransactionInput, CreateRawTransactionOutput, - CreateWallet, GetAddressInfo, GetNewAddress, ImportDescriptor, ImportDescriptorResult, - ListDescriptors, ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, - PsbtBumpFeeOptions, SighashType, SignRawTransactionWithWallet, TestMempoolAccept, - WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, + CreateWallet, ImportDescriptor, ImportDescriptorResult, ListDescriptors, + ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, + SighashType, SignRawTransactionWithWallet, TestMempoolAccept, WalletCreateFundedPsbt, + WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, }, }; @@ -609,10 +610,14 @@ impl Wallet for Client { .await } - async fn get_address_info(&self, address: &Address) -> ClientResult { + async fn get_address_info(&self, address: &Address) -> ClientResult { trace!(address = %address, "Getting address info"); - self.call::("getaddressinfo", &[to_value(address.to_string())?]) - .await + let resp = self + .call::("getaddressinfo", &[to_value(address.to_string())?]) + .await?; + Ok(resp + .into_model() + .map_err(|e| ClientError::Parse(e.to_string()))?) } async fn list_unspent( @@ -1026,7 +1031,7 @@ mod test { let info_address = client.get_new_address().await.unwrap(); let address_info = client.get_address_info(&info_address).await.unwrap(); assert_eq!(address_info.address, info_address.as_unchecked().clone()); - assert!(address_info.is_mine.unwrap_or(false)); + assert!(address_info.is_mine); assert!(address_info.solvable.unwrap_or(false)); let unspent_address = client.get_new_address().await.unwrap(); diff --git a/src/traits.rs b/src/traits.rs index 5f65a9b..99d5d9f 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,8 +1,8 @@ use bitcoin::{bip32::Xpriv, block::Header, Address, Block, BlockHash, Network, Transaction, Txid}; use corepc_types::model::{ - GetBlockchainInfo, GetMempoolInfo, GetRawMempool, GetRawMempoolVerbose, GetRawTransaction, - GetRawTransactionVerbose, GetTransaction, GetTxOut, ListTransactions, ListUnspent, - SubmitPackage, + GetAddressInfo, GetBlockchainInfo, GetMempoolInfo, GetRawMempool, GetRawMempoolVerbose, + GetRawTransaction, GetRawTransactionVerbose, GetTransaction, GetTxOut, ListTransactions, + ListUnspent, SubmitPackage, }; use std::future::Future; @@ -10,7 +10,7 @@ use crate::{ client::ClientResult, types::{ CreateRawTransactionArguments, CreateRawTransactionInput, CreateRawTransactionOutput, - GetAddressInfo, ImportDescriptor, ImportDescriptorResult, ListUnspentQueryOptions, + ImportDescriptor, ImportDescriptorResult, ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, SignRawTransactionWithWallet, TestMempoolAccept, WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, diff --git a/src/types.rs b/src/types.rs index fc9c387..9046d5d 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,7 +1,6 @@ use bitcoin::{ - address::{self, NetworkUnchecked}, consensus::{self}, - Address, Amount, FeeRate, Psbt, Transaction, Txid, + Amount, FeeRate, Psbt, Transaction, Txid, }; use serde::{ de::{self, Visitor}, @@ -73,23 +72,6 @@ impl Serialize for CreateRawTransactionOutput { } } -/// Result of the JSON-RPC method `getnewaddress`. -/// -/// # Note -/// -/// This assumes that the UTXOs are present in the underlying Bitcoin -/// client's wallet. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct GetNewAddress(pub String); - -impl GetNewAddress { - /// Converts json straight to a [`Address`]. - pub fn address(self) -> Result, address::ParseError> { - let address = self.0.parse::>()?; - Ok(address) - } -} - /// Models the result of JSON-RPC method `testmempoolaccept`. #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct TestMempoolAccept { @@ -367,35 +349,6 @@ where } } -/// Deserializes the address string into proper [`Address`]s. -/// -/// # Note -/// -/// The user is responsible for ensuring that the address is valid, -/// since this functions returns an [`Address`]. -fn deserialize_address<'d, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'d>, -{ - struct AddressVisitor; - impl Visitor<'_> for AddressVisitor { - type Value = Address; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(formatter, "a Bitcoin address string expected") - } - - fn visit_str(self, v: &str) -> Result - where - E: de::Error, - { - v.parse::>() - .map_err(|e| E::custom(format!("failed to deserialize address: {e}"))) - } - } - deserializer.deserialize_any(AddressVisitor) -} - /// Signature hash types for Bitcoin transactions. /// /// These types specify which parts of a transaction are included in the signature @@ -580,47 +533,6 @@ pub struct WalletProcessPsbtResult { pub hex: Option, } -/// Result of the `getaddressinfo` RPC method. -/// -/// Provides detailed information about a Bitcoin address, including ownership -/// status, watching capabilities, and spending permissions within the wallet. -/// -/// # Note -/// -/// Optional fields may be `None` if the wallet doesn't have specific information -/// about the address or if the address is not related to the wallet. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct GetAddressInfo { - /// The Bitcoin address that was queried. - /// - /// Returns the same address that was provided as input to `getaddressinfo`, - /// validated and parsed into the proper Address type. - #[serde(deserialize_with = "deserialize_address")] - pub address: Address, - - /// Whether the address belongs to the wallet (can receive payments to it). - /// - /// `true` if the wallet owns the private key or can generate signatures for this address. - /// `false` if the address is not owned by the wallet. `None` if ownership status is unknown. - #[serde(rename = "ismine")] - pub is_mine: Option, - - /// Whether the address is watch-only (monitored but not spendable). - /// - /// `true` if the wallet watches this address for incoming transactions but cannot - /// spend from it (no private key). `false` if the address is fully controlled. - /// `None` if watch status is not applicable. - #[serde(rename = "iswatchonly")] - pub is_watchonly: Option, - - /// Whether the wallet knows how to spend coins sent to this address. - /// - /// `true` if the wallet has enough information (private keys, scripts) to create - /// valid spending transactions from this address. `false` if the address cannot - /// be spent by this wallet. `None` if spendability cannot be determined. - pub solvable: Option, -} - /// Query options for filtering unspent transaction outputs. /// /// Used with `list_unspent` to apply additional filtering criteria From 80394e718011085447f69d4d7245ff2d071f5a8b Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Thu, 2 Oct 2025 10:45:48 -0300 Subject: [PATCH 13/23] refactor: use corepc-types TestMempoolAccept --- src/client.rs | 25 +++++++++++++++++++------ src/traits.rs | 7 +++---- src/types.rs | 10 ---------- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/client.rs b/src/client.rs index de93e43..0fa90f2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -20,7 +20,7 @@ use corepc_types::v29::{ GetAddressInfo, GetBlockHeaderVerbose, GetBlockVerboseOne, GetBlockVerboseZero, GetBlockchainInfo, GetMempoolInfo, GetNewAddress, GetRawMempool, GetRawMempoolVerbose, GetRawTransaction, GetRawTransactionVerbose, GetTransaction, GetTxOut, ListTransactions, - SubmitPackage, + SubmitPackage, TestMempoolAccept, }; use reqwest::{ header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}, @@ -41,7 +41,7 @@ use crate::{ CreateRawTransactionArguments, CreateRawTransactionInput, CreateRawTransactionOutput, CreateWallet, ImportDescriptor, ImportDescriptorResult, ListDescriptors, ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, - SighashType, SignRawTransactionWithWallet, TestMempoolAccept, WalletCreateFundedPsbt, + SighashType, SignRawTransactionWithWallet, WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, }, }; @@ -482,11 +482,18 @@ impl Broadcaster for Client { } } - async fn test_mempool_accept(&self, tx: &Transaction) -> ClientResult> { + async fn test_mempool_accept( + &self, + tx: &Transaction, + ) -> ClientResult { let txstr = serialize_hex(tx); trace!(%txstr, "Testing mempool accept"); - self.call::>("testmempoolaccept", &[to_value([txstr])?]) - .await + let resp = self + .call::("testmempoolaccept", &[to_value([txstr])?]) + .await?; + Ok(resp + .into_model() + .map_err(|e| ClientError::Parse(e.to_string()))?) } async fn submit_package(&self, txs: &[Transaction]) -> ClientResult { @@ -946,7 +953,10 @@ mod test { .test_mempool_accept(&tx) .await .expect("must be able to test mempool accept"); - let got = txids.first().expect("there must be at least one txid"); + let got = txids + .results + .first() + .expect("there must be at least one txid"); assert_eq!( got.txid, tx.compute_txid(), @@ -1023,6 +1033,7 @@ mod test { .test_mempool_accept(signed_tx) .await .unwrap() + .results .first() .unwrap() .txid; @@ -1402,6 +1413,7 @@ mod test { .test_mempool_accept(&signed_tx) .await .unwrap() + .results .first() .unwrap() .txid; @@ -1428,6 +1440,7 @@ mod test { .test_mempool_accept(&signed_tx) .await .unwrap() + .results .first() .unwrap() .txid; diff --git a/src/traits.rs b/src/traits.rs index 99d5d9f..01c9831 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -2,7 +2,7 @@ use bitcoin::{bip32::Xpriv, block::Header, Address, Block, BlockHash, Network, T use corepc_types::model::{ GetAddressInfo, GetBlockchainInfo, GetMempoolInfo, GetRawMempool, GetRawMempoolVerbose, GetRawTransaction, GetRawTransactionVerbose, GetTransaction, GetTxOut, ListTransactions, - ListUnspent, SubmitPackage, + ListUnspent, SubmitPackage, TestMempoolAccept, }; use std::future::Future; @@ -12,8 +12,7 @@ use crate::{ CreateRawTransactionArguments, CreateRawTransactionInput, CreateRawTransactionOutput, ImportDescriptor, ImportDescriptorResult, ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, SignRawTransactionWithWallet, - TestMempoolAccept, WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, - WalletProcessPsbtResult, + WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, }, }; @@ -150,7 +149,7 @@ pub trait Broadcaster { fn test_mempool_accept( &self, tx: &Transaction, - ) -> impl Future>> + Send; + ) -> impl Future> + Send; /// Submit a package of raw transactions (serialized, hex-encoded) to local node. /// diff --git a/src/types.rs b/src/types.rs index 9046d5d..de06b73 100644 --- a/src/types.rs +++ b/src/types.rs @@ -72,16 +72,6 @@ impl Serialize for CreateRawTransactionOutput { } } -/// Models the result of JSON-RPC method `testmempoolaccept`. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct TestMempoolAccept { - /// The transaction id. - #[serde(deserialize_with = "deserialize_txid")] - pub txid: Txid, - /// Rejection reason, if any. - pub reject_reason: Option, -} - /// Models the result of JSON-RPC method `signrawtransactionwithwallet`. /// /// # Note From 9145d9e7d36c573cb26c3365f77610e0dc59a1f9 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Thu, 2 Oct 2025 10:50:42 -0300 Subject: [PATCH 14/23] refactor: use corepc-types SignRawTransaction --- src/client.rs | 86 +++++++++++++++++++++------------------------------ src/traits.rs | 6 ++-- src/types.rs | 18 ----------- 3 files changed, 38 insertions(+), 72 deletions(-) diff --git a/src/client.rs b/src/client.rs index 0fa90f2..1b3bea1 100644 --- a/src/client.rs +++ b/src/client.rs @@ -20,7 +20,7 @@ use corepc_types::v29::{ GetAddressInfo, GetBlockHeaderVerbose, GetBlockVerboseOne, GetBlockVerboseZero, GetBlockchainInfo, GetMempoolInfo, GetNewAddress, GetRawMempool, GetRawMempoolVerbose, GetRawTransaction, GetRawTransactionVerbose, GetTransaction, GetTxOut, ListTransactions, - SubmitPackage, TestMempoolAccept, + SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, }; use reqwest::{ header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}, @@ -41,8 +41,8 @@ use crate::{ CreateRawTransactionArguments, CreateRawTransactionInput, CreateRawTransactionOutput, CreateWallet, ImportDescriptor, ImportDescriptorResult, ListDescriptors, ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, - SighashType, SignRawTransactionWithWallet, WalletCreateFundedPsbt, - WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, + SighashType, WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, + WalletProcessPsbtResult, }, }; @@ -693,15 +693,19 @@ impl Signer for Client { &self, tx: &Transaction, prev_outputs: Option>, - ) -> ClientResult { + ) -> ClientResult { let tx_hex = serialize_hex(tx); trace!(tx_hex = %tx_hex, "Signing transaction"); trace!(?prev_outputs, "Signing transaction with previous outputs"); - self.call::( - "signrawtransactionwithwallet", - &[to_value(tx_hex)?, to_value(prev_outputs)?], - ) - .await + let resp = self + .call::( + "signrawtransactionwithwallet", + &[to_value(tx_hex)?, to_value(prev_outputs)?], + ) + .await?; + Ok(resp + .into_model() + .map_err(|e| ClientError::Parse(e.to_string()))?) } async fn get_xpriv(&self) -> ClientResult> { @@ -805,11 +809,7 @@ mod test { use std::sync::Once; - use bitcoin::{ - consensus::{self}, - hashes::Hash, - transaction, Amount, FeeRate, NetworkKind, - }; + use bitcoin::{hashes::Hash, transaction, Amount, FeeRate, NetworkKind}; use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; use super::*; @@ -946,7 +946,7 @@ mod test { .await .unwrap(); assert!(got.complete); - assert!(consensus::encode::deserialize_hex::(&got.hex).is_ok()); + assert!(got.errors.is_empty()); // test_mempool_accept let txids = client @@ -1194,15 +1194,11 @@ mod test { ], }; let parent = client.create_raw_transaction(parent_raw_tx).await.unwrap(); - let signed_parent: Transaction = consensus::encode::deserialize_hex( - client - .sign_raw_transaction_with_wallet(&parent, None) - .await - .unwrap() - .hex - .as_str(), - ) - .unwrap(); + let signed_parent = client + .sign_raw_transaction_with_wallet(&parent, None) + .await + .unwrap() + .tx; // sanity check let parent_submitted = client.send_raw_transaction(&signed_parent).await.unwrap(); @@ -1221,15 +1217,11 @@ mod test { ], }; let child = client.create_raw_transaction(child_raw_tx).await.unwrap(); - let signed_child: Transaction = consensus::encode::deserialize_hex( - client - .sign_raw_transaction_with_wallet(&child, None) - .await - .unwrap() - .hex - .as_str(), - ) - .unwrap(); + let signed_child = client + .sign_raw_transaction_with_wallet(&child, None) + .await + .unwrap() + .tx; // Ok now we have a parent and a child transaction. let result = client @@ -1276,15 +1268,11 @@ mod test { parent.version = transaction::Version(3); assert_eq!(parent.version, transaction::Version(3)); trace!(?parent, "parent:"); - let signed_parent: Transaction = consensus::encode::deserialize_hex( - client - .sign_raw_transaction_with_wallet(&parent, None) - .await - .unwrap() - .hex - .as_str(), - ) - .unwrap(); + let signed_parent = client + .sign_raw_transaction_with_wallet(&parent, None) + .await + .unwrap() + .tx; assert_eq!(signed_parent.version, transaction::Version(3)); // Assert that the parent tx cannot be broadcasted. @@ -1315,15 +1303,11 @@ mod test { witness_script: None, amount: Some(COINBASE_AMOUNT.to_btc()), }]; - let signed_child: Transaction = consensus::encode::deserialize_hex( - client - .sign_raw_transaction_with_wallet(&child, Some(prev_outputs)) - .await - .unwrap() - .hex - .as_str(), - ) - .unwrap(); + let signed_child = client + .sign_raw_transaction_with_wallet(&child, Some(prev_outputs)) + .await + .unwrap() + .tx; assert_eq!(signed_child.version, transaction::Version(3)); // Assert that the child tx cannot be broadcasted. diff --git a/src/traits.rs b/src/traits.rs index 01c9831..e157c13 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -2,7 +2,7 @@ use bitcoin::{bip32::Xpriv, block::Header, Address, Block, BlockHash, Network, T use corepc_types::model::{ GetAddressInfo, GetBlockchainInfo, GetMempoolInfo, GetRawMempool, GetRawMempoolVerbose, GetRawTransaction, GetRawTransactionVerbose, GetTransaction, GetTxOut, ListTransactions, - ListUnspent, SubmitPackage, TestMempoolAccept, + ListUnspent, SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, }; use std::future::Future; @@ -11,8 +11,8 @@ use crate::{ types::{ CreateRawTransactionArguments, CreateRawTransactionInput, CreateRawTransactionOutput, ImportDescriptor, ImportDescriptorResult, ListUnspentQueryOptions, - PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, SignRawTransactionWithWallet, - WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, + PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, WalletCreateFundedPsbt, + WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, }, }; diff --git a/src/types.rs b/src/types.rs index de06b73..495684e 100644 --- a/src/types.rs +++ b/src/types.rs @@ -7,8 +7,6 @@ use serde::{ Deserialize, Deserializer, Serialize, Serializer, }; -use crate::error::SignRawTransactionWithWalletError; - /// Models the arguments of JSON-RPC method `createrawtransaction`. /// /// # Note @@ -72,22 +70,6 @@ impl Serialize for CreateRawTransactionOutput { } } -/// Models the result of JSON-RPC method `signrawtransactionwithwallet`. -/// -/// # Note -/// -/// This assumes that the transactions are present in the underlying Bitcoin -/// client's wallet. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct SignRawTransactionWithWallet { - /// The Transaction ID. - pub hex: String, - /// If the transaction has a complete set of signatures. - pub complete: bool, - /// Errors, if any. - pub errors: Option>, -} - /// Models the optional previous transaction outputs argument for the method /// `signrawtransactionwithwallet`. /// From 334b7a2af6391df453ea66882413cc6555a588d1 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Thu, 2 Oct 2025 11:04:21 -0300 Subject: [PATCH 15/23] refactor: use corepc-types ListDescriptor --- src/client.rs | 36 ++++++++++++++++++++++-------------- src/traits.rs | 11 ++++++----- src/types.rs | 29 +---------------------------- 3 files changed, 29 insertions(+), 47 deletions(-) diff --git a/src/client.rs b/src/client.rs index 1b3bea1..79ed0c1 100644 --- a/src/client.rs +++ b/src/client.rs @@ -19,8 +19,9 @@ use corepc_types::model; use corepc_types::v29::{ GetAddressInfo, GetBlockHeaderVerbose, GetBlockVerboseOne, GetBlockVerboseZero, GetBlockchainInfo, GetMempoolInfo, GetNewAddress, GetRawMempool, GetRawMempoolVerbose, - GetRawTransaction, GetRawTransactionVerbose, GetTransaction, GetTxOut, ListTransactions, - SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, + GetRawTransaction, GetRawTransactionVerbose, GetTransaction, GetTxOut, ImportDescriptors, + ListDescriptors, ListTransactions, SignRawTransactionWithWallet, SubmitPackage, + TestMempoolAccept, }; use reqwest::{ header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}, @@ -39,10 +40,9 @@ use crate::{ traits::{Broadcaster, Reader, Signer, Wallet}, types::{ CreateRawTransactionArguments, CreateRawTransactionInput, CreateRawTransactionOutput, - CreateWallet, ImportDescriptor, ImportDescriptorResult, ListDescriptors, - ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, - SighashType, WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, - WalletProcessPsbtResult, + CreateWallet, ImportDescriptorInput, ListUnspentQueryOptions, PreviousTransactionOutput, + PsbtBumpFee, PsbtBumpFeeOptions, SighashType, WalletCreateFundedPsbt, + WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, }, }; @@ -725,8 +725,8 @@ impl Signer for Client { // We are only interested in the one that contains `tr(` let descriptor = descriptors .iter() - .find(|d| d.desc.contains("tr(")) - .map(|d| d.desc.clone()) + .find(|d| d.descriptor.contains("tr(")) + .map(|d| d.descriptor.clone()) .ok_or(ClientError::Xpriv)?; // Now we extract the xpriv from the `tr()` up to the first `/` @@ -744,9 +744,9 @@ impl Signer for Client { async fn import_descriptors( &self, - descriptors: Vec, + descriptors: Vec, wallet_name: String, - ) -> ClientResult> { + ) -> ClientResult { let wallet_args = CreateWallet { wallet_name, load_on_startup: Some(true), @@ -763,7 +763,7 @@ impl Signer for Client { .await; let result = self - .call::>("importdescriptors", &[to_value(descriptors)?]) + .call::("importdescriptors", &[to_value(descriptors)?]) .await?; Ok(result) } @@ -810,6 +810,7 @@ mod test { use std::sync::Once; use bitcoin::{hashes::Hash, transaction, Amount, FeeRate, NetworkKind}; + use corepc_types::v29::ImportDescriptorsResult; use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; use super::*; @@ -989,7 +990,7 @@ mod test { // taken from https://github.com/rust-bitcoin/rust-bitcoin/blob/bb38aeb786f408247d5bbc88b9fa13616c74c009/bitcoin/examples/taproot-psbt.rs#L18C38-L18C149 let descriptor_string = "tr([e61b318f/20000'/20']tprv8ZgxMBicQKsPd4arFr7sKjSnKFDVMR2JHw9Y8L9nXN4kiok4u28LpHijEudH3mMYoL4pM5UL9Bgdz2M4Cy8EzfErmU9m86ZTw6hCzvFeTg7/101/*)#2plamwqs".to_owned(); let timestamp = "now".to_owned(); - let list_descriptors = vec![ImportDescriptor { + let list_descriptors = vec![ImportDescriptorInput { desc: descriptor_string, active: Some(true), timestamp, @@ -997,8 +998,15 @@ mod test { let got = client .import_descriptors(list_descriptors, "strata".to_owned()) .await - .unwrap(); - let expected = vec![ImportDescriptorResult { success: true }]; + .unwrap() + .0; + let expected = vec![ImportDescriptorsResult { + success: true, + warnings: Some(vec![ + "Range not given, using default keypool range".to_string() + ]), + error: None, + }]; assert_eq!(expected, got); let psbt_address = client.get_new_address().await.unwrap(); diff --git a/src/traits.rs b/src/traits.rs index e157c13..63bf4dc 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -4,15 +4,16 @@ use corepc_types::model::{ GetRawTransaction, GetRawTransactionVerbose, GetTransaction, GetTxOut, ListTransactions, ListUnspent, SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, }; +use corepc_types::v29::ImportDescriptors; use std::future::Future; +use crate::types::ImportDescriptorInput; use crate::{ client::ClientResult, types::{ CreateRawTransactionArguments, CreateRawTransactionInput, CreateRawTransactionOutput, - ImportDescriptor, ImportDescriptorResult, ListUnspentQueryOptions, - PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, WalletCreateFundedPsbt, - WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, + ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, + WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, }, }; @@ -346,9 +347,9 @@ pub trait Signer { /// Imports the descriptors into the wallet. fn import_descriptors( &self, - descriptors: Vec, + descriptors: Vec, wallet_name: String, - ) -> impl Future>> + Send; + ) -> impl Future> + Send; /// Updates a PSBT with input information from the wallet and optionally signs it. /// diff --git a/src/types.rs b/src/types.rs index 495684e..df2751c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -110,30 +110,9 @@ pub struct PreviousTransactionOutput { pub amount: Option, } -/// Models the result of the JSON-RPC method `listdescriptors`. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct ListDescriptors { - /// The descriptors - pub descriptors: Vec, -} - -/// Models the Descriptor in the result of the JSON-RPC method `listdescriptors`. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct ListDescriptor { - /// The descriptor. - pub desc: String, -} - -/// Models the result of the JSON-RPC method `importdescriptors`. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct ImportDescriptors { - /// The descriptors - pub descriptors: Vec, -} - /// Models the Descriptor in the result of the JSON-RPC method `importdescriptors`. #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct ImportDescriptor { +pub struct ImportDescriptorInput { /// The descriptor. pub desc: String, /// Set this descriptor to be the active descriptor @@ -143,12 +122,6 @@ pub struct ImportDescriptor { /// in UNIX epoch time. Can also be a string "now" pub timestamp: String, } -/// Models the Descriptor in the result of the JSON-RPC method `importdescriptors`. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct ImportDescriptorResult { - /// Result. - pub success: bool, -} /// Models the `createwallet` JSON-RPC method. /// From 3d075a45025badacf59f4b3f074659177cf11f84 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Thu, 2 Oct 2025 11:07:17 -0300 Subject: [PATCH 16/23] refactor: use corepc-types CreateWallet --- src/client.rs | 16 ++++++++-------- src/types.rs | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/client.rs b/src/client.rs index 79ed0c1..229763d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -15,7 +15,6 @@ use bitcoin::{ consensus::{self, encode::serialize_hex}, Address, Block, BlockHash, Network, Transaction, Txid, }; -use corepc_types::model; use corepc_types::v29::{ GetAddressInfo, GetBlockHeaderVerbose, GetBlockVerboseOne, GetBlockVerboseZero, GetBlockchainInfo, GetMempoolInfo, GetNewAddress, GetRawMempool, GetRawMempoolVerbose, @@ -23,6 +22,7 @@ use corepc_types::v29::{ ListDescriptors, ListTransactions, SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, }; +use corepc_types::{model, v29::CreateWallet}; use reqwest::{ header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}, Client as ReqwestClient, @@ -40,9 +40,9 @@ use crate::{ traits::{Broadcaster, Reader, Signer, Wallet}, types::{ CreateRawTransactionArguments, CreateRawTransactionInput, CreateRawTransactionOutput, - CreateWallet, ImportDescriptorInput, ListUnspentQueryOptions, PreviousTransactionOutput, - PsbtBumpFee, PsbtBumpFeeOptions, SighashType, WalletCreateFundedPsbt, - WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, + CreateWalletArguments, ImportDescriptorInput, ListUnspentQueryOptions, + PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, SighashType, + WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, }, }; @@ -747,19 +747,19 @@ impl Signer for Client { descriptors: Vec, wallet_name: String, ) -> ClientResult { - let wallet_args = CreateWallet { - wallet_name, + let wallet_args = CreateWalletArguments { + name: wallet_name, load_on_startup: Some(true), }; // TODO: this should check for -35 error code which is good, // means that is already created let _wallet_create = self - .call::("createwallet", &[to_value(wallet_args.clone())?]) + .call::("createwallet", &[to_value(wallet_args.clone())?]) .await; // TODO: this should check for -35 error code which is good, -18 is bad. let _wallet_load = self - .call::("loadwallet", &[to_value(wallet_args)?]) + .call::("loadwallet", &[to_value(wallet_args)?]) .await; let result = self diff --git a/src/types.rs b/src/types.rs index df2751c..9218967 100644 --- a/src/types.rs +++ b/src/types.rs @@ -129,9 +129,9 @@ pub struct ImportDescriptorInput { /// /// This can also be used for the `loadwallet` JSON-RPC method. #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct CreateWallet { +pub struct CreateWalletArguments { /// Wallet name - pub wallet_name: String, + pub name: String, /// Load on startup pub load_on_startup: Option, } From 7fb98f08d587666a2a3d3e866652f4a63e8064b8 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Thu, 2 Oct 2025 11:16:22 -0300 Subject: [PATCH 17/23] refactor: use corepc-types PSBT stuff --- src/client.rs | 54 +++++---- src/traits.rs | 13 ++- src/types.rs | 296 +------------------------------------------------- 3 files changed, 41 insertions(+), 322 deletions(-) diff --git a/src/client.rs b/src/client.rs index 229763d..c1555d2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -19,8 +19,8 @@ use corepc_types::v29::{ GetAddressInfo, GetBlockHeaderVerbose, GetBlockVerboseOne, GetBlockVerboseZero, GetBlockchainInfo, GetMempoolInfo, GetNewAddress, GetRawMempool, GetRawMempoolVerbose, GetRawTransaction, GetRawTransactionVerbose, GetTransaction, GetTxOut, ImportDescriptors, - ListDescriptors, ListTransactions, SignRawTransactionWithWallet, SubmitPackage, - TestMempoolAccept, + ListDescriptors, ListTransactions, PsbtBumpFee, SignRawTransactionWithWallet, SubmitPackage, + TestMempoolAccept, WalletCreateFundedPsbt, WalletProcessPsbt, }; use corepc_types::{model, v29::CreateWallet}; use reqwest::{ @@ -41,8 +41,7 @@ use crate::{ types::{ CreateRawTransactionArguments, CreateRawTransactionInput, CreateRawTransactionOutput, CreateWalletArguments, ImportDescriptorInput, ListUnspentQueryOptions, - PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, SighashType, - WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, + PreviousTransactionOutput, PsbtBumpFeeOptions, SighashType, WalletCreateFundedPsbtOptions, }, }; @@ -603,18 +602,22 @@ impl Wallet for Client { locktime: Option, options: Option, bip32_derivs: Option, - ) -> ClientResult { - self.call::( - "walletcreatefundedpsbt", - &[ - to_value(inputs)?, - to_value(outputs)?, - to_value(locktime.unwrap_or(0))?, - to_value(options.unwrap_or_default())?, - to_value(bip32_derivs)?, - ], - ) - .await + ) -> ClientResult { + let resp = self + .call::( + "walletcreatefundedpsbt", + &[ + to_value(inputs)?, + to_value(outputs)?, + to_value(locktime.unwrap_or(0))?, + to_value(options.unwrap_or_default())?, + to_value(bip32_derivs)?, + ], + ) + .await?; + Ok(resp + .into_model() + .map_err(|e| ClientError::Parse(e.to_string()))?) } async fn get_address_info(&self, address: &Address) -> ClientResult { @@ -774,7 +777,7 @@ impl Signer for Client { sign: Option, sighashtype: Option, bip32_derivs: Option, - ) -> ClientResult { + ) -> ClientResult { let mut params = vec![to_value(psbt)?, to_value(sign.unwrap_or(true))?]; if let Some(sighashtype) = sighashtype { @@ -785,22 +788,29 @@ impl Signer for Client { params.push(to_value(bip32_derivs)?); } - self.call::("walletprocesspsbt", ¶ms) - .await + let resp = self + .call::("walletprocesspsbt", ¶ms) + .await?; + Ok(resp + .into_model() + .map_err(|e| ClientError::Parse(e.to_string()))?) } async fn psbt_bump_fee( &self, txid: &Txid, options: Option, - ) -> ClientResult { + ) -> ClientResult { let mut params = vec![to_value(txid.to_string())?]; if let Some(options) = options { params.push(to_value(options)?); } - self.call::("psbtbumpfee", ¶ms).await + let resp = self.call::("psbtbumpfee", ¶ms).await?; + Ok(resp + .into_model() + .map_err(|e| ClientError::Parse(e.to_string()))?) } } @@ -1026,7 +1036,7 @@ mod test { .wallet_process_psbt(&funded_psbt.psbt.to_string(), None, None, None) .await .unwrap(); - assert!(!processed_psbt.psbt.as_ref().unwrap().inputs.is_empty()); + assert!(!processed_psbt.psbt.inputs.is_empty()); assert!(processed_psbt.complete); let finalized_psbt = client diff --git a/src/traits.rs b/src/traits.rs index 63bf4dc..8b39fcf 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -2,18 +2,19 @@ use bitcoin::{bip32::Xpriv, block::Header, Address, Block, BlockHash, Network, T use corepc_types::model::{ GetAddressInfo, GetBlockchainInfo, GetMempoolInfo, GetRawMempool, GetRawMempoolVerbose, GetRawTransaction, GetRawTransactionVerbose, GetTransaction, GetTxOut, ListTransactions, - ListUnspent, SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, + ListUnspent, PsbtBumpFee, SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, + WalletCreateFundedPsbt, WalletProcessPsbt, }; use corepc_types::v29::ImportDescriptors; use std::future::Future; -use crate::types::ImportDescriptorInput; +use crate::types::{ImportDescriptorInput, SighashType}; use crate::{ client::ClientResult, types::{ CreateRawTransactionArguments, CreateRawTransactionInput, CreateRawTransactionOutput, - ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFee, PsbtBumpFeeOptions, - WalletCreateFundedPsbt, WalletCreateFundedPsbtOptions, WalletProcessPsbtResult, + ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFeeOptions, + WalletCreateFundedPsbtOptions, }, }; @@ -367,9 +368,9 @@ pub trait Signer { &self, psbt: &str, sign: Option, - sighashtype: Option, + sighashtype: Option, bip32_derivs: Option, - ) -> impl Future> + Send; + ) -> impl Future> + Send; /// Bumps the fee of an opt-in-RBF transaction, replacing it with a new transaction. /// diff --git a/src/types.rs b/src/types.rs index 9218967..4606835 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,7 +1,4 @@ -use bitcoin::{ - consensus::{self}, - Amount, FeeRate, Psbt, Transaction, Txid, -}; +use bitcoin::{Amount, FeeRate, Txid}; use serde::{ de::{self, Visitor}, Deserialize, Deserializer, Serialize, Serializer, @@ -136,31 +133,6 @@ pub struct CreateWalletArguments { pub load_on_startup: Option, } -/// Deserializes the amount in BTC into proper [`Amount`]s. -fn deserialize_bitcoin<'d, D>(deserializer: D) -> Result -where - D: Deserializer<'d>, -{ - struct SatVisitor; - - impl Visitor<'_> for SatVisitor { - type Value = Amount; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(formatter, "a float representation of btc values expected") - } - - fn visit_f64(self, v: f64) -> Result - where - E: de::Error, - { - let amount = Amount::from_btc(v).expect("Amount deserialization failed"); - Ok(amount) - } - } - deserializer.deserialize_any(SatVisitor) -} - /// Serializes the optional [`Amount`] into BTC. fn serialize_option_bitcoin(amount: &Option, serializer: S) -> Result where @@ -172,39 +144,6 @@ where } } -/// Deserializes the fee rate from sat/vB into proper [`FeeRate`]. -/// -/// Note: Bitcoin Core 0.21+ uses sat/vB for fee rates for most RPC methods/results. -fn deserialize_feerate<'d, D>(deserializer: D) -> Result -where - D: Deserializer<'d>, -{ - struct FeeRateVisitor; - - impl Visitor<'_> for FeeRateVisitor { - type Value = FeeRate; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - formatter, - "a numeric representation of fee rate in sat/vB expected" - ) - } - - fn visit_f64(self, v: f64) -> Result - where - E: de::Error, - { - // The value is already in sat/vB (Bitcoin Core 0.21+) - let sat_per_vb = v.round() as u64; - let fee_rate = FeeRate::from_sat_per_vb(sat_per_vb) - .ok_or_else(|| de::Error::custom("Invalid fee rate"))?; - Ok(fee_rate) - } - } - deserializer.deserialize_any(FeeRateVisitor) -} - /// Deserializes the transaction id string into proper [`Txid`]s. fn deserialize_txid<'d, D>(deserializer: D) -> Result where @@ -231,74 +170,11 @@ where deserializer.deserialize_any(TxidVisitor) } -/// Deserializes a base64-encoded PSBT string into proper [`Psbt`]s. -/// -/// # Note -/// -/// Expects a valid base64-encoded PSBT as defined in BIP 174. The PSBT -/// string must contain valid transaction data and metadata for successful parsing. -fn deserialize_psbt<'d, D>(deserializer: D) -> Result -where - D: Deserializer<'d>, -{ - struct PsbtVisitor; - - impl Visitor<'_> for PsbtVisitor { - type Value = Psbt; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(formatter, "a base64-encoded PSBT string expected") - } - - fn visit_str(self, v: &str) -> Result - where - E: de::Error, - { - v.parse::() - .map_err(|e| E::custom(format!("failed to deserialize PSBT: {e}"))) - } - } - deserializer.deserialize_any(PsbtVisitor) -} - -/// Deserializes an optional base64-encoded PSBT string into `Option`. -/// -/// # Note -/// -/// When the JSON field is `null` or missing, returns `None`. When present, -/// deserializes the base64 PSBT string using the same validation as [`deserialize_psbt`]. -fn deserialize_option_psbt<'d, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'d>, -{ - let opt: Option = Option::deserialize(deserializer)?; - match opt { - Some(s) => s - .parse::() - .map(Some) - .map_err(|e| de::Error::custom(format!("failed to deserialize PSBT: {e}"))), - None => Ok(None), - } -} - -fn deserialize_option_tx<'d, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'d>, -{ - let opt: Option = Option::deserialize(deserializer)?; - match opt { - Some(s) => consensus::encode::deserialize_hex::(&s) - .map(Some) - .map_err(|e| de::Error::custom(format!("failed to deserialize transaction hex: {e}"))), - None => Ok(None), - } -} - /// Signature hash types for Bitcoin transactions. /// /// These types specify which parts of a transaction are included in the signature /// hash calculation when signing transaction inputs. Used with wallet signing -/// operations like `wallet_process_psbt`. +/// operations like `walletprocesspsbt`. /// /// # Note /// @@ -404,80 +280,6 @@ pub struct WalletCreateFundedPsbtOptions { pub replaceable: Option, } -/// Result of the `walletcreatefundedpsbt` RPC method. -/// -/// Contains a funded PSBT created by the wallet with automatically selected inputs -/// to cover the specified outputs, along with fee information and change output details. -/// -/// # Note -/// -/// The PSBT returned is not signed and requires further processing with -/// `wallet_process_psbt` or `finalize_psbt` before broadcasting. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct WalletCreateFundedPsbt { - /// The funded PSBT with inputs selected by the wallet. - /// - /// Contains the unsigned transaction structure with all necessary - /// input and output information for subsequent signing operations. - #[serde(deserialize_with = "deserialize_psbt")] - pub psbt: Psbt, - - /// The fee amount in BTC paid by this transaction. - /// - /// Represents the total fee calculated based on the selected inputs, - /// outputs, and the specified fee rate or confirmation target. - #[serde(deserialize_with = "deserialize_bitcoin")] - pub fee: Amount, - - /// The position of the change output in the transaction outputs array. - /// - /// If no change output was created (exact amount match), this will be -1. - /// Otherwise, indicates the zero-based index of the change output. - #[serde(rename = "changepos")] - pub change_pos: i32, -} - -/// Result of the `walletprocesspsbt` and `finalizepsbt` RPC methods. -/// -/// Contains the processed PSBT state, completion status, and optionally the -/// extracted final transaction. This struct handles the Bitcoin Core's PSBT -/// workflow where PSBTs can be incrementally signed and eventually finalized. -/// -/// # Note -/// -/// The `psbt` field contains the updated PSBT after processing, while `hex` -/// contains the final transaction only when `complete` is `true` and extraction -/// is requested. Both fields may be `None` depending on the operation context. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct WalletProcessPsbtResult { - /// The processed Partially Signed Bitcoin Transaction. - /// - /// Contains the PSBT after wallet processing with any signatures or input data - /// that could be added. Will be `None` if the transaction was fully extracted - /// and the PSBT is no longer needed. - #[serde(deserialize_with = "deserialize_option_psbt")] - pub psbt: Option, - - /// Whether the transaction is complete and ready for broadcast. - /// - /// `true` indicates all required signatures have been collected and the - /// transaction can be finalized. `false` means more signatures are needed - /// before the transaction can be broadcast to the network. - pub complete: bool, - - /// The final transaction ready for broadcast (when complete). - /// - /// Contains the fully signed and finalized transaction when `complete` is `true` - /// and extraction was requested. Will be `None` for incomplete transactions or - /// when extraction is not performed. - #[serde( - deserialize_with = "deserialize_option_tx", - skip_serializing_if = "Option::is_none", - default - )] - pub hex: Option, -} - /// Query options for filtering unspent transaction outputs. /// /// Used with `list_unspent` to apply additional filtering criteria @@ -539,97 +341,3 @@ pub struct PsbtBumpFeeOptions { #[serde(skip_serializing_if = "Option::is_none")] pub original_change_index: Option, } - -/// Result of the psbtbumpfee RPC method. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct PsbtBumpFee { - /// The base64-encoded unsigned PSBT of the new transaction. - #[serde(deserialize_with = "deserialize_psbt")] - pub psbt: Psbt, - - /// The fee of the replaced transaction. - #[serde(deserialize_with = "deserialize_feerate")] - pub origfee: FeeRate, - - /// The fee of the new transaction. - #[serde(deserialize_with = "deserialize_feerate")] - pub fee: FeeRate, - - /// Errors encountered during processing (if any). - pub errors: Option>, -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json; - - // Taken from https://docs.rs/bitcoin/0.32.6/src/bitcoin/psbt/mod.rs.html#1515-1520 - // BIP 174 test vector with inputs and outputs (more realistic than empty transaction) - const TEST_PSBT: &str = "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAAAA"; - - // Valid Bitcoin transaction hex (Genesis block coinbase transaction) - const TEST_TX_HEX: &str = "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000"; - - #[test] - fn test_wallet_process_psbt_result() { - let valid_psbt = TEST_PSBT; - - // Test complete with hex - let test_tx_hex = TEST_TX_HEX; - let json1 = format!(r#"{{"psbt":"{valid_psbt}","complete":true,"hex":"{test_tx_hex}"}}"#); - let result1: WalletProcessPsbtResult = serde_json::from_str(&json1).unwrap(); - assert!(result1.psbt.is_some()); - assert!(result1.complete); - assert!(result1.hex.is_some()); - let tx = result1.hex.unwrap(); - assert!(!tx.input.is_empty()); - assert!(!tx.output.is_empty()); - - // Test incomplete without hex - let json2 = format!(r#"{{"psbt":"{valid_psbt}","complete":false}}"#); - let result2: WalletProcessPsbtResult = serde_json::from_str(&json2).unwrap(); - assert!(result2.psbt.is_some()); - assert!(!result2.complete); - } - - #[test] - fn test_sighashtype_serialize() { - let sighash = SighashType::All; - let serialized = serde_json::to_string(&sighash).unwrap(); - assert_eq!(serialized, "\"ALL\""); - - let sighash2 = SighashType::AllPlusAnyoneCanPay; - let serialized2 = serde_json::to_string(&sighash2).unwrap(); - assert_eq!(serialized2, "\"ALL|ANYONECANPAY\""); - } - - #[test] - fn test_list_unspent_query_options_camelcase() { - let options = ListUnspentQueryOptions { - minimum_amount: Some(Amount::from_btc(0.5).unwrap()), - maximum_amount: Some(Amount::from_btc(2.0).unwrap()), - maximum_count: Some(10), - }; - let serialized = serde_json::to_string(&options).unwrap(); - - assert!(serialized.contains("\"minimumAmount\":0.5")); - assert!(serialized.contains("\"maximumAmount\":2.0")); - assert!(serialized.contains("\"maximumCount\":10")); - } - - #[test] - fn test_psbt_parsing() { - // Test valid PSBT parsing - let valid_psbt = TEST_PSBT; - let json1 = format!(r#"{{"psbt":"{valid_psbt}","fee":0.001,"changepos":-1}}"#); - let result1: WalletCreateFundedPsbt = serde_json::from_str(&json1).unwrap(); - assert!(!result1.psbt.inputs.is_empty()); // BIP 174 test vector has inputs - - // Test invalid PSBT parsing fails - let invalid_psbt = "invalid_base64"; - let json2 = format!(r#"{{"psbt":"{invalid_psbt}","fee":0.001,"changepos":-1}}"#); - let result2 = serde_json::from_str::(&json2); - assert!(result2.is_err()); - } -} From 2bd9e946ebedcce1f7350bb507800230d0b8c916 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Thu, 2 Oct 2025 11:27:12 -0300 Subject: [PATCH 18/23] meta: feature-gate stuff --- Cargo.toml | 2 + src/client/mod.rs | 228 ++++++++++++++++++++++++++++++ src/{client.rs => client/v29.rs} | 235 ++----------------------------- src/lib.rs | 31 +++- src/traits.rs | 2 +- 5 files changed, 268 insertions(+), 230 deletions(-) create mode 100644 src/client/mod.rs rename src/{client.rs => client/v29.rs} (84%) diff --git a/Cargo.toml b/Cargo.toml index 57ab7ce..e7afa30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,8 @@ categories = ["cryptography::cryptocurrencies"] keywords = ["crypto", "bitcoin"] [features] +default = ["29_0"] +29_0 = [] [dependencies] base64 = "0.22.1" diff --git a/src/client/mod.rs b/src/client/mod.rs new file mode 100644 index 0000000..f134640 --- /dev/null +++ b/src/client/mod.rs @@ -0,0 +1,228 @@ +use std::{ + fmt, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, + time::Duration, +}; + +use crate::error::{BitcoinRpcError, ClientError}; +use base64::{engine::general_purpose, Engine}; +use reqwest::{ + header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}, + Client as ReqwestClient, +}; +use serde::{de, Deserialize, Serialize}; +use serde_json::{json, value::Value}; +use tokio::time::sleep; +use tracing::*; + +#[cfg(feature = "29_0")] +pub mod v29; + +/// This is an alias for the result type returned by the [`Client`]. +pub type ClientResult = Result; + +/// The maximum number of retries for a request. +const DEFAULT_MAX_RETRIES: u8 = 3; + +/// The maximum number of retries for a request. +const DEFAULT_RETRY_INTERVAL_MS: u64 = 1_000; + +/// Custom implementation to convert a value to a `Value` type. +pub fn to_value(value: T) -> ClientResult +where + T: Serialize, +{ + serde_json::to_value(value) + .map_err(|e| ClientError::Param(format!("Error creating value: {e}"))) +} + +/// An `async` client for interacting with a `bitcoind` instance. +#[derive(Debug, Clone)] +pub struct Client { + /// The URL of the `bitcoind` instance. + url: String, + + /// The underlying `async` HTTP client. + client: ReqwestClient, + + /// The ID of the current request. + /// + /// # Implementation Details + /// + /// Using an [`Arc`] so that [`Client`] is [`Clone`]. + id: Arc, + + /// The maximum number of retries for a request. + max_retries: u8, + + /// Interval between retries for a request in ms. + retry_interval: u64, +} + +/// Response returned by the `bitcoind` RPC server. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +struct Response { + pub result: Option, + pub error: Option, + pub id: u64, +} + +impl Client { + /// Creates a new [`Client`] with the given URL, username, and password. + pub fn new( + url: String, + username: String, + password: String, + max_retries: Option, + retry_interval: Option, + ) -> ClientResult { + if username.is_empty() || password.is_empty() { + return Err(ClientError::MissingUserPassword); + } + + let user_pw = general_purpose::STANDARD.encode(format!("{username}:{password}")); + let authorization = format!("Basic {user_pw}") + .parse() + .map_err(|_| ClientError::Other("Error parsing header".to_string()))?; + + let content_type = "application/json" + .parse() + .map_err(|_| ClientError::Other("Error parsing header".to_string()))?; + let headers = + HeaderMap::from_iter([(AUTHORIZATION, authorization), (CONTENT_TYPE, content_type)]); + + trace!(headers = ?headers); + + let client = ReqwestClient::builder() + .default_headers(headers) + .build() + .map_err(|e| ClientError::Other(format!("Could not create client: {e}")))?; + + let id = Arc::new(AtomicUsize::new(0)); + + let max_retries = max_retries.unwrap_or(DEFAULT_MAX_RETRIES); + let retry_interval = retry_interval.unwrap_or(DEFAULT_RETRY_INTERVAL_MS); + + trace!(url = %url, "Created bitcoin client"); + + Ok(Self { + url, + client, + id, + max_retries, + retry_interval, + }) + } + + fn next_id(&self) -> usize { + self.id.fetch_add(1, Ordering::AcqRel) + } + + async fn call( + &self, + method: &str, + params: &[Value], + ) -> ClientResult { + let mut retries = 0; + loop { + trace!(%method, ?params, %retries, "Calling bitcoin client"); + + let id = self.next_id(); + + let response = self + .client + .post(&self.url) + .json(&json!({ + "jsonrpc": "1.0", + "id": id, + "method": method, + "params": params + })) + .send() + .await; + trace!(?response, "Response received"); + match response { + Ok(resp) => { + // Check HTTP status code first before parsing body + let resp = match resp.error_for_status() { + Err(e) if e.is_status() => { + if let Some(status) = e.status() { + let reason = + status.canonical_reason().unwrap_or("Unknown").to_string(); + return Err(ClientError::Status(status.as_u16(), reason)); + } else { + return Err(ClientError::Other(e.to_string())); + } + } + Err(e) => { + return Err(ClientError::Other(e.to_string())); + } + Ok(resp) => resp, + }; + + let raw_response = resp + .text() + .await + .map_err(|e| ClientError::Parse(e.to_string()))?; + trace!(%raw_response, "Raw response received"); + let data: Response = serde_json::from_str(&raw_response) + .map_err(|e| ClientError::Parse(e.to_string()))?; + if let Some(err) = data.error { + return Err(ClientError::Server(err.code, err.message)); + } + return data + .result + .ok_or_else(|| ClientError::Other("Empty data received".to_string())); + } + Err(err) => { + warn!(err = %err, "Error calling bitcoin client"); + + if err.is_body() { + // Body error is unrecoverable + return Err(ClientError::Body(err.to_string())); + } else if err.is_status() { + // Status error is unrecoverable + let e = match err.status() { + Some(code) => ClientError::Status(code.as_u16(), err.to_string()), + _ => ClientError::Other(err.to_string()), + }; + return Err(e); + } else if err.is_decode() { + // Error decoding response, might be recoverable + let e = ClientError::MalformedResponse(err.to_string()); + warn!(%e, "decoding error, retrying..."); + } else if err.is_connect() { + // Connection error, might be recoverable + let e = ClientError::Connection(err.to_string()); + warn!(%e, "connection error, retrying..."); + } else if err.is_timeout() { + // Timeout error, might be recoverable + let e = ClientError::Timeout; + warn!(%e, "timeout error, retrying..."); + } else if err.is_request() { + // General request error, might be recoverable + let e = ClientError::Request(err.to_string()); + warn!(%e, "request error, retrying..."); + } else if err.is_builder() { + // Request builder error is unrecoverable + return Err(ClientError::ReqBuilder(err.to_string())); + } else if err.is_redirect() { + // Redirect error is unrecoverable + return Err(ClientError::HttpRedirect(err.to_string())); + } else { + // Unknown error is unrecoverable + return Err(ClientError::Other("Unknown error".to_string())); + } + } + } + retries += 1; + if retries >= self.max_retries { + return Err(ClientError::MaxRetriesExceeded(self.max_retries)); + } + sleep(Duration::from_millis(self.retry_interval)).await; + } + } +} diff --git a/src/client.rs b/src/client/v29.rs similarity index 84% rename from src/client.rs rename to src/client/v29.rs index c1555d2..6920247 100644 --- a/src/client.rs +++ b/src/client/v29.rs @@ -1,14 +1,7 @@ -use std::{ - env::var, - fmt, - sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, - }, - time::Duration, -}; +//! This module contains the implementation of the [`Client`] for Bitcoin Core v29. + +use std::env::var; -use base64::{engine::general_purpose, Engine}; use bitcoin::{ bip32::Xpriv, block::Header, @@ -23,234 +16,22 @@ use corepc_types::v29::{ TestMempoolAccept, WalletCreateFundedPsbt, WalletProcessPsbt, }; use corepc_types::{model, v29::CreateWallet}; -use reqwest::{ - header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}, - Client as ReqwestClient, -}; -use serde::{de, Deserialize, Serialize}; -use serde_json::{ - json, - value::{RawValue, Value}, -}; -use tokio::time::sleep; +use serde_json::value::{RawValue, Value}; use tracing::*; use crate::{ - error::{BitcoinRpcError, ClientError}, + client::Client, + error::ClientError, + to_value, traits::{Broadcaster, Reader, Signer, Wallet}, types::{ CreateRawTransactionArguments, CreateRawTransactionInput, CreateRawTransactionOutput, CreateWalletArguments, ImportDescriptorInput, ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFeeOptions, SighashType, WalletCreateFundedPsbtOptions, }, + ClientResult, }; -/// This is an alias for the result type returned by the [`Client`]. -pub type ClientResult = Result; - -/// The maximum number of retries for a request. -const DEFAULT_MAX_RETRIES: u8 = 3; - -/// The maximum number of retries for a request. -const DEFAULT_RETRY_INTERVAL_MS: u64 = 1_000; - -/// Custom implementation to convert a value to a `Value` type. -pub fn to_value(value: T) -> ClientResult -where - T: Serialize, -{ - serde_json::to_value(value) - .map_err(|e| ClientError::Param(format!("Error creating value: {e}"))) -} - -/// An `async` client for interacting with a `bitcoind` instance. -#[derive(Debug, Clone)] -pub struct Client { - /// The URL of the `bitcoind` instance. - url: String, - - /// The underlying `async` HTTP client. - client: ReqwestClient, - - /// The ID of the current request. - /// - /// # Implementation Details - /// - /// Using an [`Arc`] so that [`Client`] is [`Clone`]. - id: Arc, - - /// The maximum number of retries for a request. - max_retries: u8, - - /// Interval between retries for a request in ms. - retry_interval: u64, -} - -/// Response returned by the `bitcoind` RPC server. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -struct Response { - pub result: Option, - pub error: Option, - pub id: u64, -} - -impl Client { - /// Creates a new [`Client`] with the given URL, username, and password. - pub fn new( - url: String, - username: String, - password: String, - max_retries: Option, - retry_interval: Option, - ) -> ClientResult { - if username.is_empty() || password.is_empty() { - return Err(ClientError::MissingUserPassword); - } - - let user_pw = general_purpose::STANDARD.encode(format!("{username}:{password}")); - let authorization = format!("Basic {user_pw}") - .parse() - .map_err(|_| ClientError::Other("Error parsing header".to_string()))?; - - let content_type = "application/json" - .parse() - .map_err(|_| ClientError::Other("Error parsing header".to_string()))?; - let headers = - HeaderMap::from_iter([(AUTHORIZATION, authorization), (CONTENT_TYPE, content_type)]); - - trace!(headers = ?headers); - - let client = ReqwestClient::builder() - .default_headers(headers) - .build() - .map_err(|e| ClientError::Other(format!("Could not create client: {e}")))?; - - let id = Arc::new(AtomicUsize::new(0)); - - let max_retries = max_retries.unwrap_or(DEFAULT_MAX_RETRIES); - let retry_interval = retry_interval.unwrap_or(DEFAULT_RETRY_INTERVAL_MS); - - trace!(url = %url, "Created bitcoin client"); - - Ok(Self { - url, - client, - id, - max_retries, - retry_interval, - }) - } - - fn next_id(&self) -> usize { - self.id.fetch_add(1, Ordering::AcqRel) - } - - async fn call( - &self, - method: &str, - params: &[Value], - ) -> ClientResult { - let mut retries = 0; - loop { - trace!(%method, ?params, %retries, "Calling bitcoin client"); - - let id = self.next_id(); - - let response = self - .client - .post(&self.url) - .json(&json!({ - "jsonrpc": "1.0", - "id": id, - "method": method, - "params": params - })) - .send() - .await; - trace!(?response, "Response received"); - match response { - Ok(resp) => { - // Check HTTP status code first before parsing body - let resp = match resp.error_for_status() { - Err(e) if e.is_status() => { - if let Some(status) = e.status() { - let reason = - status.canonical_reason().unwrap_or("Unknown").to_string(); - return Err(ClientError::Status(status.as_u16(), reason)); - } else { - return Err(ClientError::Other(e.to_string())); - } - } - Err(e) => { - return Err(ClientError::Other(e.to_string())); - } - Ok(resp) => resp, - }; - - let raw_response = resp - .text() - .await - .map_err(|e| ClientError::Parse(e.to_string()))?; - trace!(%raw_response, "Raw response received"); - let data: Response = serde_json::from_str(&raw_response) - .map_err(|e| ClientError::Parse(e.to_string()))?; - if let Some(err) = data.error { - return Err(ClientError::Server(err.code, err.message)); - } - return data - .result - .ok_or_else(|| ClientError::Other("Empty data received".to_string())); - } - Err(err) => { - warn!(err = %err, "Error calling bitcoin client"); - - if err.is_body() { - // Body error is unrecoverable - return Err(ClientError::Body(err.to_string())); - } else if err.is_status() { - // Status error is unrecoverable - let e = match err.status() { - Some(code) => ClientError::Status(code.as_u16(), err.to_string()), - _ => ClientError::Other(err.to_string()), - }; - return Err(e); - } else if err.is_decode() { - // Error decoding response, might be recoverable - let e = ClientError::MalformedResponse(err.to_string()); - warn!(%e, "decoding error, retrying..."); - } else if err.is_connect() { - // Connection error, might be recoverable - let e = ClientError::Connection(err.to_string()); - warn!(%e, "connection error, retrying..."); - } else if err.is_timeout() { - // Timeout error, might be recoverable - let e = ClientError::Timeout; - warn!(%e, "timeout error, retrying..."); - } else if err.is_request() { - // General request error, might be recoverable - let e = ClientError::Request(err.to_string()); - warn!(%e, "request error, retrying..."); - } else if err.is_builder() { - // Request builder error is unrecoverable - return Err(ClientError::ReqBuilder(err.to_string())); - } else if err.is_redirect() { - // Redirect error is unrecoverable - return Err(ClientError::HttpRedirect(err.to_string())); - } else { - // Unknown error is unrecoverable - return Err(ClientError::Other("Unknown error".to_string())); - } - } - } - retries += 1; - if retries >= self.max_retries { - return Err(ClientError::MaxRetriesExceeded(self.max_retries)); - } - sleep(Duration::from_millis(self.retry_interval)).await; - } - } -} - impl Reader for Client { async fn estimate_smart_fee(&self, conf_target: u16) -> ClientResult { let result = self diff --git a/src/lib.rs b/src/lib.rs index 174fc8b..5495ab7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,36 @@ +//! BitcoinD JSON-RPC Async Client +//! +//! # Features +//! +//! - `29_0`: Enable support for Bitcoin Core v29 +//! +//! # Usage +//! +//! ```rust +//! use bitcoind_async_client::Client; +//! +//! let client = Client::new("http://localhost:8332", "username", "password", None, None).await?; +//! +//! let blockchain_info = client.get_blockchain_info().await?; +//! ``` +//! +//! # License +//! +//! This work is dual-licensed under MIT and Apache 2.0. +//! You can choose between one of them if you use this work. + pub mod client; pub mod error; pub mod traits; + +// TODO: remove this once upstreamed PRs are merged pub mod types; +pub use client::*; + +// v29 +#[cfg(feature = "29_0")] +pub use client::v29; + #[cfg(test)] pub mod test_utils; - -pub use client::*; diff --git a/src/traits.rs b/src/traits.rs index 8b39fcf..5581521 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -10,12 +10,12 @@ use std::future::Future; use crate::types::{ImportDescriptorInput, SighashType}; use crate::{ - client::ClientResult, types::{ CreateRawTransactionArguments, CreateRawTransactionInput, CreateRawTransactionOutput, ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFeeOptions, WalletCreateFundedPsbtOptions, }, + ClientResult, }; /// Basic functionality that any Bitcoin client that interacts with the From 988c2b1e3f4c10d812eafe3f932ea702e7135e21 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Thu, 2 Oct 2025 12:14:40 -0300 Subject: [PATCH 19/23] doc: fix docstrings --- src/traits.rs | 2 +- src/types.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/traits.rs b/src/traits.rs index 5581521..82bd57d 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -363,7 +363,7 @@ pub trait Signer { /// /// # Returns /// - /// Returns a [`WalletProcessPsbtResult`] with the processed PSBT and completion status. + /// Returns a [`WalletProcessPsbt`] with the processed PSBT and completion status. fn wallet_process_psbt( &self, psbt: &str, diff --git a/src/types.rs b/src/types.rs index 4606835..832034e 100644 --- a/src/types.rs +++ b/src/types.rs @@ -30,9 +30,9 @@ pub struct CreateRawTransactionInput { #[derive(Clone, Debug, PartialEq, Deserialize)] #[serde(untagged)] pub enum CreateRawTransactionOutput { - /// A pair of an [`Address`] string and an [`Amount`] in BTC. + /// A pair of an [`bitcoin::Address`] string and an [`Amount`] in BTC. AddressAmount { - /// An [`Address`] string. + /// An [`bitcoin::Address`] string. address: String, /// An [`Amount`] in BTC. amount: f64, From 3f2077f75abb725f3bfa10b8b4d6b91a77010257 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Thu, 2 Oct 2025 12:14:58 -0300 Subject: [PATCH 20/23] chore: clippy lints --- src/client/v29.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/client/v29.rs b/src/client/v29.rs index 6920247..3993b57 100644 --- a/src/client/v29.rs +++ b/src/client/v29.rs @@ -271,9 +271,9 @@ impl Broadcaster for Client { let resp = self .call::("testmempoolaccept", &[to_value([txstr])?]) .await?; - Ok(resp + resp .into_model() - .map_err(|e| ClientError::Parse(e.to_string()))?) + .map_err(|e| ClientError::Parse(e.to_string())) } async fn submit_package(&self, txs: &[Transaction]) -> ClientResult { @@ -396,9 +396,9 @@ impl Wallet for Client { ], ) .await?; - Ok(resp + resp .into_model() - .map_err(|e| ClientError::Parse(e.to_string()))?) + .map_err(|e| ClientError::Parse(e.to_string())) } async fn get_address_info(&self, address: &Address) -> ClientResult { @@ -406,9 +406,9 @@ impl Wallet for Client { let resp = self .call::("getaddressinfo", &[to_value(address.to_string())?]) .await?; - Ok(resp + resp .into_model() - .map_err(|e| ClientError::Parse(e.to_string()))?) + .map_err(|e| ClientError::Parse(e.to_string())) } async fn list_unspent( @@ -487,9 +487,9 @@ impl Signer for Client { &[to_value(tx_hex)?, to_value(prev_outputs)?], ) .await?; - Ok(resp + resp .into_model() - .map_err(|e| ClientError::Parse(e.to_string()))?) + .map_err(|e| ClientError::Parse(e.to_string())) } async fn get_xpriv(&self) -> ClientResult> { @@ -572,9 +572,9 @@ impl Signer for Client { let resp = self .call::("walletprocesspsbt", ¶ms) .await?; - Ok(resp + resp .into_model() - .map_err(|e| ClientError::Parse(e.to_string()))?) + .map_err(|e| ClientError::Parse(e.to_string())) } async fn psbt_bump_fee( @@ -589,9 +589,9 @@ impl Signer for Client { } let resp = self.call::("psbtbumpfee", ¶ms).await?; - Ok(resp + resp .into_model() - .map_err(|e| ClientError::Parse(e.to_string()))?) + .map_err(|e| ClientError::Parse(e.to_string())) } } From d2a80d2fde776f6458e17babd6f5bf967ed3fbae Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Thu, 2 Oct 2025 12:16:35 -0300 Subject: [PATCH 21/23] chore: fix module-level docs --- src/lib.rs | 4 ++-- src/types.rs | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5495ab7..f1677f2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,10 +6,10 @@ //! //! # Usage //! -//! ```rust +//! ```rust,ignore //! use bitcoind_async_client::Client; //! -//! let client = Client::new("http://localhost:8332", "username", "password", None, None).await?; +//! let client = Client::new("http://localhost:8332".to_string(), "username".to_string(), "password".to_string(), None, None).await?; //! //! let blockchain_info = client.get_blockchain_info().await?; //! ``` diff --git a/src/types.rs b/src/types.rs index 832034e..919b0ae 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,3 +1,5 @@ +//! Types that are not returned by the RPC server, but used as arguments/inputs of the RPC methods. + use bitcoin::{Amount, FeeRate, Txid}; use serde::{ de::{self, Visitor}, From e06869e950a3b3c547e3c756ca64a6b9f4308eff Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Thu, 2 Oct 2025 12:18:56 -0300 Subject: [PATCH 22/23] chore: cargo fmt --- src/client/v29.rs | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/client/v29.rs b/src/client/v29.rs index 3993b57..0ffd73e 100644 --- a/src/client/v29.rs +++ b/src/client/v29.rs @@ -8,14 +8,14 @@ use bitcoin::{ consensus::{self, encode::serialize_hex}, Address, Block, BlockHash, Network, Transaction, Txid, }; +use corepc_types::model; use corepc_types::v29::{ - GetAddressInfo, GetBlockHeaderVerbose, GetBlockVerboseOne, GetBlockVerboseZero, + CreateWallet, GetAddressInfo, GetBlockHeaderVerbose, GetBlockVerboseOne, GetBlockVerboseZero, GetBlockchainInfo, GetMempoolInfo, GetNewAddress, GetRawMempool, GetRawMempoolVerbose, GetRawTransaction, GetRawTransactionVerbose, GetTransaction, GetTxOut, ImportDescriptors, ListDescriptors, ListTransactions, PsbtBumpFee, SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, WalletCreateFundedPsbt, WalletProcessPsbt, }; -use corepc_types::{model, v29::CreateWallet}; use serde_json::value::{RawValue, Value}; use tracing::*; @@ -271,8 +271,7 @@ impl Broadcaster for Client { let resp = self .call::("testmempoolaccept", &[to_value([txstr])?]) .await?; - resp - .into_model() + resp.into_model() .map_err(|e| ClientError::Parse(e.to_string())) } @@ -396,8 +395,7 @@ impl Wallet for Client { ], ) .await?; - resp - .into_model() + resp.into_model() .map_err(|e| ClientError::Parse(e.to_string())) } @@ -406,8 +404,7 @@ impl Wallet for Client { let resp = self .call::("getaddressinfo", &[to_value(address.to_string())?]) .await?; - resp - .into_model() + resp.into_model() .map_err(|e| ClientError::Parse(e.to_string())) } @@ -487,8 +484,7 @@ impl Signer for Client { &[to_value(tx_hex)?, to_value(prev_outputs)?], ) .await?; - resp - .into_model() + resp.into_model() .map_err(|e| ClientError::Parse(e.to_string())) } @@ -572,8 +568,7 @@ impl Signer for Client { let resp = self .call::("walletprocesspsbt", ¶ms) .await?; - resp - .into_model() + resp.into_model() .map_err(|e| ClientError::Parse(e.to_string())) } @@ -589,8 +584,7 @@ impl Signer for Client { } let resp = self.call::("psbtbumpfee", ¶ms).await?; - resp - .into_model() + resp.into_model() .map_err(|e| ClientError::Parse(e.to_string())) } } From 591f9abf04de4d6f0983f723597553bcf865b830 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Thu, 9 Oct 2025 14:16:59 -0300 Subject: [PATCH 23/23] chore(deps): bump to corepc-types 0.10.1 --- Cargo.lock | 12 ++--- Cargo.toml | 4 +- src/client/v29.rs | 130 ++++------------------------------------------ 3 files changed, 17 insertions(+), 129 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e54fbcc..d4053ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -251,9 +251,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "corepc-client" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ce205b817339b55d93bdb41d66704cfc5299f89576ab24107bd2abf4c29c1e" +checksum = "7755b8b9219b23d166a5897b5e2d8266cbdd0de5861d351b96f6db26bcf415f3" dependencies = [ "bitcoin", "corepc-types", @@ -265,9 +265,9 @@ dependencies = [ [[package]] name = "corepc-node" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76025e0755bc411fda75e0912a0a0e511d13b54988bf05b90d7681f0c7570b67" +checksum = "69bd382fc775f760a8b55d658527621b890eaa3d8e8bc9779864659b172e81c6" dependencies = [ "anyhow", "bitcoin_hashes", @@ -284,9 +284,9 @@ dependencies = [ [[package]] name = "corepc-types" -version = "0.9.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f0231e773ddcfebb8eb8627a16553de56ab064a03843d847f409107ad661f25" +checksum = "c22db78b0223b66f82f92b14345f06307078f76d94b18280431ea9bc6cd9cbb6" dependencies = [ "bitcoin", "serde", diff --git a/Cargo.toml b/Cargo.toml index e7afa30..703ca54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ default = ["29_0"] [dependencies] base64 = "0.22.1" bitcoin = { version = "0.32.6", features = ["serde", "base64"] } -corepc-types = "0.9.0" +corepc-types = "0.10.1" hex = { package = "hex-conservative", version = "0.2.1" } # for optimization keep in sync with bitcoin reqwest = { version = "0.12.22", default-features = false, features = [ "http2", @@ -45,7 +45,7 @@ tracing = { version = "0.1.41", default-features = false } [dev-dependencies] anyhow = "1.0.100" -corepc-node = { version = "0.9.0", features = ["29_0", "download"] } +corepc-node = { version = "0.10.0", features = ["29_0", "download"] } tracing-subscriber = { version = "0.3.20", features = ["env-filter"] } [profile.release] diff --git a/src/client/v29.rs b/src/client/v29.rs index 0ffd73e..fb5ea26 100644 --- a/src/client/v29.rs +++ b/src/client/v29.rs @@ -13,8 +13,8 @@ use corepc_types::v29::{ CreateWallet, GetAddressInfo, GetBlockHeaderVerbose, GetBlockVerboseOne, GetBlockVerboseZero, GetBlockchainInfo, GetMempoolInfo, GetNewAddress, GetRawMempool, GetRawMempoolVerbose, GetRawTransaction, GetRawTransactionVerbose, GetTransaction, GetTxOut, ImportDescriptors, - ListDescriptors, ListTransactions, PsbtBumpFee, SignRawTransactionWithWallet, SubmitPackage, - TestMempoolAccept, WalletCreateFundedPsbt, WalletProcessPsbt, + ListDescriptors, ListTransactions, ListUnspent, PsbtBumpFee, SignRawTransactionWithWallet, + SubmitPackage, TestMempoolAccept, WalletCreateFundedPsbt, WalletProcessPsbt, }; use serde_json::value::{RawValue, Value}; use tracing::*; @@ -125,53 +125,10 @@ impl Reader for Client { async fn get_raw_mempool_verbose(&self) -> ClientResult { let resp = self - .call::("getrawmempool", &[to_value(true)?]) + .call::("getrawmempool", &[to_value(true)?]) .await?; - trace!(?resp, "Got raw mempool verbose"); - - let mut mempool_map: serde_json::Map = - serde_json::from_value(resp).map_err(|e| ClientError::Parse(e.to_string()))?; - - // FIXME(corepc-types): Transform field names in each mempool entry - for (_txid, entry) in &mut mempool_map { - if let Some(entry_map) = entry.as_object_mut() { - // Rename vsize to size - if let Some(vsize) = entry_map.remove("vsize") { - entry_map.insert("size".to_string(), vsize); - } - - // Flatten fees object: fees.base -> fee, fees.modified -> modifiedfee, etc. - // Keep the fees object too, as model might need both - if let Some(fees_obj) = entry_map.get("fees").cloned() { - if let Some(fees_map) = fees_obj.as_object() { - if let Some(base) = fees_map.get("base") { - entry_map.insert("fee".to_string(), base.clone()); - } - if let Some(modified) = fees_map.get("modified") { - entry_map.insert("modifiedfee".to_string(), modified.clone()); - } - if let Some(ancestor) = fees_map.get("ancestor") { - entry_map.insert("ancestorfees".to_string(), ancestor.clone()); - } - if let Some(descendant) = fees_map.get("descendant") { - entry_map.insert("descendantfees".to_string(), descendant.clone()); - } - } - } - - // Remove fields not expected by model - entry_map.remove("bip125-replaceable"); - entry_map.remove("unbroadcast"); - entry_map.remove("weight"); - } - } - - let mempool_verbose: GetRawMempoolVerbose = - serde_json::from_value(serde_json::Value::Object(mempool_map)) - .map_err(|e| ClientError::Parse(e.to_string()))?; - mempool_verbose - .into_model() + resp.into_model() .map_err(|e| ClientError::Parse(e.to_string())) } @@ -278,50 +235,11 @@ impl Broadcaster for Client { async fn submit_package(&self, txs: &[Transaction]) -> ClientResult { let txstrs: Vec = txs.iter().map(serialize_hex).collect(); let resp = self - .call::("submitpackage", &[to_value(txstrs)?]) + .call::("submitpackage", &[to_value(txstrs)?]) .await?; trace!(?resp, "Got submit package response"); - let mut package_map: serde_json::Map = - serde_json::from_value(resp).map_err(|e| ClientError::Parse(e.to_string()))?; - - // FIXME(corepc-types): Add missing effective-includes field to tx-results - // bitcoind only returns effective-includes for some transactions (child txs with fee bumping) - // but the model expects it to be present in all transactions - if let Some(tx_results) = package_map.get("tx-results").cloned() { - if let Some(mut tx_results_map) = tx_results.as_object().cloned() { - for (_wtxid, tx_result) in &mut tx_results_map { - if let Some(tx_result_map) = tx_result.as_object_mut() { - if let Some(fees) = tx_result_map.get("fees").cloned() { - if let Some(mut fees_map) = fees.as_object().cloned() { - // Add empty effective-includes if not present - if !fees_map.contains_key("effective-includes") { - fees_map.insert( - "effective-includes".to_string(), - serde_json::Value::Array(vec![]), - ); - } - tx_result_map.insert( - "fees".to_string(), - serde_json::Value::Object(fees_map), - ); - } - } - } - } - package_map.insert( - "tx-results".to_string(), - serde_json::Value::Object(tx_results_map), - ); - } - } - - let submit_package: SubmitPackage = - serde_json::from_value(serde_json::Value::Object(package_map)) - .map_err(|e| ClientError::Parse(e.to_string()))?; - - submit_package - .into_model() + resp.into_model() .map_err(|e| ClientError::Parse(e.to_string())) } } @@ -431,41 +349,11 @@ impl Wallet for Client { params.push(to_value(query_options)?); } - let resp = self - .call::("listunspent", ¶ms) - .await?; + let resp = self.call::("listunspent", ¶ms).await?; trace!(?resp, "Got UTXOs"); - let mut utxos: Vec = - serde_json::from_value(resp).map_err(|e| ClientError::Parse(e.to_string()))?; - - // FIXME(corepc-types): Transform field names in each UTXO - for utxo in &mut utxos { - if let Some(utxo_map) = utxo.as_object_mut() { - // Rename scriptPubKey to script_pubkey - if let Some(script_pubkey) = utxo_map.remove("scriptPubKey") { - utxo_map.insert("script_pubkey".to_string(), script_pubkey); - } - - // Rename desc to descriptor - if let Some(desc) = utxo_map.remove("desc") { - utxo_map.insert("descriptor".to_string(), desc); - } - - // Add missing label field if not present - if !utxo_map.contains_key("label") { - utxo_map.insert( - "label".to_string(), - serde_json::Value::String(String::new()), - ); - } - } - } - - let list_unspent: model::ListUnspent = - serde_json::from_value(serde_json::Value::Array(utxos)) - .map_err(|e| ClientError::Parse(e.to_string()))?; - Ok(list_unspent) + resp.into_model() + .map_err(|e| ClientError::Parse(e.to_string())) } }