diff --git a/integration_test/tests/wallet.rs b/integration_test/tests/wallet.rs index 99ba73961..6831c5892 100644 --- a/integration_test/tests/wallet.rs +++ b/integration_test/tests/wallet.rs @@ -411,7 +411,6 @@ fn wallet__get_unconfirmed_balance__modelled() { } #[test] -#[cfg(feature = "v29_and_below")] fn wallet__get_wallet_info__modelled() { let node = Node::with_wallet(Wallet::Default, &[]); node.mine_a_block(); @@ -815,7 +814,7 @@ fn wallet__list_unspent__modelled() { } #[test] -#[cfg(all(feature = "v29_and_below", not(feature = "v17")))] +#[cfg(not(feature = "v17"))] fn wallet__list_wallet_dir() { let wallet_name = "test-wallet"; let node = Node::with_wallet(Wallet::None, &[]); @@ -865,6 +864,8 @@ fn wallet__lock_unspent() { #[test] #[cfg(all(feature = "v29_and_below", not(feature = "v23_and_below")))] fn wallet__migrate_wallet() { + // In v30 it is no longer possible to create a legacy wallet. + // It is tested in v29 and has no documented changes in v30. let node = Node::with_wallet(Wallet::None, &["-deprecatedrpc=create_bdb"]); let wallet_name = "legacy_wallet"; node.client.create_legacy_wallet(wallet_name).expect("createlegacywallet"); @@ -1012,9 +1013,13 @@ fn wallet__send_to_address__modelled() { } #[test] -#[cfg(feature = "v29_and_below")] +#[cfg(feature = "v30_and_below")] fn wallet__set_tx_fee() { + #[cfg(feature = "v29_and_below")] let node = Node::with_wallet(Wallet::Default, &[]); + #[cfg(not(feature = "v29_and_below"))] + let node = Node::with_wallet(Wallet::Default, &["-deprecatedrpc=settxfee"]); + let fee_rate = FeeRate::from_sat_per_vb(2).expect("2 sat/vb is valid"); let json: SetTxFee = node.client.set_tx_fee(fee_rate).expect("settxfee"); diff --git a/types/src/model/wallet.rs b/types/src/model/wallet.rs index dba090499..e5750caa4 100644 --- a/types/src/model/wallet.rs +++ b/types/src/model/wallet.rs @@ -438,16 +438,17 @@ pub struct GetWalletInfo { pub wallet_version: u32, /// Database format. v21 and later only. pub format: Option, - /// The total confirmed balance of the wallet in BTC. - pub balance: Amount, - /// The total unconfirmed balance of the wallet in BTC. - pub unconfirmed_balance: Amount, - /// The total immature balance of the wallet in BTC. - pub immature_balance: Amount, - /// The total number of transactions in the wallet + /// The total confirmed balance of the wallet in BTC. v17 to v29 only. + pub balance: Option, + /// The total unconfirmed balance of the wallet in BTC. v17 to v29 only. + pub unconfirmed_balance: Option, + /// The total immature balance of the wallet in BTC. v17 to v29 only. + pub immature_balance: Option, + /// The total number of transactions in the wallet. pub tx_count: u32, /// The timestamp (seconds since Unix epoch) of the oldest pre-generated key in the key pool. - pub keypool_oldest: u32, + /// v17 to v29 only. + pub keypool_oldest: Option, /// How many new keys are pre-generated (only counts external keys). pub keypool_size: u32, /// How many new keys are pre-generated for internal use (used for change outputs, only appears @@ -474,6 +475,8 @@ pub struct GetWalletInfo { pub blank: Option, /// The start time for blocks scanning. v26 and later only. pub birthtime: Option, + /// The flags currently set on the wallet. v30 and later only. + pub flags: Option>, /// Hash and height of the block this information was generated on. v26 and later only. pub last_processed_block: Option, } diff --git a/types/src/v17/wallet/into.rs b/types/src/v17/wallet/into.rs index 220cfe47d..ec8d5ab46 100644 --- a/types/src/v17/wallet/into.rs +++ b/types/src/v17/wallet/into.rs @@ -438,11 +438,11 @@ impl GetWalletInfo { wallet_name: self.wallet_name, wallet_version, format: None, - balance, - unconfirmed_balance, - immature_balance, + balance: Some(balance), + unconfirmed_balance: Some(unconfirmed_balance), + immature_balance: Some(immature_balance), tx_count, - keypool_oldest, + keypool_oldest: Some(keypool_oldest), keypool_size, keypool_size_hd_internal, unlocked_until: self.unlocked_until, @@ -455,6 +455,7 @@ impl GetWalletInfo { external_signer: None, blank: None, birthtime: None, + flags: None, last_processed_block: None, }) } diff --git a/types/src/v18/wallet/into.rs b/types/src/v18/wallet/into.rs index 41cb98cf7..c985ce058 100644 --- a/types/src/v18/wallet/into.rs +++ b/types/src/v18/wallet/into.rs @@ -191,11 +191,11 @@ impl GetWalletInfo { wallet_name: self.wallet_name, wallet_version, format: None, - balance, - unconfirmed_balance, - immature_balance, + balance: Some(balance), + unconfirmed_balance: Some(unconfirmed_balance), + immature_balance: Some(immature_balance), tx_count, - keypool_oldest, + keypool_oldest: Some(keypool_oldest), keypool_size, keypool_size_hd_internal, unlocked_until: self.unlocked_until, @@ -208,6 +208,7 @@ impl GetWalletInfo { external_signer: None, blank: None, birthtime: None, + flags: None, last_processed_block: None, }) } diff --git a/types/src/v19/wallet/into.rs b/types/src/v19/wallet/into.rs index bb8730689..8162336e6 100644 --- a/types/src/v19/wallet/into.rs +++ b/types/src/v19/wallet/into.rs @@ -138,11 +138,11 @@ impl GetWalletInfo { wallet_name: self.wallet_name, wallet_version, format: None, - balance, - unconfirmed_balance, - immature_balance, + balance: Some(balance), + unconfirmed_balance: Some(unconfirmed_balance), + immature_balance: Some(immature_balance), tx_count, - keypool_oldest, + keypool_oldest: Some(keypool_oldest), keypool_size, keypool_size_hd_internal, unlocked_until: self.unlocked_until, @@ -155,6 +155,7 @@ impl GetWalletInfo { external_signer: None, blank: None, birthtime: None, + flags: None, last_processed_block: None, }) } diff --git a/types/src/v21/wallet/into.rs b/types/src/v21/wallet/into.rs index 75c1a1d5f..0417993d3 100644 --- a/types/src/v21/wallet/into.rs +++ b/types/src/v21/wallet/into.rs @@ -93,11 +93,11 @@ impl GetWalletInfo { Ok(model::GetWalletInfo { wallet_name: self.wallet_name, wallet_version, - balance, - unconfirmed_balance, - immature_balance, + balance: Some(balance), + unconfirmed_balance: Some(unconfirmed_balance), + immature_balance: Some(immature_balance), tx_count, - keypool_oldest, + keypool_oldest: Some(keypool_oldest), keypool_size, keypool_size_hd_internal, unlocked_until: self.unlocked_until, @@ -111,6 +111,7 @@ impl GetWalletInfo { external_signer: None, blank: None, birthtime: None, + flags: None, last_processed_block: None, }) } diff --git a/types/src/v23/wallet/into.rs b/types/src/v23/wallet/into.rs index f1a0e21fa..75601a188 100644 --- a/types/src/v23/wallet/into.rs +++ b/types/src/v23/wallet/into.rs @@ -121,11 +121,11 @@ impl GetWalletInfo { Ok(model::GetWalletInfo { wallet_name: self.wallet_name, wallet_version, - balance, - unconfirmed_balance, - immature_balance, + balance: Some(balance), + unconfirmed_balance: Some(unconfirmed_balance), + immature_balance: Some(immature_balance), tx_count, - keypool_oldest: keypool_oldest.unwrap_or(0), + keypool_oldest: Some(keypool_oldest.unwrap_or(0)), keypool_size, keypool_size_hd_internal: keypool_size_hd_internal.unwrap_or(0), unlocked_until: self.unlocked_until, @@ -139,6 +139,7 @@ impl GetWalletInfo { external_signer: Some(self.external_signer), blank: None, birthtime: None, + flags: None, last_processed_block: None, }) } diff --git a/types/src/v26/wallet/into.rs b/types/src/v26/wallet/into.rs index a7b449b69..da984fa4b 100644 --- a/types/src/v26/wallet/into.rs +++ b/types/src/v26/wallet/into.rs @@ -147,11 +147,11 @@ impl GetWalletInfo { wallet_name: self.wallet_name, wallet_version, format: Some(self.format), - balance, - unconfirmed_balance, - immature_balance, + balance: Some(balance), + unconfirmed_balance: Some(unconfirmed_balance), + immature_balance: Some(immature_balance), tx_count, - keypool_oldest: keypool_oldest.unwrap_or(0), + keypool_oldest: Some(keypool_oldest.unwrap_or(0)), keypool_size, keypool_size_hd_internal: keypool_size_hd_internal.unwrap_or(0), unlocked_until: self.unlocked_until, @@ -164,6 +164,7 @@ impl GetWalletInfo { external_signer: Some(self.external_signer), blank: Some(self.blank), birthtime: self.birthtime, + flags: None, last_processed_block, }) } diff --git a/types/src/v30/mod.rs b/types/src/v30/mod.rs index ff6d7d794..6ba7656eb 100644 --- a/types/src/v30/mod.rs +++ b/types/src/v30/mod.rs @@ -191,7 +191,7 @@ //! | getreceivedbyaddress | version + model | | //! | getreceivedbylabel | version + model | | //! | gettransaction | version + model | | -//! | getwalletinfo | version + model | TODO | +//! | getwalletinfo | version + model | | //! | importdescriptors | version | | //! | importprunedfunds | returns nothing | | //! | keypoolrefill | returns nothing | | @@ -199,14 +199,14 @@ //! | listdescriptors | version | | //! | listlabels | version | | //! | listlockunspent | version + model | | -//! | migratewallet | version | TODO | +//! | migratewallet | version | Untested in v30, unchanged from v29 | //! | psbtbumpfee | version + model | | //! | listreceivedbyaddress | version + model | | //! | listreceivedbylabel | version + model | | //! | listsinceblock | version + model | | //! | listtransactions | version + model | | //! | listunspent | version + model | | -//! | listwalletdir | version | TODO | +//! | listwalletdir | version | | //! | listwallets | version + model | | //! | loadwallet | version + model | | //! | lockunspent | version | | @@ -218,7 +218,7 @@ //! | sendmany | version + model | | //! | sendtoaddress | version + model | | //! | setlabel | returns nothing | | -//! | settxfee | version | TODO | +//! | settxfee | version | | //! | setwalletflag | version | | //! | signmessage | version + model | | //! | signrawtransactionwithwallet | version + model | | @@ -245,6 +245,7 @@ mod blockchain; mod mining; mod raw_transactions; +mod wallet; #[doc(inline)] pub use self::{ @@ -256,6 +257,10 @@ pub use self::{ TaprootLeaf, TaprootLeafError, TaprootScript, TaprootScriptError, TaprootScriptPathSig, TaprootScriptPathSigError, }, + wallet::{ + GetWalletInfo, GetWalletInfoError, GetWalletInfoScanning, LastProcessedBlock, + LastProcessedBlockError, ListWalletDir, ListWalletDirWallet, + }, }; #[doc(inline)] pub use crate::{ @@ -289,8 +294,7 @@ pub use crate::{ ActiveCommand, AnalyzePsbt, AnalyzePsbtError, AnalyzePsbtInput, AnalyzePsbtInputMissing, AnalyzePsbtInputMissingError, DeriveAddresses, GetAddressInfoError, GetReceivedByLabel, GetZmqNotifications, JoinPsbts, JsonRpcError, ListReceivedByAddress, - ListReceivedByAddressItem, ListReceivedByLabel, ListReceivedByLabelError, ListWalletDir, - ListWalletDirWallet, UtxoUpdatePsbt, + ListReceivedByAddressItem, ListReceivedByLabel, ListReceivedByLabelError, UtxoUpdatePsbt, }, v19::{ Bip9SoftforkInfo, Bip9SoftforkStatistics, Bip9SoftforkStatus, GetBalancesMine, @@ -325,9 +329,8 @@ pub use crate::{ v26::{ AddrManInfoNetwork, CreateWallet, DescriptorProcessPsbt, DescriptorProcessPsbtError, DumpTxOutSet, DumpTxOutSetError, GetAddrManInfo, GetBalances, GetBalancesError, - GetPeerInfo, GetTransactionError, GetTxOutSetInfo, GetTxOutSetInfoError, GetWalletInfo, - GetWalletInfoError, GetWalletInfoScanning, LastProcessedBlock, LastProcessedBlockError, - LoadTxOutSet, LoadTxOutSetError, LoadWallet, PeerInfo, UnloadWallet, WalletProcessPsbt, + GetPeerInfo, GetTransactionError, GetTxOutSetInfo, GetTxOutSetInfoError, LoadTxOutSet, + LoadTxOutSetError, LoadWallet, PeerInfo, UnloadWallet, WalletProcessPsbt, WalletProcessPsbtError, }, v27::{GetPrioritisedTransactions, PrioritisedTransaction}, diff --git a/types/src/v30/wallet/error.rs b/types/src/v30/wallet/error.rs new file mode 100644 index 000000000..1fc74a432 --- /dev/null +++ b/types/src/v30/wallet/error.rs @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: CC0-1.0 + +use core::fmt; + +use bitcoin::amount::ParseAmountError; +use bitcoin::hex; + +use crate::error::write_err; +use crate::NumericError; + +/// Error when converting a `GetWalletInfo` type into the model type. +#[derive(Debug)] +pub enum GetWalletInfoError { + /// Conversion of numeric type to expected type failed. + Numeric(NumericError), + /// Conversion of the `pay_tx_fee` field failed. + PayTxFee(ParseAmountError), + /// Conversion of the `last_processed_block` field failed. + LastProcessedBlock(LastProcessedBlockError), +} + +impl fmt::Display for GetWalletInfoError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Self::Numeric(ref e) => write_err!(f, "numeric"; e), + Self::PayTxFee(ref e) => + write_err!(f, "conversion of the `pay_tx_fee` field failed"; e), + Self::LastProcessedBlock(ref e) => + write_err!(f, "conversion of the `last_processed_block` field failed"; e), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for GetWalletInfoError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match *self { + Self::Numeric(ref e) => Some(e), + Self::PayTxFee(ref e) => Some(e), + Self::LastProcessedBlock(ref e) => Some(e), + } + } +} + +impl From for GetWalletInfoError { + fn from(e: NumericError) -> Self { Self::Numeric(e) } +} + +/// Error when converting a `LastProcessedBlock` type into the model type. +#[derive(Debug)] +pub enum LastProcessedBlockError { + /// Conversion of the `hash` field failed. + Hash(hex::HexToArrayError), + /// Conversion of the `height` field failed. + Height(NumericError), +} + +impl fmt::Display for LastProcessedBlockError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Self::Hash(ref e) => write_err!(f, "conversion of the `hash` field failed"; e), + Self::Height(ref e) => write_err!(f, "conversion of the `height` field failed"; e), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for LastProcessedBlockError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match *self { + Self::Hash(ref e) => Some(e), + Self::Height(ref e) => Some(e), + } + } +} + +impl From for LastProcessedBlockError { + fn from(e: NumericError) -> Self { Self::Height(e) } +} diff --git a/types/src/v30/wallet/into.rs b/types/src/v30/wallet/into.rs new file mode 100644 index 000000000..db4a90cf5 --- /dev/null +++ b/types/src/v30/wallet/into.rs @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: CC0-1.0 + +use bitcoin::BlockHash; + +use super::{ + GetWalletInfo, GetWalletInfoError, GetWalletInfoScanning, LastProcessedBlock, + LastProcessedBlockError, +}; +use crate::model; + +impl GetWalletInfo { + /// Converts version specific type to a version nonspecific, more strongly typed type. + pub fn into_model(self) -> Result { + use GetWalletInfoError as E; + + let wallet_version = crate::to_u32(self.wallet_version, "wallet_version")?; + let tx_count = crate::to_u32(self.tx_count, "tx_count")?; + let keypool_size = crate::to_u32(self.keypool_size, "keypool_size")?; + let keypool_size_hd_internal = self + .keypool_size_hd_internal + .map(|v| crate::to_u32(v, "keypool_size_hd_internal")) + .transpose()?; + let pay_tx_fee = crate::btc_per_kb(self.pay_tx_fee).map_err(E::PayTxFee)?; + let last_processed_block = self + .last_processed_block + .map(|l| l.into_model()) + .transpose() + .map_err(E::LastProcessedBlock)?; + + let scanning = match self.scanning { + GetWalletInfoScanning::Details { duration, progress } => + Some(model::GetWalletInfoScanning::Details { duration, progress }), + GetWalletInfoScanning::NotScanning(b) => + Some(model::GetWalletInfoScanning::NotScanning(b)), + }; + + Ok(model::GetWalletInfo { + wallet_name: self.wallet_name, + wallet_version, + format: Some(self.format), + balance: None, + unconfirmed_balance: None, + immature_balance: None, + tx_count, + keypool_oldest: None, + keypool_size, + keypool_size_hd_internal: keypool_size_hd_internal.unwrap_or(0), + unlocked_until: self.unlocked_until, + pay_tx_fee, + hd_seed_id: None, + private_keys_enabled: self.private_keys_enabled, + avoid_reuse: Some(self.avoid_reuse), + scanning, + descriptors: Some(self.descriptors), + external_signer: Some(self.external_signer), + blank: Some(self.blank), + birthtime: self.birthtime, + flags: Some(self.flags), + last_processed_block, + }) + } +} + +impl LastProcessedBlock { + /// Converts version specific type to a version nonspecific, more strongly typed type. + pub fn into_model(self) -> Result { + let hash = self.hash.parse::().map_err(LastProcessedBlockError::Hash)?; + let height = crate::to_u32(self.height, "height")?; + Ok(model::LastProcessedBlock { height, hash }) + } +} diff --git a/types/src/v30/wallet/mod.rs b/types/src/v30/wallet/mod.rs new file mode 100644 index 000000000..ca84a81d1 --- /dev/null +++ b/types/src/v30/wallet/mod.rs @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! The JSON-RPC API for Bitcoin Core `v30` - wallet. +//! +//! Types for methods found under the `== Wallet ==` section of the API docs. + +mod error; +mod into; + +use serde::{Deserialize, Serialize}; + +pub use self::error::{GetWalletInfoError, LastProcessedBlockError}; + +/// Result of the JSON-RPC method `getwalletinfo`. +/// +/// > getwalletinfo +/// > +/// > Returns an object containing various wallet state info. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[cfg_attr(feature = "serde-deny-unknown-fields", serde(deny_unknown_fields))] +pub struct GetWalletInfo { + /// the wallet name + #[serde(rename = "walletname")] + pub wallet_name: String, + /// the wallet version + #[serde(rename = "walletversion")] + pub wallet_version: i64, + /// the database format (bdb or sqlite) + pub format: String, + /// the total number of transactions in the wallet + #[serde(rename = "txcount")] + pub tx_count: i64, + /// how many new keys are pre-generated (only counts external keys) + #[serde(rename = "keypoolsize")] + pub keypool_size: i64, + /// how many new keys are pre-generated for internal use (used for change outputs, only appears if the wallet is using this feature, otherwise external keys are used) + #[serde(rename = "keypoolsize_hd_internal")] + pub keypool_size_hd_internal: Option, + /// the UNIX epoch time until which the wallet is unlocked for transfers, or 0 if the wallet is locked (only present for passphrase-encrypted wallets) + pub unlocked_until: Option, + /// the transaction fee configuration, set in BTC/kvB + #[serde(rename = "paytxfee")] + pub pay_tx_fee: f64, + /// false if privatekeys are disabled for this wallet (enforced watch-only wallet) + pub private_keys_enabled: bool, + /// whether this wallet tracks clean/dirty coins in terms of reuse + pub avoid_reuse: bool, + /// current scanning details, or false if no scan is in progress + pub scanning: GetWalletInfoScanning, + /// whether this wallet uses descriptors for scriptPubKey management + pub descriptors: bool, + /// whether this wallet is configured to use an external signer such as a hardware wallet + pub external_signer: bool, + /// Whether this wallet intentionally does not contain any keys, scripts, or descriptors + pub blank: bool, + /// The start time for blocks scanning. It could be modified by (re)importing any descriptor with an earlier timestamp. + pub birthtime: Option, + /// The flags currently set on the wallet. + pub flags: Vec, + /// hash and height of the block this information was generated on + #[serde(rename = "lastprocessedblock")] + pub last_processed_block: Option, +} + +/// Current scanning details. Part of `getwalletinfo`. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[serde(untagged)] +pub enum GetWalletInfoScanning { + /// Scanning details. + Details { duration: u64, progress: f64 }, + /// Not scanning (false). + NotScanning(bool), +} + +/// Last processed block item. Part of of `getwalletinfo`. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[cfg_attr(feature = "serde-deny-unknown-fields", serde(deny_unknown_fields))] +pub struct LastProcessedBlock { + /// Hash of the block this information was generated on. + pub hash: String, + /// Height of the block this information was generated on. + pub height: i64, +} + +/// Result of the JSON-RPC method `listwalletdir`. +/// +/// > listwalletdir +/// > +/// > Returns a list of wallets in the wallet directory. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[cfg_attr(feature = "serde-deny-unknown-fields", serde(deny_unknown_fields))] +pub struct ListWalletDir { + /// The list of wallets in the wallet directory. + pub wallets: Vec, +} + +/// Wallet entry. Part of `listwalletdir`. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[cfg_attr(feature = "serde-deny-unknown-fields", serde(deny_unknown_fields))] +pub struct ListWalletDirWallet { + /// The wallet name. + pub name: String, + /// Warning messages, if any, related to loading the wallet. + pub warnings: Option>, +}