diff --git a/Cargo.lock b/Cargo.lock index b3faed4..d4053ff 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", @@ -250,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", @@ -264,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", @@ -283,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 0b48ecf..703ca54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,10 +19,13 @@ categories = ["cryptography::cryptocurrencies"] keywords = ["crypto", "bitcoin"] [features] +default = ["29_0"] +29_0 = [] [dependencies] base64 = "0.22.1" bitcoin = { version = "0.32.6", features = ["serde", "base64"] } +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", @@ -42,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/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 65% rename from src/client.rs rename to src/client/v29.rs index cfb5c79..fb5ea26 100644 --- a/src/client.rs +++ b/src/client/v29.rs @@ -1,254 +1,37 @@ -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, consensus::{self, encode::serialize_hex}, Address, Block, BlockHash, Network, Transaction, Txid, }; -use reqwest::{ - header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}, - Client as ReqwestClient, +use corepc_types::model; +use corepc_types::v29::{ + CreateWallet, GetAddressInfo, GetBlockHeaderVerbose, GetBlockVerboseOne, GetBlockVerboseZero, + GetBlockchainInfo, GetMempoolInfo, GetNewAddress, GetRawMempool, GetRawMempoolVerbose, + GetRawTransaction, GetRawTransactionVerbose, GetTransaction, GetTxOut, ImportDescriptors, + ListDescriptors, ListTransactions, ListUnspent, PsbtBumpFee, SignRawTransactionWithWallet, + SubmitPackage, TestMempoolAccept, WalletCreateFundedPsbt, WalletProcessPsbt, }; -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 super::types::GetBlockHeaderVerbosityZero; use crate::{ - error::{BitcoinRpcError, ClientError}, + client::Client, + error::ClientError, + to_value, traits::{Broadcaster, Reader, Signer, Wallet}, types::{ - CreateRawTransaction, CreateRawTransactionInput, CreateRawTransactionOutput, CreateWallet, - 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, + 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 @@ -270,30 +53,31 @@ 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) } 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; @@ -319,9 +103,12 @@ 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 { @@ -330,39 +117,55 @@ 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?; + + resp.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( &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( @@ -370,16 +173,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 { @@ -412,17 +219,28 @@ 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?; + resp.into_model() + .map_err(|e| ClientError::Parse(e.to_string())) } - 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"); + + resp.into_model() + .map_err(|e| ClientError::Parse(e.to_string())) } } @@ -437,20 +255,23 @@ 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_utxos(&self) -> ClientResult> { - let resp = self.call::>("listunspent", &[]).await?; - trace!(?resp, "Got UTXOs"); - Ok(resp) + 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 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> { @@ -459,7 +280,7 @@ impl Wallet for Client { async fn create_raw_transaction( &self, - raw_tx: CreateRawTransaction, + raw_tx: CreateRawTransactionArguments, ) -> ClientResult { let raw_tx = self .call::( @@ -479,24 +300,30 @@ 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?; + resp.into_model() + .map_err(|e| ClientError::Parse(e.to_string())) } - 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?; + resp.into_model() + .map_err(|e| ClientError::Parse(e.to_string())) } async fn list_unspent( @@ -506,7 +333,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(); @@ -522,7 +349,11 @@ 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"); + + resp.into_model() + .map_err(|e| ClientError::Parse(e.to_string())) } } @@ -531,15 +362,18 @@ 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?; + resp.into_model() + .map_err(|e| ClientError::Parse(e.to_string())) } async fn get_xpriv(&self) -> ClientResult> { @@ -559,8 +393,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 `/` @@ -578,26 +412,26 @@ impl Signer for Client { async fn import_descriptors( &self, - descriptors: Vec, + descriptors: Vec, wallet_name: String, - ) -> ClientResult> { - let wallet_args = CreateWallet { - wallet_name, + ) -> ClientResult { + 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 - .call::>("importdescriptors", &[to_value(descriptors)?]) + .call::("importdescriptors", &[to_value(descriptors)?]) .await?; Ok(result) } @@ -608,7 +442,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 { @@ -619,22 +453,27 @@ impl Signer for Client { params.push(to_value(bip32_derivs)?); } - self.call::("walletprocesspsbt", ¶ms) - .await + let resp = self + .call::("walletprocesspsbt", ¶ms) + .await?; + 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?; + resp.into_model() + .map_err(|e| ClientError::Parse(e.to_string())) } } @@ -643,11 +482,8 @@ mod test { use std::sync::Once; - use bitcoin::{ - consensus::{self, encode::deserialize_hex}, - hashes::Hash, - transaction, Amount, FeeRate, NetworkKind, - }; + use bitcoin::{hashes::Hash, transaction, Amount, FeeRate, NetworkKind}; + use corepc_types::v29::ImportDescriptorsResult; use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; use super::*; @@ -734,7 +570,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); @@ -744,8 +580,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 @@ -760,18 +596,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(); @@ -784,14 +620,17 @@ 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 .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(), @@ -804,7 +643,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 @@ -813,7 +652,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; @@ -824,7 +663,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, @@ -832,8 +671,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(); @@ -853,7 +699,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 @@ -868,6 +714,7 @@ mod test { .test_mempool_accept(signed_tx) .await .unwrap() + .results .first() .unwrap() .txid; @@ -876,7 +723,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(); @@ -896,7 +743,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( @@ -908,8 +755,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() }); @@ -930,8 +777,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)); } @@ -962,7 +809,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(); @@ -1009,7 +856,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, @@ -1028,20 +875,16 @@ 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(); - let child_raw_tx = CreateRawTransaction { + let child_raw_tx = CreateRawTransactionArguments { inputs: vec![CreateRawTransactionInput { txid: parent_submitted.to_string(), vout: 0, @@ -1055,15 +898,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 @@ -1096,7 +935,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, @@ -1110,15 +949,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. @@ -1127,7 +962,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, @@ -1149,15 +984,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. @@ -1230,7 +1061,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" ); @@ -1247,6 +1078,7 @@ mod test { .test_mempool_accept(&signed_tx) .await .unwrap() + .results .first() .unwrap() .txid; @@ -1273,6 +1105,7 @@ mod test { .test_mempool_accept(&signed_tx) .await .unwrap() + .results .first() .unwrap() .txid; diff --git a/src/lib.rs b/src/lib.rs index 174fc8b..f1677f2 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,ignore +//! use bitcoind_async_client::Client; +//! +//! 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?; +//! ``` +//! +//! # 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 5851ad6..82bd57d 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,17 +1,21 @@ use bitcoin::{bip32::Xpriv, block::Header, Address, Block, BlockHash, Network, Transaction, Txid}; +use corepc_types::model::{ + GetAddressInfo, GetBlockchainInfo, GetMempoolInfo, GetRawMempool, GetRawMempoolVerbose, + GetRawTransaction, GetRawTransactionVerbose, GetTransaction, GetTxOut, ListTransactions, + ListUnspent, PsbtBumpFee, SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, + WalletCreateFundedPsbt, WalletProcessPsbt, +}; +use corepc_types::v29::ImportDescriptors; use std::future::Future; +use crate::types::{ImportDescriptorInput, SighashType}; use crate::{ - client::ClientResult, types::{ - 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, + CreateRawTransactionArguments, CreateRawTransactionInput, CreateRawTransactionOutput, + ListUnspentQueryOptions, PreviousTransactionOutput, PsbtBumpFeeOptions, + WalletCreateFundedPsbtOptions, }, + ClientResult, }; /// Basic functionality that any Bitcoin client that interacts with the @@ -86,7 +90,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( @@ -100,13 +104,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( @@ -147,7 +151,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. /// @@ -194,14 +198,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 @@ -211,7 +207,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; @@ -219,7 +215,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. @@ -318,7 +314,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 @@ -352,9 +348,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. /// @@ -367,14 +363,14 @@ 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, 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 1a1bee6..919b0ae 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,299 +1,10 @@ -use std::collections::BTreeMap; +//! Types that are not returned by the RPC server, but used as arguments/inputs of the RPC methods. -use bitcoin::{ - absolute::Height, - address::{self, NetworkUnchecked}, - block::Header, - consensus::{self, encode}, - Address, Amount, Block, BlockHash, FeeRate, Psbt, SignedAmount, Transaction, Txid, Wtxid, -}; +use bitcoin::{Amount, FeeRate, Txid}; use serde::{ - de::{self, IntoDeserializer, Visitor}, + 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 `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. -/// -/// 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. -/// -/// 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` -#[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 `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 ) -/// > -/// > 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`. /// @@ -301,7 +12,7 @@ pub struct ScriptPubkey { /// /// 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, } @@ -321,9 +32,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, @@ -358,276 +69,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 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: GetTransactionDetailCategory, - pub amount: f64, - pub label: Option, - pub vout: u32, - pub fee: Option, - 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 -/// -/// 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 `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 -/// -/// 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 `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 { - /// 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 -/// -/// 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`. /// @@ -668,30 +109,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 @@ -701,12 +121,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. /// @@ -714,38 +128,13 @@ pub struct ImportDescriptorResult { /// /// 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, } -/// 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 @@ -757,79 +146,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 *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 *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 @@ -856,182 +172,11 @@ 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 -/// -/// 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), - } -} - -/// 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) -} - -/// 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 /// hash calculation when signing transaction inputs. Used with wallet signing -/// operations like `wallet_process_psbt`. +/// operations like `walletprocesspsbt`. /// /// # Note /// @@ -1137,121 +282,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, -} - -/// 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 @@ -1313,97 +343,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()); - } -}