From 9683813aaa0d213add0295239a8af3b05a77183a Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Fri, 8 Mar 2024 21:21:58 -0700 Subject: [PATCH] zcash_client_sqlite: Use account source metadata for note selection. --- zcash_client_backend/CHANGELOG.md | 13 ++- zcash_client_backend/src/data_api.rs | 97 +++++++++++++++++-- zcash_client_backend/src/data_api/error.rs | 9 ++ zcash_client_backend/src/data_api/wallet.rs | 21 ++-- .../src/data_api/wallet/input_selection.rs | 19 +--- zcash_client_sqlite/src/chain.rs | 34 +++---- zcash_client_sqlite/src/lib.rs | 61 +++++++----- zcash_client_sqlite/src/testing.rs | 27 ++++-- zcash_client_sqlite/src/testing/pool.rs | 60 +++++++----- zcash_client_sqlite/src/wallet.rs | 27 +++--- zcash_keys/CHANGELOG.md | 4 +- zcash_keys/src/keys.rs | 27 ++++++ 12 files changed, 278 insertions(+), 121 deletions(-) diff --git a/zcash_client_backend/CHANGELOG.md b/zcash_client_backend/CHANGELOG.md index 67c8bdb51c..aeac526349 100644 --- a/zcash_client_backend/CHANGELOG.md +++ b/zcash_client_backend/CHANGELOG.md @@ -16,6 +16,7 @@ and this library adheres to Rust's notion of - `Account` - `AccountBalance::with_orchard_balance_mut` - `AccountBirthday::orchard_frontier` + - `AccountSources` - `BlockMetadata::orchard_tree_size` - `DecryptedTransaction::{new, tx(), orchard_outputs()}` - `ScannedBlock::orchard` @@ -54,9 +55,11 @@ and this library adheres to Rust's notion of - `get_account_for_ufvk` now returns an `Self::Account` instead of a bare `AccountId` - Changes to the `InputSource` trait: - - `select_spendable_notes` now takes its `target_value` argument as a - `NonNegativeAmount`. Also, the values of the returned map are also - `NonNegativeAmount`s instead of `Amount`s. + - `select_spendable_notes` has changed: + - It now takes its `target_value` argument as a `NonNegativeAmount`. + - Instead of an `AccountId`, it takes an `AccountSources` argument. The + separate `sources` argument has been removed. + - The values of the returned map are `NonNegativeAmount`s instead of `Amount`s. - Fields of `DecryptedTransaction` are now private. Use `DecryptedTransaction::new` and the newly provided accessors instead. - Fields of `SentTransaction` are now private. Use `SentTransaction::new` @@ -68,6 +71,10 @@ and this library adheres to Rust's notion of - `fn put_orchard_subtree_roots` - Added method `WalletRead::validate_seed` - Removed `Error::AccountNotFound` variant. + - `wallet::input_selection::InputSelector::propose_transaction` now takes an + `AccountSources` rather than a bare `AccountId`. + - `wallet::{propose_transfer, propose_standard_transfer_to_address}` now + each take an `AccountSources` instead of a bare `AccountId`. - `zcash_client_backend::decrypt`: - Fields of `DecryptedOutput` are now private. Use `DecryptedOutput::new` and the newly provided accessors instead. diff --git a/zcash_client_backend/src/data_api.rs b/zcash_client_backend/src/data_api.rs index 313f162fb5..de738efb37 100644 --- a/zcash_client_backend/src/data_api.rs +++ b/zcash_client_backend/src/data_api.rs @@ -315,6 +315,28 @@ pub trait Account { /// Returns the UFVK that the wallet backend has stored for the account, if any. fn ufvk(&self) -> Option<&UnifiedFullViewingKey>; + + /// Returns the default sources of funds that are available to spending by the account. + /// + /// This corresponds to the set of shielded pools for which the account's UFVK can + /// maintain balance. + fn default_sources(&self) -> Option> { + self.ufvk().map(|ufvk| { + #[cfg(not(feature = "orchard"))] + let use_orchard = false; + #[cfg(feature = "orchard")] + let use_orchard = ufvk.orchard().is_some(); + + let use_sapling = ufvk.sapling().is_some(); + + AccountSources { + account_id: self.id(), + use_orchard, + use_sapling, + use_transparent: false, + } + }) + } } impl Account for (A, UnifiedFullViewingKey) { @@ -337,6 +359,68 @@ impl Account for (A, Option) { } } +/// A type that describes what FVK components are known to the wallet for an account. +#[derive(Clone, Copy, Debug)] +pub struct AccountSources { + account_id: AccountId, + use_orchard: bool, + use_sapling: bool, + use_transparent: bool, +} + +impl AccountSources { + /// Constructs AccountSources from its constituent parts + pub fn new( + account_id: AccountId, + use_orchard: bool, + use_sapling: bool, + use_transparent: bool, + ) -> Self { + Self { + account_id, + use_orchard, + use_sapling, + use_transparent, + } + } + + /// Returns the id for the account to which this metadata applies. + pub fn account_id(&self) -> AccountId { + self.account_id + } + + /// Returns whether the account has an Orchard balance and spendability determination + /// capability. + pub fn use_orchard(&self) -> bool { + self.use_orchard + } + + /// Returns whether the account has an Sapling balance and spendability determination + /// capability. + pub fn use_sapling(&self) -> bool { + self.use_sapling + } + + /// Returns whether the account has a Transparent balance determination capability. + pub fn use_transparent(&self) -> bool { + self.use_transparent + } + + /// Restricts the sources to be used to those for which the given [`UnifiedSpendingKey`] + /// provides a spending capability. + pub fn filter_with_usk(&mut self, usk: &UnifiedSpendingKey) { + self.use_orchard &= usk.has_orchard(); + self.use_sapling &= usk.has_sapling(); + self.use_transparent &= usk.has_transparent(); + } + + /// Returns the [`UnifiedAddressRequest`] that will produce a [`UnifiedAddress`] having + /// receivers corresponding to the spending capabilities described by this value. + pub fn to_unified_address_request(&self) -> Option { + UnifiedAddressRequest::new(self.use_orchard, self.use_sapling, self.use_transparent) + } +} + /// A polymorphic ratio type, usually used for rational numbers. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Ratio { @@ -473,9 +557,8 @@ pub trait InputSource { /// be included. fn select_spendable_notes( &self, - account: Self::AccountId, + inputs_from: AccountSources, target_value: NonNegativeAmount, - sources: &[ShieldedProtocol], anchor_height: BlockHeight, exclude: &[Self::NoteRef], ) -> Result>, Self::Error>; @@ -1404,9 +1487,10 @@ pub mod testing { }; use super::{ - chain::CommitmentTreeRoot, scanning::ScanRange, AccountBirthday, BlockMetadata, - DecryptedTransaction, InputSource, NullifierQuery, ScannedBlock, SentTransaction, - WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT, + chain::CommitmentTreeRoot, scanning::ScanRange, AccountBirthday, AccountSources, + BlockMetadata, DecryptedTransaction, InputSource, NullifierQuery, ScannedBlock, + SentTransaction, WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, + SAPLING_SHARD_HEIGHT, }; #[cfg(feature = "transparent-inputs")] @@ -1457,9 +1541,8 @@ pub mod testing { fn select_spendable_notes( &self, - _account: Self::AccountId, + _inputs_from: AccountSources, _target_value: NonNegativeAmount, - _sources: &[ShieldedProtocol], _anchor_height: BlockHeight, _exclude: &[Self::NoteRef], ) -> Result>, Self::Error> { diff --git a/zcash_client_backend/src/data_api/error.rs b/zcash_client_backend/src/data_api/error.rs index 0641571bb8..46804254c9 100644 --- a/zcash_client_backend/src/data_api/error.rs +++ b/zcash_client_backend/src/data_api/error.rs @@ -42,6 +42,9 @@ pub enum Error { /// No account could be found corresponding to a provided spending key. KeyNotRecognized, + /// No shielded source of funds was available for an account. + NoShieldedSources, + /// Zcash amount computation encountered an overflow or underflow. BalanceError(BalanceError), @@ -122,6 +125,12 @@ where "Wallet does not contain an account corresponding to the provided spending key" ) } + Error::NoShieldedSources => { + write!( + f, + "A wallet account contains no shielded sources of funds." + ) + } Error::BalanceError(e) => write!( f, "The value lies outside the valid range of Zcash amounts: {:?}.", diff --git a/zcash_client_backend/src/data_api/wallet.rs b/zcash_client_backend/src/data_api/wallet.rs index aa73bd3f8c..2509e239ea 100644 --- a/zcash_client_backend/src/data_api/wallet.rs +++ b/zcash_client_backend/src/data_api/wallet.rs @@ -41,7 +41,7 @@ use sapling::{ }; use std::num::NonZeroU32; -use super::InputSource; +use super::{AccountSources, InputSource}; use crate::{ address::Address, data_api::{ @@ -269,7 +269,7 @@ where wallet_db, params, StandardFeeRule::PreZip313, - account.id(), + account.default_sources().ok_or(Error::NoShieldedSources)?, min_confirmations, to, amount, @@ -380,7 +380,7 @@ where let proposal = propose_transfer( wallet_db, params, - account.id(), + account.default_sources().ok_or(Error::NoShieldedSources)?, input_selector, request, min_confirmations, @@ -405,7 +405,7 @@ where pub fn propose_transfer( wallet_db: &mut DbT, params: &ParamsT, - spend_from_account: ::AccountId, + sources: AccountSources<::AccountId>, input_selector: &InputsT, request: zip321::TransactionRequest, min_confirmations: NonZeroU32, @@ -435,7 +435,7 @@ where wallet_db, target_height, anchor_height, - spend_from_account, + sources, request, ) .map_err(Error::from) @@ -453,9 +453,10 @@ where /// * `wallet_db`: A read/write reference to the wallet database. /// * `params`: Consensus parameters. /// * `fee_rule`: The fee rule to use in creating the transaction. -/// * `spend_from_account`: The unified account that controls the funds that will be spent -/// in the resulting transaction. This procedure will return an error if the -/// account ID does not correspond to an account known to the wallet. +/// * `sources`: Metadata that describes the unified account and the pools from which +/// funds may be spent in the resulting transaction. This procedure will return an +/// error if the contained account ID does not correspond to an account known to +/// the wallet. /// * `min_confirmations`: The minimum number of confirmations that a previously /// received note must have in the blockchain in order to be considered for being /// spent. A value of 10 confirmations is recommended and 0-conf transactions are @@ -472,7 +473,7 @@ pub fn propose_standard_transfer_to_address( wallet_db: &mut DbT, params: &ParamsT, fee_rule: StandardFeeRule, - spend_from_account: ::AccountId, + sources: AccountSources<::AccountId>, min_confirmations: NonZeroU32, to: &Address, amount: NonNegativeAmount, @@ -520,7 +521,7 @@ where propose_transfer( wallet_db, params, - spend_from_account, + sources, &input_selector, request, min_confirmations, diff --git a/zcash_client_backend/src/data_api/wallet/input_selection.rs b/zcash_client_backend/src/data_api/wallet/input_selection.rs index 1627698bab..a327216d7e 100644 --- a/zcash_client_backend/src/data_api/wallet/input_selection.rs +++ b/zcash_client_backend/src/data_api/wallet/input_selection.rs @@ -21,7 +21,7 @@ use zcash_primitives::{ use crate::{ address::{Address, UnifiedAddress}, - data_api::InputSource, + data_api::{AccountSources, InputSource}, fees::{sapling, ChangeError, ChangeStrategy, DustOutputPolicy}, proposal::{Proposal, ProposalError, ShieldedInputs}, wallet::{Note, ReceivedNote, WalletTransparentOutput}, @@ -148,7 +148,7 @@ pub trait InputSelector { wallet_db: &Self::InputSource, target_height: BlockHeight, anchor_height: BlockHeight, - account: ::AccountId, + sources: AccountSources<::AccountId>, transaction_request: TransactionRequest, ) -> Result< Proposal::NoteRef>, @@ -328,7 +328,7 @@ where wallet_db: &Self::InputSource, target_height: BlockHeight, anchor_height: BlockHeight, - account: ::AccountId, + sources: AccountSources<::AccountId>, transaction_request: TransactionRequest, ) -> Result< Proposal, @@ -454,19 +454,8 @@ where Err(other) => return Err(other.into()), } - #[cfg(not(feature = "orchard"))] - let selectable_pools = &[ShieldedProtocol::Sapling]; - #[cfg(feature = "orchard")] - let selectable_pools = &[ShieldedProtocol::Sapling, ShieldedProtocol::Orchard]; - shielded_inputs = wallet_db - .select_spendable_notes( - account, - amount_required, - selectable_pools, - anchor_height, - &exclude, - ) + .select_spendable_notes(sources, amount_required, anchor_height, &exclude) .map_err(InputSelectorError::DataSource)?; let new_available = shielded_inputs diff --git a/zcash_client_sqlite/src/chain.rs b/zcash_client_sqlite/src/chain.rs index 38b32d3c1b..c753d7a091 100644 --- a/zcash_client_sqlite/src/chain.rs +++ b/zcash_client_sqlite/src/chain.rs @@ -439,7 +439,7 @@ mod tests { .with_block_cache() .with_test_account(AccountBirthday::from_sapling_activation) .build(); - let account = st.test_account().unwrap(); + let account = st.test_account().unwrap().0.account_id(); let dfvk = st.test_account_sapling().unwrap(); @@ -456,7 +456,7 @@ mod tests { st.scan_cached_blocks(h, 2); // Account balance should reflect both received notes - assert_eq!(st.get_total_balance(account.0), (value + value2).unwrap()); + assert_eq!(st.get_total_balance(account), (value + value2).unwrap()); // "Rewind" to height of last scanned block st.wallet_mut() @@ -464,7 +464,7 @@ mod tests { .unwrap(); // Account balance should be unaltered - assert_eq!(st.get_total_balance(account.0), (value + value2).unwrap()); + assert_eq!(st.get_total_balance(account), (value + value2).unwrap()); // Rewind so that one block is dropped st.wallet_mut() @@ -472,13 +472,13 @@ mod tests { .unwrap(); // Account balance should only contain the first received note - assert_eq!(st.get_total_balance(account.0), value); + assert_eq!(st.get_total_balance(account), value); // Scan the cache again st.scan_cached_blocks(h, 2); // Account balance should again reflect both received notes - assert_eq!(st.get_total_balance(account.0), (value + value2).unwrap()); + assert_eq!(st.get_total_balance(account), (value + value2).unwrap()); } #[test] @@ -487,7 +487,7 @@ mod tests { .with_block_cache() .with_test_account(AccountBirthday::from_sapling_activation) .build(); - let account = st.test_account().unwrap(); + let account = st.test_account().unwrap().0.account_id(); let (_, usk, _) = st.test_account().unwrap(); let dfvk = st.test_account_sapling().unwrap(); @@ -496,7 +496,7 @@ mod tests { let value = NonNegativeAmount::const_from_u64(50000); let (h1, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); st.scan_cached_blocks(h1, 1); - assert_eq!(st.get_total_balance(account.0), value); + assert_eq!(st.get_total_balance(account), value); // Create blocks to reach SAPLING_ACTIVATION_HEIGHT + 2 let (h2, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); @@ -508,7 +508,7 @@ mod tests { // Now scan the block of height SAPLING_ACTIVATION_HEIGHT + 1 st.scan_cached_blocks(h2, 1); assert_eq!( - st.get_total_balance(account.0), + st.get_total_balance(account), NonNegativeAmount::const_from_u64(150_000) ); @@ -544,7 +544,7 @@ mod tests { .with_block_cache() .with_test_account(AccountBirthday::from_sapling_activation) .build(); - let account = st.test_account().unwrap(); + let account = st.test_account().unwrap().0.account_id(); let dfvk = st.test_account_sapling().unwrap(); @@ -562,7 +562,7 @@ mod tests { assert_eq!(summary.received_sapling_note_count(), 1); // Account balance should reflect the received note - assert_eq!(st.get_total_balance(account.0), value); + assert_eq!(st.get_total_balance(account), value); // Create a second fake CompactBlock sending more value to the address let value2 = NonNegativeAmount::const_from_u64(7); @@ -575,7 +575,7 @@ mod tests { assert_eq!(summary.received_sapling_note_count(), 1); // Account balance should reflect both received notes - assert_eq!(st.get_total_balance(account.0), (value + value2).unwrap()); + assert_eq!(st.get_total_balance(account), (value + value2).unwrap()); } #[test] @@ -584,7 +584,7 @@ mod tests { .with_block_cache() .with_test_account(AccountBirthday::from_sapling_activation) .build(); - let account = st.test_account().unwrap(); + let account = st.test_account().unwrap().0.account_id(); let dfvk = st.test_account_sapling().unwrap(); // Wallet summary is not yet available @@ -599,7 +599,7 @@ mod tests { st.scan_cached_blocks(received_height, 1); // Account balance should reflect the received note - assert_eq!(st.get_total_balance(account.0), value); + assert_eq!(st.get_total_balance(account), value); // Create a second fake CompactBlock spending value from the address let extsk2 = ExtendedSpendingKey::master(&[0]); @@ -611,7 +611,7 @@ mod tests { st.scan_cached_blocks(spent_height, 1); // Account balance should equal the change - assert_eq!(st.get_total_balance(account.0), (value - value2).unwrap()); + assert_eq!(st.get_total_balance(account), (value - value2).unwrap()); } #[test] @@ -620,7 +620,7 @@ mod tests { .with_block_cache() .with_test_account(AccountBirthday::from_sapling_activation) .build(); - let account = st.test_account().unwrap(); + let account = st.test_account().unwrap().0.account_id(); let dfvk = st.test_account_sapling().unwrap(); @@ -642,12 +642,12 @@ mod tests { st.scan_cached_blocks(spent_height, 1); // Account balance should equal the change - assert_eq!(st.get_total_balance(account.0), (value - value2).unwrap()); + assert_eq!(st.get_total_balance(account), (value - value2).unwrap()); // Now scan the block in which we received the note that was spent. st.scan_cached_blocks(received_height, 1); // Account balance should be the same. - assert_eq!(st.get_total_balance(account.0), (value - value2).unwrap()); + assert_eq!(st.get_total_balance(account), (value - value2).unwrap()); } } diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index a624cd918c..7f4f4f9171 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -60,9 +60,9 @@ use zcash_client_backend::{ self, chain::{BlockSource, CommitmentTreeRoot}, scanning::{ScanPriority, ScanRange}, - AccountBirthday, BlockMetadata, DecryptedTransaction, InputSource, NullifierQuery, - ScannedBlock, SentTransaction, WalletCommitmentTrees, WalletRead, WalletSummary, - WalletWrite, SAPLING_SHARD_HEIGHT, + AccountBirthday, AccountSources, BlockMetadata, DecryptedTransaction, InputSource, + NullifierQuery, ScannedBlock, SentTransaction, WalletCommitmentTrees, WalletRead, + WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT, }, keys::{ AddressGenerationError, UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey, @@ -212,20 +212,23 @@ impl, P: consensus::Parameters> InputSource for fn select_spendable_notes( &self, - account: AccountId, + sources: AccountSources, target_value: NonNegativeAmount, - _sources: &[ShieldedProtocol], anchor_height: BlockHeight, exclude: &[Self::NoteRef], ) -> Result>, Self::Error> { - wallet::sapling::select_spendable_sapling_notes( - self.conn.borrow(), - &self.params, - account, - target_value, - anchor_height, - exclude, - ) + if sources.use_sapling() { + wallet::sapling::select_spendable_sapling_notes( + self.conn.borrow(), + &self.params, + sources.account_id(), + target_value, + anchor_height, + exclude, + ) + } else { + Ok(vec![]) + } } #[cfg(feature = "transparent-inputs")] @@ -1317,7 +1320,7 @@ mod tests { use secrecy::SecretVec; use zcash_client_backend::data_api::{AccountBirthday, WalletRead, WalletWrite}; - use crate::{testing::TestBuilder, AccountId, DEFAULT_UA_REQUEST}; + use crate::{testing::TestBuilder, AccountId}; #[cfg(feature = "unstable")] use { @@ -1332,7 +1335,7 @@ mod tests { .build(); assert!({ - let account = st.test_account().unwrap().0; + let account = st.test_account().unwrap().0.account_id(); st.wallet() .validate_seed(account, st.test_seed().unwrap()) .unwrap() @@ -1348,7 +1351,7 @@ mod tests { // check that passing an invalid seed results in a failure assert!({ - let account = st.test_account().unwrap().0; + let account = st.test_account().unwrap().0.account_id(); !st.wallet() .validate_seed(account, &SecretVec::new(vec![1u8; 32])) .unwrap() @@ -1360,20 +1363,29 @@ mod tests { let mut st = TestBuilder::new() .with_test_account(AccountBirthday::from_sapling_activation) .build(); - let account = st.test_account().unwrap(); + let sources = st.test_account().unwrap().0; - let current_addr = st.wallet().get_current_address(account.0).unwrap(); + let current_addr = st + .wallet() + .get_current_address(sources.account_id()) + .unwrap(); assert!(current_addr.is_some()); // TODO: Add Orchard let addr2 = st .wallet_mut() - .get_next_available_address(account.0, DEFAULT_UA_REQUEST) + .get_next_available_address( + sources.account_id(), + sources.to_unified_address_request().unwrap(), + ) .unwrap(); assert!(addr2.is_some()); assert_ne!(current_addr, addr2); - let addr2_cur = st.wallet().get_current_address(account.0).unwrap(); + let addr2_cur = st + .wallet() + .get_current_address(sources.account_id()) + .unwrap(); assert_eq!(addr2, addr2_cur); } @@ -1385,17 +1397,20 @@ mod tests { .with_block_cache() .with_test_account(AccountBirthday::from_sapling_activation) .build(); - let account = st.test_account().unwrap(); + let sources = st.test_account().unwrap().0; let (_, usk, _) = st.test_account().unwrap(); let ufvk = usk.to_unified_full_viewing_key(); let (taddr, _) = usk.default_transparent_address(); - let receivers = st.wallet().get_transparent_receivers(account.0).unwrap(); + let receivers = st + .wallet() + .get_transparent_receivers(sources.account_id()) + .unwrap(); // The receiver for the default UA should be in the set. assert!(receivers.contains_key( - ufvk.default_address(DEFAULT_UA_REQUEST) + ufvk.default_address(sources.to_unified_address_request().unwrap()) .expect("A valid default address exists for the UFVK") .0 .transparent() diff --git a/zcash_client_sqlite/src/testing.rs b/zcash_client_sqlite/src/testing.rs index 0cf71a6df1..9387ee63ce 100644 --- a/zcash_client_sqlite/src/testing.rs +++ b/zcash_client_sqlite/src/testing.rs @@ -44,6 +44,7 @@ use zcash_client_backend::{ zip321, }; use zcash_client_backend::{ + data_api::AccountSources, fees::{standard, DustOutputPolicy}, ShieldedProtocol, }; @@ -488,10 +489,20 @@ impl TestState { } /// Exposes the test account, if enabled via [`TestBuilder::with_test_account`]. - pub(crate) fn test_account(&self) -> Option<(AccountId, UnifiedSpendingKey, AccountBirthday)> { - self.test_account - .as_ref() - .map(|(_, a, k, b)| (*a, k.clone(), b.clone())) + pub(crate) fn test_account( + &self, + ) -> Option<( + AccountSources, + UnifiedSpendingKey, + AccountBirthday, + )> { + self.test_account.as_ref().map(|(_, a, k, b)| { + ( + AccountSources::new(*a, k.has_orchard(), k.has_sapling(), k.has_transparent()), + k.clone(), + b.clone(), + ) + }) } /// Exposes the test account's Sapling DFVK, if enabled via [`TestBuilder::with_test_account`]. @@ -583,7 +594,7 @@ impl TestState { #[allow(clippy::type_complexity)] pub(crate) fn propose_transfer( &mut self, - spend_from_account: AccountId, + account_sources: AccountSources, input_selector: &InputsT, request: zip321::TransactionRequest, min_confirmations: NonZeroU32, @@ -603,7 +614,7 @@ impl TestState { propose_transfer::<_, _, _, Infallible>( &mut self.db_data, ¶ms, - spend_from_account, + account_sources, input_selector, request, min_confirmations, @@ -615,7 +626,7 @@ impl TestState { #[allow(clippy::too_many_arguments)] pub(crate) fn propose_standard_transfer( &mut self, - spend_from_account: AccountId, + account_sources: AccountSources, fee_rule: StandardFeeRule, min_confirmations: NonZeroU32, to: &Address, @@ -637,7 +648,7 @@ impl TestState { &mut self.db_data, ¶ms, fee_rule, - spend_from_account, + account_sources, min_confirmations, to, amount, diff --git a/zcash_client_sqlite/src/testing/pool.rs b/zcash_client_sqlite/src/testing/pool.rs index 331250e878..6182f203a6 100644 --- a/zcash_client_sqlite/src/testing/pool.rs +++ b/zcash_client_sqlite/src/testing/pool.rs @@ -127,7 +127,8 @@ pub(crate) fn send_single_step_proposed_transfer() { .with_test_account(AccountBirthday::from_sapling_activation) .build(); - let (account, usk, _) = st.test_account().unwrap(); + let (sources, usk, _) = st.test_account().unwrap(); + let account = sources.account_id(); let dfvk = T::test_account_fvk(&st); // Add funds to the wallet in a single note @@ -174,7 +175,7 @@ pub(crate) fn send_single_step_proposed_transfer() { let proposal = st .propose_transfer( - account, + sources, input_selector, request, NonZeroU32::new(1).unwrap(), @@ -274,7 +275,8 @@ pub(crate) fn send_multi_step_proposed_transfer() { .with_test_account(AccountBirthday::from_sapling_activation) .build(); - let (account, usk, _) = st.test_account().unwrap(); + let (sources, usk, _) = st.test_account().unwrap(); + let account = sources.account_id(); let dfvk = T::test_account_fvk(&st); // Add funds to the wallet in a single note @@ -317,7 +319,7 @@ pub(crate) fn send_multi_step_proposed_transfer() { ); let proposal0 = st .propose_transfer( - account, + sources, &input_selector, request0, NonZeroU32::new(1).unwrap(), @@ -478,7 +480,8 @@ pub(crate) fn spend_fails_on_unverified_notes() { .with_test_account(AccountBirthday::from_sapling_activation) .build(); - let (account, usk, _) = st.test_account().unwrap(); + let (sources, usk, _) = st.test_account().unwrap(); + let account = sources.account_id(); let dfvk = T::test_account_fvk(&st); // Add funds to the wallet in a single note @@ -526,7 +529,7 @@ pub(crate) fn spend_fails_on_unverified_notes() { let to = T::sk_default_address(&extsk2); assert_matches!( st.propose_standard_transfer::( - account, + sources, StandardFeeRule::Zip317, NonZeroU32::new(2).unwrap(), &to, @@ -556,7 +559,7 @@ pub(crate) fn spend_fails_on_unverified_notes() { // Spend still fails assert_matches!( st.propose_standard_transfer::( - account, + sources, StandardFeeRule::Zip317, NonZeroU32::new(10).unwrap(), &to, @@ -591,7 +594,7 @@ pub(crate) fn spend_fails_on_unverified_notes() { let min_confirmations = NonZeroU32::new(10).unwrap(); let proposal = st .propose_standard_transfer::( - account, + sources, StandardFeeRule::Zip317, min_confirmations, &to, @@ -625,7 +628,8 @@ pub(crate) fn spend_fails_on_locked_notes() { .with_test_account(AccountBirthday::from_sapling_activation) .build(); - let (account, usk, _) = st.test_account().unwrap(); + let (sources, usk, _) = st.test_account().unwrap(); + let account = sources.account_id(); let dfvk = T::test_account_fvk(&st); // TODO: This test was originally written to use the pre-zip-313 fee rule @@ -648,7 +652,7 @@ pub(crate) fn spend_fails_on_locked_notes() { let min_confirmations = NonZeroU32::new(1).unwrap(); let proposal = st .propose_standard_transfer::( - account, + sources, fee_rule, min_confirmations, &to, @@ -668,7 +672,7 @@ pub(crate) fn spend_fails_on_locked_notes() { // A second proposal fails because there are no usable notes assert_matches!( st.propose_standard_transfer::( - account, + sources, fee_rule, NonZeroU32::new(1).unwrap(), &to, @@ -698,7 +702,7 @@ pub(crate) fn spend_fails_on_locked_notes() { // Second proposal still fails assert_matches!( st.propose_standard_transfer::( - account, + sources, fee_rule, NonZeroU32::new(1).unwrap(), &to, @@ -731,7 +735,7 @@ pub(crate) fn spend_fails_on_locked_notes() { let min_confirmations = NonZeroU32::new(1).unwrap(); let proposal = st .propose_standard_transfer::( - account, + sources, fee_rule, min_confirmations, &to, @@ -762,7 +766,8 @@ pub(crate) fn ovk_policy_prevents_recovery_from_chain() { .with_test_account(AccountBirthday::from_sapling_activation) .build(); - let (account, usk, _) = st.test_account().unwrap(); + let (sources, usk, _) = st.test_account().unwrap(); + let account = sources.account_id(); let dfvk = T::test_account_fvk(&st); // Add funds to the wallet in a single note @@ -796,7 +801,7 @@ pub(crate) fn ovk_policy_prevents_recovery_from_chain() { > { let min_confirmations = NonZeroU32::new(1).unwrap(); let proposal = st.propose_standard_transfer( - account, + sources, fee_rule, min_confirmations, &addr2, @@ -857,7 +862,8 @@ pub(crate) fn spend_succeeds_to_t_addr_zero_change() { .with_test_account(AccountBirthday::from_sapling_activation) .build(); - let (account, usk, _) = st.test_account().unwrap(); + let (sources, usk, _) = st.test_account().unwrap(); + let account = sources.account_id(); let dfvk = T::test_account_fvk(&st); // Add funds to the wallet in a single note @@ -879,7 +885,7 @@ pub(crate) fn spend_succeeds_to_t_addr_zero_change() { let min_confirmations = NonZeroU32::new(1).unwrap(); let proposal = st .propose_standard_transfer::( - account, + sources, fee_rule, min_confirmations, &to, @@ -903,7 +909,8 @@ pub(crate) fn change_note_spends_succeed() { .with_test_account(AccountBirthday::from_sapling_activation) .build(); - let (account, usk, _) = st.test_account().unwrap(); + let (sources, usk, _) = st.test_account().unwrap(); + let account = sources.account_id(); let dfvk = T::test_account_fvk(&st); // Add funds to the wallet in a single note owned by the internal spending key @@ -944,7 +951,7 @@ pub(crate) fn change_note_spends_succeed() { let min_confirmations = NonZeroU32::new(1).unwrap(); let proposal = st .propose_standard_transfer::( - account, + sources, fee_rule, min_confirmations, &to, @@ -1079,7 +1086,8 @@ pub(crate) fn zip317_spend() { .with_test_account(AccountBirthday::from_sapling_activation) .build(); - let (account, usk, _) = st.test_account().unwrap(); + let (sources, usk, _) = st.test_account().unwrap(); + let account = sources.account_id(); let dfvk = T::test_account_fvk(&st); // Add funds to the wallet @@ -1172,7 +1180,8 @@ pub(crate) fn shield_transparent() { .with_test_account(AccountBirthday::from_sapling_activation) .build(); - let (account_id, usk, _) = st.test_account().unwrap(); + let (sources, usk, _) = st.test_account().unwrap(); + let account_id = sources.account_id(); let dfvk = T::test_account_fvk(&st); let uaddr = st @@ -1299,10 +1308,10 @@ pub(crate) fn birthday_in_anchor_shard() { st.scan_cached_blocks(birthday.height() + 5, 20); // Verify that the received note is not considered spendable - let account = st.test_account().unwrap(); + let account = st.test_account().unwrap().0.account_id(); let spendable = T::select_spendable_notes( &st, - account.0, + account, NonNegativeAmount::const_from_u64(300000), received_tx_height + 10, &[], @@ -1317,7 +1326,7 @@ pub(crate) fn birthday_in_anchor_shard() { // Verify that the received note is now considered spendable let spendable = T::select_spendable_notes( &st, - account.0, + account, NonNegativeAmount::const_from_u64(300000), received_tx_height + 10, &[], @@ -1333,7 +1342,8 @@ pub(crate) fn checkpoint_gaps() { .with_test_account(AccountBirthday::from_sapling_activation) .build(); - let (account, usk, birthday) = st.test_account().unwrap(); + let (sources, usk, birthday) = st.test_account().unwrap(); + let account = sources.account_id(); let dfvk = T::test_account_fvk(&st); // Generate a block with funds belonging to our wallet. diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs index 5eb067d271..77b2d2b990 100644 --- a/zcash_client_sqlite/src/wallet.rs +++ b/zcash_client_sqlite/src/wallet.rs @@ -2404,7 +2404,7 @@ mod tests { let st = TestBuilder::new() .with_test_account(AccountBirthday::from_sapling_activation) .build(); - let account = st.test_account().unwrap(); + let account = st.test_account().unwrap().0.account_id(); // The account should have no summary information assert_eq!(st.get_wallet_summary(0), None); @@ -2418,11 +2418,11 @@ mod tests { ); // The default address is set for the test account - assert_matches!(st.wallet().get_current_address(account.0), Ok(Some(_))); + assert_matches!(st.wallet().get_current_address(account), Ok(Some(_))); // No default address is set for an un-initialized account assert_matches!( - st.wallet().get_current_address(AccountId(account.0 .0 + 1)), + st.wallet().get_current_address(AccountId(account.0 + 1)), Ok(None) ); } @@ -2436,10 +2436,10 @@ mod tests { .with_test_account(AccountBirthday::from_sapling_activation) .build(); - let (account_id, _, _) = st.test_account().unwrap(); + let (sources, _, _) = st.test_account().unwrap(); let uaddr = st .wallet() - .get_current_address(account_id) + .get_current_address(sources.account_id()) .unwrap() .unwrap(); let taddr = uaddr.transparent().unwrap(); @@ -2447,7 +2447,7 @@ mod tests { let height_1 = BlockHeight::from_u32(12345); let bal_absent = st .wallet() - .get_transparent_balances(account_id, height_1) + .get_transparent_balances(sources.account_id(), height_1) .unwrap(); assert!(bal_absent.is_empty()); @@ -2499,7 +2499,7 @@ mod tests { ); assert_matches!( - st.wallet().get_transparent_balances(account_id, height_2), + st.wallet().get_transparent_balances(sources.account_id(), height_2), Ok(h) if h.get(taddr) == Some(&value) ); @@ -2524,7 +2524,7 @@ mod tests { let st = TestBuilder::new() .with_test_account(AccountBirthday::from_sapling_activation) .build(); - let account_id = st.test_account().unwrap().0; + let account_id = st.test_account().unwrap().0.account_id(); let account_parameters = get_account(st.wallet(), account_id).unwrap().unwrap(); let expected_account_index = zip32::AccountId::try_from(0).unwrap(); @@ -2544,10 +2544,10 @@ mod tests { .with_test_account(AccountBirthday::from_sapling_activation) .build(); - let (account_id, usk, _) = st.test_account().unwrap(); + let (sources, usk, _) = st.test_account().unwrap(); let uaddr = st .wallet() - .get_current_address(account_id) + .get_current_address(sources.account_id()) .unwrap() .unwrap(); let taddr = uaddr.transparent().unwrap(); @@ -2569,14 +2569,17 @@ mod tests { .get_wallet_summary(min_confirmations) .unwrap() .unwrap(); - let balance = summary.account_balances().get(&account_id).unwrap(); + let balance = summary + .account_balances() + .get(&sources.account_id()) + .unwrap(); assert_eq!(balance.unshielded(), expected); // Check the older APIs for consistency. let max_height = st.wallet().chain_height().unwrap().unwrap() + 1 - min_confirmations; assert_eq!( st.wallet() - .get_transparent_balances(account_id, max_height) + .get_transparent_balances(sources.account_id(), max_height) .unwrap() .get(taddr) .cloned() diff --git a/zcash_keys/CHANGELOG.md b/zcash_keys/CHANGELOG.md index 79654d4c95..38cc1b0a1f 100644 --- a/zcash_keys/CHANGELOG.md +++ b/zcash_keys/CHANGELOG.md @@ -7,7 +7,9 @@ and this library adheres to Rust's notion of ## [Unreleased] ### Added -- `zcash_keys::keys::HdSeedFingerprint` +- `zcash_keys::keys`: + - `HdSeedFingerprint` + - `UnifiedSpendingKey::{has_orchard, has_sapling, has_transparent}` - `zcash_keys::address::Address::has_receiver` - `impl Display for zcash_keys::keys::AddressGenerationError` - `impl std::error::Error for zcash_keys::keys::AddressGenerationError` diff --git a/zcash_keys/src/keys.rs b/zcash_keys/src/keys.rs index 27dfb8393b..779cbe4f27 100644 --- a/zcash_keys/src/keys.rs +++ b/zcash_keys/src/keys.rs @@ -269,18 +269,45 @@ impl UnifiedSpendingKey { &self.transparent } + /// Returns whether the key provides transparent P2PKH spending capability. + pub fn has_transparent(&self) -> bool { + #[cfg(feature = "transparent-inputs")] + return true; + + #[cfg(not(feature = "transparent-inputs"))] + return false; + } + /// Returns the Sapling extended spending key component of this unified spending key. #[cfg(feature = "sapling")] pub fn sapling(&self) -> &sapling::ExtendedSpendingKey { &self.sapling } + /// Returns whether the key provides Sapling spending capability. + pub fn has_sapling(&self) -> bool { + #[cfg(feature = "sapling")] + return true; + + #[cfg(not(feature = "sapling"))] + return false; + } + /// Returns the Orchard spending key component of this unified spending key. #[cfg(feature = "orchard")] pub fn orchard(&self) -> &orchard::keys::SpendingKey { &self.orchard } + /// Returns whether the key provides Orchard spending capability. + pub fn has_orchard(&self) -> bool { + #[cfg(feature = "orchard")] + return true; + + #[cfg(not(feature = "orchard"))] + return false; + } + /// Returns a binary encoding of this key suitable for decoding with [`decode`]. /// /// The encoded form of a unified spending key is only intended for use