Merge remote-tracking branch 'upstream/main' into uivk

This commit is contained in:
Andrew Arnott 2024-03-13 19:17:16 -06:00
commit 1770c2ec5f
No known key found for this signature in database
GPG Key ID: 48F18646D6868924
9 changed files with 511 additions and 452 deletions

View File

@ -16,6 +16,7 @@ and this library adheres to Rust's notion of
- `Account`
- `AccountBalance::with_orchard_balance_mut`
- `AccountBirthday::orchard_frontier`
- `AccountKind`
- `BlockMetadata::orchard_tree_size`
- `DecryptedTransaction::{new, tx(), orchard_outputs()}`
- `ScannedBlock::orchard`
@ -24,12 +25,14 @@ and this library adheres to Rust's notion of
- `ORCHARD_SHARD_HEIGHT`
- `BlockMetadata::orchard_tree_size`
- `WalletSummary::next_orchard_subtree_index`
- `chain::ChainState`
- `chain::ScanSummary::{spent_orchard_note_count, received_orchard_note_count}`
- `zcash_client_backend::fees`:
- `orchard`
- `ChangeValue::orchard`
- `zcash_client_backend::proto`:
- `service::TreeState::orchard_tree`
- `service::TreeState::to_chain_state`
- `impl TryFrom<&CompactOrchardAction> for CompactAction`
- `CompactOrchardAction::{cmx, nf, ephemeral_key}`
- `zcash_client_backend::scanning`:
@ -51,10 +54,11 @@ and this library adheres to Rust's notion of
- Arguments to `ScannedBlock::from_parts` have changed.
- Changes to the `WalletRead` trait:
- Added `Account` associated type.
- Added `get_account` method.
- Added `get_derived_account` method.
- `get_account_for_ufvk` now returns `Self::Account` instead of a bare
`AccountId`.
- Added `get_orchard_nullifiers` method.
- `get_account_for_ufvk` now returns an `Self::Account` instead of a bare
`AccountId`
- Added `get_seed_account` method.
- 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

View File

@ -315,12 +315,32 @@ impl AccountBalance {
}
}
/// The kinds of accounts supported by `zcash_client_backend`.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum AccountKind {
/// An account derived from a known seed.
Derived {
seed_fingerprint: HdSeedFingerprint,
account_index: zip32::AccountId,
},
/// An account imported from a viewing key.
Imported,
}
/// A set of capabilities that a client account must provide.
pub trait Account<AccountId: Copy> {
/// Returns the unique identifier for the account.
fn id(&self) -> AccountId;
/// Returns whether this account is derived or imported, and the derivation parameters
/// if applicable.
fn kind(&self) -> AccountKind;
/// Returns the UFVK that the wallet backend has stored for the account, if any.
///
/// Accounts for which this returns `None` cannot be used in wallet contexts, because
/// they are unable to maintain an accurate balance.
fn ufvk(&self) -> Option<&UnifiedFullViewingKey>;
}
@ -329,6 +349,10 @@ impl<A: Copy> Account<A> for (A, UnifiedFullViewingKey) {
self.0
}
fn kind(&self) -> AccountKind {
AccountKind::Imported
}
fn ufvk(&self) -> Option<&UnifiedFullViewingKey> {
Some(&self.1)
}
@ -339,6 +363,10 @@ impl<A: Copy> Account<A> for (A, Option<UnifiedFullViewingKey>) {
self.0
}
fn kind(&self) -> AccountKind {
AccountKind::Imported
}
fn ufvk(&self) -> Option<&UnifiedFullViewingKey> {
self.1.as_ref()
}
@ -543,6 +571,23 @@ pub trait WalletRead {
/// The concrete account type used by this wallet backend.
type Account: Account<Self::AccountId>;
/// Returns a vector with the IDs of all accounts known to this wallet.
fn get_account_ids(&self) -> Result<Vec<Self::AccountId>, Self::Error>;
/// Returns the account corresponding to the given ID, if any.
fn get_account(
&self,
account_id: Self::AccountId,
) -> Result<Option<Self::Account>, Self::Error>;
/// Returns the account corresponding to a given [`HdSeedFingerprint`] and
/// [`zip32::AccountId`], if any.
fn get_derived_account(
&self,
seed: &HdSeedFingerprint,
account_id: zip32::AccountId,
) -> Result<Option<Self::Account>, Self::Error>;
/// Verifies that the given seed corresponds to the viewing key for the specified account.
///
/// Returns:
@ -558,12 +603,50 @@ pub trait WalletRead {
seed: &SecretVec<u8>,
) -> Result<bool, Self::Error>;
/// Returns the account corresponding to a given [`UnifiedFullViewingKey`], if any.
fn get_account_for_ufvk(
&self,
ufvk: &UnifiedFullViewingKey,
) -> Result<Option<Self::Account>, Self::Error>;
/// Returns the most recently generated unified address for the specified account, if the
/// account identifier specified refers to a valid account for this wallet.
///
/// This will return `Ok(None)` if the account identifier does not correspond to a known
/// account.
fn get_current_address(
&self,
account: Self::AccountId,
) -> Result<Option<UnifiedAddress>, Self::Error>;
/// Returns the birthday height for the given account, or an error if the account is not known
/// to the wallet.
fn get_account_birthday(&self, account: Self::AccountId) -> Result<BlockHeight, Self::Error>;
/// Returns the birthday height for the wallet.
///
/// This returns the earliest birthday height among accounts maintained by this wallet,
/// or `Ok(None)` if the wallet has no initialized accounts.
fn get_wallet_birthday(&self) -> Result<Option<BlockHeight>, Self::Error>;
/// Returns the wallet balances and sync status for an account given the specified minimum
/// number of confirmations, or `Ok(None)` if the wallet has no balance data available.
fn get_wallet_summary(
&self,
min_confirmations: u32,
) -> Result<Option<WalletSummary<Self::AccountId>>, Self::Error>;
/// Returns the height of the chain as known to the wallet as of the most recent call to
/// [`WalletWrite::update_chain_tip`].
///
/// This will return `Ok(None)` if the height of the current consensus chain tip is unknown.
fn chain_height(&self) -> Result<Option<BlockHeight>, Self::Error>;
/// Returns the block hash for the block at the given height, if the
/// associated block data is available. Returns `Ok(None)` if the hash
/// is not found in the database.
fn get_block_hash(&self, block_height: BlockHeight) -> Result<Option<BlockHash>, Self::Error>;
/// Returns the available block metadata for the block at the specified height, if any.
fn block_metadata(&self, height: BlockHeight) -> Result<Option<BlockMetadata>, Self::Error>;
@ -576,6 +659,11 @@ pub trait WalletRead {
/// block.
fn block_fully_scanned(&self) -> Result<Option<BlockMetadata>, Self::Error>;
/// Returns the block height and hash for the block at the maximum scanned block height.
///
/// This will return `Ok(None)` if no blocks have been scanned.
fn get_max_height_hash(&self) -> Result<Option<(BlockHeight, BlockHash)>, Self::Error>;
/// Returns block metadata for the maximum height that the wallet has scanned.
///
/// If the wallet is fully synced, this will be equivalent to `block_fully_scanned`;
@ -614,66 +702,15 @@ pub trait WalletRead {
/// Returns the minimum block height corresponding to an unspent note in the wallet.
fn get_min_unspent_height(&self) -> Result<Option<BlockHeight>, Self::Error>;
/// Returns the block hash for the block at the given height, if the
/// associated block data is available. Returns `Ok(None)` if the hash
/// is not found in the database.
fn get_block_hash(&self, block_height: BlockHeight) -> Result<Option<BlockHash>, Self::Error>;
/// Returns the block height and hash for the block at the maximum scanned block height.
///
/// This will return `Ok(None)` if no blocks have been scanned.
fn get_max_height_hash(&self) -> Result<Option<(BlockHeight, BlockHash)>, Self::Error>;
/// Returns the block height in which the specified transaction was mined, or `Ok(None)` if the
/// transaction is not in the main chain.
fn get_tx_height(&self, txid: TxId) -> Result<Option<BlockHeight>, Self::Error>;
/// Returns the birthday height for the wallet.
///
/// This returns the earliest birthday height among accounts maintained by this wallet,
/// or `Ok(None)` if the wallet has no initialized accounts.
fn get_wallet_birthday(&self) -> Result<Option<BlockHeight>, Self::Error>;
/// Returns the birthday height for the given account, or an error if the account is not known
/// to the wallet.
fn get_account_birthday(&self, account: Self::AccountId) -> Result<BlockHeight, Self::Error>;
/// Returns the most recently generated unified address for the specified account, if the
/// account identifier specified refers to a valid account for this wallet.
///
/// This will return `Ok(None)` if the account identifier does not correspond to a known
/// account.
fn get_current_address(
&self,
account: Self::AccountId,
) -> Result<Option<UnifiedAddress>, Self::Error>;
/// Returns all unified full viewing keys known to this wallet.
fn get_unified_full_viewing_keys(
&self,
) -> Result<HashMap<Self::AccountId, UnifiedFullViewingKey>, Self::Error>;
/// Returns the account corresponding to a given [`UnifiedFullViewingKey`], if any.
fn get_account_for_ufvk(
&self,
ufvk: &UnifiedFullViewingKey,
) -> Result<Option<Self::Account>, Self::Error>;
/// Returns the account corresponding to a given [`HdSeedFingerprint`] and
/// [`zip32::AccountId`], if any.
fn get_seed_account(
&self,
seed: &HdSeedFingerprint,
account_id: zip32::AccountId,
) -> Result<Option<Self::Account>, Self::Error>;
/// Returns the wallet balances and sync status for an account given the specified minimum
/// number of confirmations, or `Ok(None)` if the wallet has no balance data available.
fn get_wallet_summary(
&self,
min_confirmations: u32,
) -> Result<Option<WalletSummary<Self::AccountId>>, Self::Error>;
/// Returns the memo for a note.
///
/// Returns `Ok(None)` if the note is known to the wallet but memo data has not yet been
@ -724,9 +761,6 @@ pub trait WalletRead {
) -> Result<HashMap<TransparentAddress, NonNegativeAmount>, Self::Error> {
Ok(HashMap::new())
}
/// Returns a vector with the IDs of all accounts known to this wallet.
fn get_account_ids(&self) -> Result<Vec<Self::AccountId>, Self::Error>;
}
/// Metadata describing the sizes of the zcash note commitment trees as of a particular block.
@ -1299,6 +1333,15 @@ pub trait WalletWrite: WalletRead {
request: UnifiedAddressRequest,
) -> Result<Option<UnifiedAddress>, Self::Error>;
/// Updates the wallet's view of the blockchain.
///
/// This method is used to provide the wallet with information about the state of the
/// blockchain, and detect any previously scanned data that needs to be re-validated
/// before proceeding with scanning. It should be called at wallet startup prior to calling
/// [`WalletRead::suggest_scan_ranges`] in order to provide the wallet with the information it
/// needs to correctly prioritize scanning operations.
fn update_chain_tip(&mut self, tip_height: BlockHeight) -> Result<(), Self::Error>;
/// Updates the state of the wallet database by persisting the provided block information,
/// along with the note commitments that were detected when scanning the block for transactions
/// pertaining to this wallet.
@ -1313,14 +1356,11 @@ pub trait WalletWrite: WalletRead {
blocks: Vec<ScannedBlock<Self::AccountId>>,
) -> Result<(), Self::Error>;
/// Updates the wallet's view of the blockchain.
///
/// This method is used to provide the wallet with information about the state of the
/// blockchain, and detect any previously scanned data that needs to be re-validated
/// before proceeding with scanning. It should be called at wallet startup prior to calling
/// [`WalletRead::suggest_scan_ranges`] in order to provide the wallet with the information it
/// needs to correctly prioritize scanning operations.
fn update_chain_tip(&mut self, tip_height: BlockHeight) -> Result<(), Self::Error>;
/// Adds a transparent UTXO received by the wallet to the data store.
fn put_received_transparent_utxo(
&mut self,
output: &WalletTransparentOutput,
) -> Result<Self::UtxoRef, Self::Error>;
/// Caches a decrypted transaction in the persistent wallet store.
fn store_decrypted_tx(
@ -1349,12 +1389,6 @@ pub trait WalletWrite: WalletRead {
///
/// There may be restrictions on heights to which it is possible to truncate.
fn truncate_to_height(&mut self, block_height: BlockHeight) -> Result<(), Self::Error>;
/// Adds a transparent UTXO received by the wallet to the data store.
fn put_received_transparent_utxo(
&mut self,
output: &WalletTransparentOutput,
) -> Result<Self::UtxoRef, Self::Error>;
}
/// This trait describes a capability for manipulating wallet note commitment trees.
@ -1521,6 +1555,25 @@ pub mod testing {
type AccountId = u32;
type Account = (Self::AccountId, UnifiedFullViewingKey);
fn get_account_ids(&self) -> Result<Vec<Self::AccountId>, Self::Error> {
Ok(Vec::new())
}
fn get_account(
&self,
_account_id: Self::AccountId,
) -> Result<Option<Self::Account>, Self::Error> {
Ok(None)
}
fn get_derived_account(
&self,
_seed: &HdSeedFingerprint,
_account_id: zip32::AccountId,
) -> Result<Option<Self::Account>, Self::Error> {
Ok(None)
}
fn validate_seed(
&self,
_account_id: Self::AccountId,
@ -1529,10 +1582,49 @@ pub mod testing {
Ok(false)
}
fn get_account_for_ufvk(
&self,
_ufvk: &UnifiedFullViewingKey,
) -> Result<Option<Self::Account>, Self::Error> {
Ok(None)
}
fn get_current_address(
&self,
_account: Self::AccountId,
) -> Result<Option<UnifiedAddress>, Self::Error> {
Ok(None)
}
fn get_account_birthday(
&self,
_account: Self::AccountId,
) -> Result<BlockHeight, Self::Error> {
Err(())
}
fn get_wallet_birthday(&self) -> Result<Option<BlockHeight>, Self::Error> {
Ok(None)
}
fn get_wallet_summary(
&self,
_min_confirmations: u32,
) -> Result<Option<WalletSummary<Self::AccountId>>, Self::Error> {
Ok(None)
}
fn chain_height(&self) -> Result<Option<BlockHeight>, Self::Error> {
Ok(None)
}
fn get_block_hash(
&self,
_block_height: BlockHeight,
) -> Result<Option<BlockHash>, Self::Error> {
Ok(None)
}
fn block_metadata(
&self,
_height: BlockHeight,
@ -1544,6 +1636,10 @@ pub mod testing {
Ok(None)
}
fn get_max_height_hash(&self) -> Result<Option<(BlockHeight, BlockHash)>, Self::Error> {
Ok(None)
}
fn block_max_scanned(&self) -> Result<Option<BlockMetadata>, Self::Error> {
Ok(None)
}
@ -1563,67 +1659,16 @@ pub mod testing {
Ok(None)
}
fn get_block_hash(
&self,
_block_height: BlockHeight,
) -> Result<Option<BlockHash>, Self::Error> {
Ok(None)
}
fn get_max_height_hash(&self) -> Result<Option<(BlockHeight, BlockHash)>, Self::Error> {
Ok(None)
}
fn get_tx_height(&self, _txid: TxId) -> Result<Option<BlockHeight>, Self::Error> {
Ok(None)
}
fn get_wallet_birthday(&self) -> Result<Option<BlockHeight>, Self::Error> {
Ok(None)
}
fn get_account_birthday(
&self,
_account: Self::AccountId,
) -> Result<BlockHeight, Self::Error> {
Err(())
}
fn get_current_address(
&self,
_account: Self::AccountId,
) -> Result<Option<UnifiedAddress>, Self::Error> {
Ok(None)
}
fn get_unified_full_viewing_keys(
&self,
) -> Result<HashMap<Self::AccountId, UnifiedFullViewingKey>, Self::Error> {
Ok(HashMap::new())
}
fn get_account_for_ufvk(
&self,
_ufvk: &UnifiedFullViewingKey,
) -> Result<Option<Self::Account>, Self::Error> {
Ok(None)
}
fn get_seed_account(
&self,
_seed: &HdSeedFingerprint,
_account_id: zip32::AccountId,
) -> Result<Option<Self::Account>, Self::Error> {
Ok(None)
}
fn get_wallet_summary(
&self,
_min_confirmations: u32,
) -> Result<Option<WalletSummary<Self::AccountId>>, Self::Error> {
Ok(None)
}
fn get_memo(&self, _id_note: NoteId) -> Result<Option<Memo>, Self::Error> {
Ok(None)
}
@ -1664,10 +1709,6 @@ pub mod testing {
) -> Result<HashMap<TransparentAddress, NonNegativeAmount>, Self::Error> {
Ok(HashMap::new())
}
fn get_account_ids(&self) -> Result<Vec<Self::AccountId>, Self::Error> {
Ok(Vec::new())
}
}
impl WalletWrite for MockWalletDb {

View File

@ -20,7 +20,7 @@ use zcash_primitives::{
};
use crate::{
data_api::InputSource,
data_api::{chain::ChainState, InputSource},
fees::{ChangeValue, TransactionBalance},
proposal::{Proposal, ProposalError, ShieldedInputs, Step, StepOutput, StepOutputIndex},
zip321::{TransactionRequest, Zip321Error},
@ -290,6 +290,20 @@ impl service::TreeState {
&orchard_tree_bytes[..],
)
}
/// Parses this tree state into a [`ChainState`] for use with [`scan_cached_blocks`].
///
/// [`scan_cached_blocks`]: crate::data_api::chain::scan_cached_blocks
pub fn to_chain_state(&self) -> io::Result<ChainState> {
Ok(ChainState::new(
self.height
.try_into()
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid block height"))?,
self.sapling_tree()?.to_frontier(),
#[cfg(feature = "orchard")]
self.orchard_tree()?.to_frontier(),
))
}
}
/// Constant for the V1 proposal serialization version.

View File

@ -11,6 +11,7 @@ and this library adheres to Rust's notion of
- A new `orchard` feature flag has been added to make it possible to
build client code without `orchard` dependendencies.
- `zcash_client_sqlite::AccountId`
- `zcash_client_sqlite::wallet::Account`
- `impl From<zcash_keys::keys::AddressGenerationError> for SqliteClientError`
### Changed

View File

@ -60,9 +60,9 @@ use zcash_client_backend::{
self,
chain::{BlockSource, ChainState, CommitmentTreeRoot},
scanning::{ScanPriority, ScanRange},
AccountBirthday, BlockMetadata, DecryptedTransaction, InputSource, NullifierQuery,
ScannedBlock, SentTransaction, WalletCommitmentTrees, WalletRead, WalletSummary,
WalletWrite, SAPLING_SHARD_HEIGHT,
Account, AccountBirthday, AccountKind, BlockMetadata, DecryptedTransaction, InputSource,
NullifierQuery, ScannedBlock, SentTransaction, WalletCommitmentTrees, WalletRead,
WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT,
},
keys::{
AddressGenerationError, UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey,
@ -100,7 +100,7 @@ pub mod error;
pub mod wallet;
use wallet::{
commitment_tree::{self, put_shard_roots},
Account, HdSeedAccount, SubtreeScanProgress,
SubtreeScanProgress,
};
#[cfg(test)]
@ -287,24 +287,46 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> InputSource for
impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for WalletDb<C, P> {
type Error = SqliteClientError;
type AccountId = AccountId;
type Account = (AccountId, Option<UnifiedFullViewingKey>);
type Account = wallet::Account;
fn get_account_ids(&self) -> Result<Vec<AccountId>, Self::Error> {
wallet::get_account_ids(self.conn.borrow())
}
fn get_account(
&self,
account_id: Self::AccountId,
) -> Result<Option<Self::Account>, Self::Error> {
wallet::get_account(self.conn.borrow(), &self.params, account_id)
}
fn get_derived_account(
&self,
seed: &HdSeedFingerprint,
account_id: zip32::AccountId,
) -> Result<Option<Self::Account>, Self::Error> {
wallet::get_derived_account(self.conn.borrow(), &self.params, seed, account_id)
}
fn validate_seed(
&self,
account_id: Self::AccountId,
seed: &SecretVec<u8>,
) -> Result<bool, Self::Error> {
if let Some(account) = wallet::get_account(self, account_id)? {
if let Account::Zip32(hdaccount) = account {
let seed_fingerprint_match =
HdSeedFingerprint::from_seed(seed) == *hdaccount.hd_seed_fingerprint();
if let Some(account) = self.get_account(account_id)? {
if let AccountKind::Derived {
seed_fingerprint,
account_index,
} = account.kind()
{
let seed_fingerprint_match = HdSeedFingerprint::from_seed(seed) == seed_fingerprint;
let usk = UnifiedSpendingKey::from_seed(
&self.params,
&seed.expose_secret()[..],
hdaccount.account_index(),
account_index,
)
.map_err(|_| SqliteClientError::KeyDerivationError(hdaccount.account_index()))?;
.map_err(|_| SqliteClientError::KeyDerivationError(account_index))?;
// Keys are not comparable with `Eq`, but addresses are, so we derive what should
// be equivalent addresses for each key and use those to check for key equality.
@ -314,7 +336,7 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for W
Ok(usk
.to_unified_full_viewing_key()
.default_address(ua_request)?
== hdaccount.ufvk().default_address(ua_request)?)
== account.default_address(ua_request)?)
},
)?;
@ -333,12 +355,53 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for W
}
}
fn get_account_for_ufvk(
&self,
ufvk: &UnifiedFullViewingKey,
) -> Result<Option<Self::Account>, Self::Error> {
wallet::get_account_for_ufvk(self.conn.borrow(), &self.params, ufvk)
}
fn get_current_address(
&self,
account: AccountId,
) -> Result<Option<UnifiedAddress>, Self::Error> {
wallet::get_current_address(self.conn.borrow(), &self.params, account)
.map(|res| res.map(|(addr, _)| addr))
}
fn get_account_birthday(&self, account: AccountId) -> Result<BlockHeight, Self::Error> {
wallet::account_birthday(self.conn.borrow(), account).map_err(SqliteClientError::from)
}
fn get_wallet_birthday(&self) -> Result<Option<BlockHeight>, Self::Error> {
wallet::wallet_birthday(self.conn.borrow()).map_err(SqliteClientError::from)
}
fn get_wallet_summary(
&self,
min_confirmations: u32,
) -> Result<Option<WalletSummary<Self::AccountId>>, Self::Error> {
// This will return a runtime error if we call `get_wallet_summary` from two
// threads at the same time, as transactions cannot nest.
wallet::get_wallet_summary(
&self.conn.borrow().unchecked_transaction()?,
&self.params,
min_confirmations,
&SubtreeScanProgress,
)
}
fn chain_height(&self) -> Result<Option<BlockHeight>, Self::Error> {
wallet::scan_queue_extrema(self.conn.borrow())
.map(|h| h.map(|range| *range.end()))
.map_err(SqliteClientError::from)
}
fn get_block_hash(&self, block_height: BlockHeight) -> Result<Option<BlockHash>, Self::Error> {
wallet::get_block_hash(self.conn.borrow(), block_height).map_err(SqliteClientError::from)
}
fn block_metadata(&self, height: BlockHeight) -> Result<Option<BlockMetadata>, Self::Error> {
wallet::block_metadata(self.conn.borrow(), &self.params, height)
}
@ -347,6 +410,10 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for W
wallet::block_fully_scanned(self.conn.borrow(), &self.params)
}
fn get_max_height_hash(&self) -> Result<Option<(BlockHeight, BlockHash)>, Self::Error> {
wallet::get_max_height_hash(self.conn.borrow()).map_err(SqliteClientError::from)
}
fn block_max_scanned(&self) -> Result<Option<BlockMetadata>, Self::Error> {
wallet::block_max_scanned(self.conn.borrow(), &self.params)
}
@ -368,69 +435,16 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for W
wallet::get_min_unspent_height(self.conn.borrow()).map_err(SqliteClientError::from)
}
fn get_block_hash(&self, block_height: BlockHeight) -> Result<Option<BlockHash>, Self::Error> {
wallet::get_block_hash(self.conn.borrow(), block_height).map_err(SqliteClientError::from)
}
fn get_max_height_hash(&self) -> Result<Option<(BlockHeight, BlockHash)>, Self::Error> {
wallet::get_max_height_hash(self.conn.borrow()).map_err(SqliteClientError::from)
}
fn get_tx_height(&self, txid: TxId) -> Result<Option<BlockHeight>, Self::Error> {
wallet::get_tx_height(self.conn.borrow(), txid).map_err(SqliteClientError::from)
}
fn get_wallet_birthday(&self) -> Result<Option<BlockHeight>, Self::Error> {
wallet::wallet_birthday(self.conn.borrow()).map_err(SqliteClientError::from)
}
fn get_account_birthday(&self, account: AccountId) -> Result<BlockHeight, Self::Error> {
wallet::account_birthday(self.conn.borrow(), account).map_err(SqliteClientError::from)
}
fn get_current_address(
&self,
account: AccountId,
) -> Result<Option<UnifiedAddress>, Self::Error> {
wallet::get_current_address(self.conn.borrow(), &self.params, account)
.map(|res| res.map(|(addr, _)| addr))
}
fn get_unified_full_viewing_keys(
&self,
) -> Result<HashMap<AccountId, UnifiedFullViewingKey>, Self::Error> {
wallet::get_unified_full_viewing_keys(self.conn.borrow(), &self.params)
}
fn get_account_for_ufvk(
&self,
ufvk: &UnifiedFullViewingKey,
) -> Result<Option<Self::Account>, Self::Error> {
wallet::get_account_for_ufvk(self.conn.borrow(), &self.params, ufvk)
}
fn get_seed_account(
&self,
seed: &HdSeedFingerprint,
account_id: zip32::AccountId,
) -> Result<Option<Self::Account>, Self::Error> {
wallet::get_seed_account(self.conn.borrow(), &self.params, seed, account_id)
}
fn get_wallet_summary(
&self,
min_confirmations: u32,
) -> Result<Option<WalletSummary<Self::AccountId>>, Self::Error> {
// This will return a runtime error if we call `get_wallet_summary` from two
// threads at the same time, as transactions cannot nest.
wallet::get_wallet_summary(
&self.conn.borrow().unchecked_transaction()?,
&self.params,
min_confirmations,
&SubtreeScanProgress,
)
}
fn get_memo(&self, note_id: NoteId) -> Result<Option<Memo>, Self::Error> {
let sent_memo = wallet::get_sent_memo(self.conn.borrow(), note_id)?;
if sent_memo.is_some() {
@ -475,10 +489,6 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for W
) -> Result<HashMap<TransparentAddress, NonNegativeAmount>, Self::Error> {
wallet::get_transparent_balances(self.conn.borrow(), &self.params, account, max_height)
}
fn get_account_ids(&self) -> Result<Vec<AccountId>, Self::Error> {
wallet::get_account_ids(self.conn.borrow())
}
}
impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P> {
@ -490,8 +500,8 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
birthday: AccountBirthday,
) -> Result<(AccountId, UnifiedSpendingKey), Self::Error> {
self.transactionally(|wdb| {
let seed_id = HdSeedFingerprint::from_seed(seed);
let account_index = wallet::max_zip32_account_index(wdb.conn.0, &seed_id)?
let seed_fingerprint = HdSeedFingerprint::from_seed(seed);
let account_index = wallet::max_zip32_account_index(wdb.conn.0, &seed_fingerprint)?
.map(|a| a.next().ok_or(SqliteClientError::AccountIdOutOfRange))
.transpose()?
.unwrap_or(zip32::AccountId::ZERO);
@ -501,8 +511,16 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
.map_err(|_| SqliteClientError::KeyDerivationError(account_index))?;
let ufvk = usk.to_unified_full_viewing_key();
let account = Account::Zip32(HdSeedAccount::new(seed_id, account_index, ufvk));
let account_id = wallet::add_account(wdb.conn.0, &wdb.params, account, birthday)?;
let account_id = wallet::add_account(
wdb.conn.0,
&wdb.params,
AccountKind::Derived {
seed_fingerprint,
account_index,
},
wallet::ViewingKey::Full(Box::new(ufvk)),
birthday,
)?;
Ok((account_id, usk))
})
@ -544,6 +562,13 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
)
}
fn update_chain_tip(&mut self, tip_height: BlockHeight) -> Result<(), Self::Error> {
let tx = self.conn.transaction()?;
wallet::scanning::update_chain_tip(&tx, &self.params, tip_height)?;
tx.commit()?;
Ok(())
}
#[tracing::instrument(skip_all, fields(height = blocks.first().map(|b| u32::from(b.height()))))]
#[allow(clippy::type_complexity)]
fn put_blocks(
@ -900,11 +925,17 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
})
}
fn update_chain_tip(&mut self, tip_height: BlockHeight) -> Result<(), Self::Error> {
let tx = self.conn.transaction()?;
wallet::scanning::update_chain_tip(&tx, &self.params, tip_height)?;
tx.commit()?;
Ok(())
fn put_received_transparent_utxo(
&mut self,
_output: &WalletTransparentOutput,
) -> Result<Self::UtxoRef, Self::Error> {
#[cfg(feature = "transparent-inputs")]
return wallet::put_received_transparent_utxo(&self.conn, &self.params, _output);
#[cfg(not(feature = "transparent-inputs"))]
panic!(
"The wallet must be compiled with the transparent-inputs feature to use this method."
);
}
fn store_decrypted_tx(
@ -1162,19 +1193,6 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
wallet::truncate_to_height(wdb.conn.0, &wdb.params, block_height)
})
}
fn put_received_transparent_utxo(
&mut self,
_output: &WalletTransparentOutput,
) -> Result<Self::UtxoRef, Self::Error> {
#[cfg(feature = "transparent-inputs")]
return wallet::put_received_transparent_utxo(&self.conn, &self.params, _output);
#[cfg(not(feature = "transparent-inputs"))]
panic!(
"The wallet must be compiled with the transparent-inputs feature to use this method."
);
}
}
impl<P: consensus::Parameters> WalletCommitmentTrees for WalletDb<rusqlite::Connection, P> {

View File

@ -67,7 +67,7 @@
use incrementalmerkletree::Retention;
use rusqlite::{self, named_params, params, OptionalExtension};
use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree};
use std::borrow::Borrow;
use std::collections::HashMap;
use std::convert::TryFrom;
use std::io::{self, Cursor};
@ -83,7 +83,7 @@ use zcash_client_backend::{
address::{Address, UnifiedAddress},
data_api::{
scanning::{ScanPriority, ScanRange},
AccountBalance, AccountBirthday, BlockMetadata, Ratio, SentTransactionOutput,
AccountBalance, AccountBirthday, AccountKind, BlockMetadata, Ratio, SentTransactionOutput,
WalletSummary, SAPLING_SHARD_HEIGHT,
},
encoding::AddressCodec,
@ -140,84 +140,59 @@ pub(crate) mod scanning;
pub(crate) const BLOCK_SAPLING_FRONTIER_ABSENT: &[u8] = &[0x0];
/// This tracks the allowed values of the `account_type` column of the `accounts` table
/// and should not be made public.
enum AccountType {
Zip32,
Imported,
fn parse_account_kind(
account_kind: u32,
hd_seed_fingerprint: Option<[u8; 32]>,
hd_account_index: Option<u32>,
) -> Result<AccountKind, SqliteClientError> {
match (account_kind, hd_seed_fingerprint, hd_account_index) {
(0, Some(seed_fp), Some(account_index)) => Ok(AccountKind::Derived {
seed_fingerprint: HdSeedFingerprint::from_bytes(seed_fp),
account_index: zip32::AccountId::try_from(account_index).map_err(|_| {
SqliteClientError::CorruptedData(
"ZIP-32 account ID from wallet DB is out of range.".to_string(),
)
})?,
}),
(1, None, None) => Ok(AccountKind::Imported),
(0, None, None) | (1, Some(_), Some(_)) => Err(SqliteClientError::CorruptedData(
"Wallet DB account_kind constraint violated".to_string(),
)),
(_, _, _) => Err(SqliteClientError::CorruptedData(
"Unrecognized account_kind".to_string(),
)),
}
}
impl TryFrom<u32> for AccountType {
type Error = ();
fn try_from(value: u32) -> Result<Self, Self::Error> {
fn account_kind_code(value: AccountKind) -> u32 {
match value {
0 => Ok(AccountType::Zip32),
1 => Ok(AccountType::Imported),
_ => Err(()),
}
AccountKind::Derived { .. } => 0,
AccountKind::Imported => 1,
}
}
impl From<AccountType> for u32 {
fn from(value: AccountType) -> Self {
match value {
AccountType::Zip32 => 0,
AccountType::Imported => 1,
}
}
}
/// Describes the key inputs and UFVK for an account that was derived from a ZIP-32 HD seed and account index.
/// The viewing key that an [`Account`] has available to it.
#[derive(Debug, Clone)]
pub(crate) struct HdSeedAccount(
HdSeedFingerprint,
zip32::AccountId,
Box<UnifiedFullViewingKey>,
);
impl HdSeedAccount {
pub fn new(
hd_seed_fingerprint: HdSeedFingerprint,
account_index: zip32::AccountId,
ufvk: UnifiedFullViewingKey,
) -> Self {
Self(hd_seed_fingerprint, account_index, Box::new(ufvk))
}
/// Returns the HD seed fingerprint for this account.
pub fn hd_seed_fingerprint(&self) -> &HdSeedFingerprint {
&self.0
}
/// Returns the ZIP-32 account index for this account.
pub fn account_index(&self) -> zip32::AccountId {
self.1
}
/// Returns the Unified Full Viewing Key for this account.
pub fn ufvk(&self) -> &UnifiedFullViewingKey {
&self.2
}
}
/// Represents an arbitrary account for which the seed and ZIP-32 account ID are not known
/// and may not have been involved in creating this account.
#[derive(Debug, Clone)]
pub(crate) enum ImportedAccount {
/// An account that was imported via its full viewing key.
pub(crate) enum ViewingKey {
/// A full viewing key.
///
/// This is available to derived accounts, as well as accounts directly imported as
/// full viewing keys.
Full(Box<UnifiedFullViewingKey>),
/// An account that was imported via its incoming viewing key.
/// An incoming viewing key.
///
/// Accounts that have this kind of viewing key cannot be used in wallet contexts,
/// because they are unable to maintain an accurate balance.
Incoming(Box<UnifiedIncomingViewingKey>),
}
/// Describes an account in terms of its UVK or ZIP-32 origins.
/// An account stored in a `zcash_client_sqlite` database.
#[derive(Debug, Clone)]
pub(crate) enum Account {
/// Inputs for a ZIP-32 HD account.
Zip32(HdSeedAccount),
/// Inputs for an imported account.
Imported(ImportedAccount),
pub struct Account {
account_id: AccountId,
kind: AccountKind,
viewing_key: ViewingKey,
}
impl Account {
@ -226,14 +201,45 @@ impl Account {
///
/// The diversifier index may be non-zero if the Unified Address includes a Sapling
/// receiver, and there was no valid Sapling receiver at diversifier index zero.
pub fn default_address(
pub(crate) fn default_address(
&self,
request: UnifiedAddressRequest,
) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> {
match &self.viewing_key {
ViewingKey::Full(ufvk) => ufvk.default_address(request),
ViewingKey::Incoming(_uivk) => todo!(),
}
}
}
impl zcash_client_backend::data_api::Account<AccountId> for Account {
fn id(&self) -> AccountId {
self.account_id
}
fn kind(&self) -> AccountKind {
self.kind
}
fn ufvk(&self) -> Option<&UnifiedFullViewingKey> {
self.viewing_key.ufvk()
}
}
impl ViewingKey {
fn ufvk(&self) -> Option<&UnifiedFullViewingKey> {
match self {
Account::Zip32(HdSeedAccount(_, _, ufvk)) => ufvk.default_address(request),
Account::Imported(ImportedAccount::Full(ufvk)) => ufvk.default_address(request),
Account::Imported(ImportedAccount::Incoming(uivk)) => uivk.default_address(request),
ViewingKey::Full(ufvk) => Some(ufvk),
ViewingKey::Incoming(_) => None,
}
}
fn uivk(&self) -> Result<UnifiedIncomingViewingKey, SqliteClientError> {
match self {
ViewingKey::Full(ufvk) => Ok(ufvk
.to_unified_incoming_viewing_key()
.map_err(|e| SqliteClientError::CorruptedData(e.to_string()))?),
ViewingKey::Incoming(uivk) => Ok((**uivk).to_owned()),
}
}
}
@ -293,65 +299,30 @@ pub(crate) fn max_zip32_account_index(
)
}
struct AccountSqlValues<'a> {
account_type: u32,
hd_seed_fingerprint: Option<&'a [u8]>,
hd_account_index: Option<u32>,
ufvk: Option<&'a UnifiedFullViewingKey>,
uivk: UnifiedIncomingViewingKey,
}
/// Returns (account_type, hd_seed_fingerprint, hd_account_index, ufvk, uivk) for a given account.
fn get_sql_values_for_account_parameters(
account: &Account,
) -> Result<AccountSqlValues, SqliteClientError> {
Ok(match account {
Account::Zip32(hdaccount) => AccountSqlValues {
account_type: AccountType::Zip32.into(),
hd_seed_fingerprint: Some(hdaccount.hd_seed_fingerprint().as_bytes()),
hd_account_index: Some(hdaccount.account_index().into()),
ufvk: Some(hdaccount.ufvk()),
uivk: hdaccount
.ufvk()
.to_unified_incoming_viewing_key()
.map_err(|e| SqliteClientError::CorruptedData(e.to_string()))?,
},
Account::Imported(ImportedAccount::Full(ufvk)) => AccountSqlValues {
account_type: AccountType::Imported.into(),
hd_seed_fingerprint: None,
hd_account_index: None,
ufvk: Some(ufvk),
uivk: ufvk
.to_unified_incoming_viewing_key()
.map_err(|e| SqliteClientError::CorruptedData(e.to_string()))?,
},
Account::Imported(ImportedAccount::Incoming(uivk)) => AccountSqlValues {
account_type: AccountType::Imported.into(),
hd_seed_fingerprint: None,
hd_account_index: None,
ufvk: None,
uivk: (**uivk).clone(),
},
})
}
pub(crate) fn add_account<P: consensus::Parameters>(
conn: &rusqlite::Transaction,
params: &P,
account: Account,
kind: AccountKind,
viewing_key: ViewingKey,
birthday: AccountBirthday,
) -> Result<AccountId, SqliteClientError> {
let args = get_sql_values_for_account_parameters(&account)?;
let (hd_seed_fingerprint, hd_account_index) = match kind {
AccountKind::Derived {
seed_fingerprint,
account_index,
} => (Some(seed_fingerprint), Some(account_index)),
AccountKind::Imported => (None, None),
};
let orchard_item = args
.ufvk
let orchard_item = viewing_key
.ufvk()
.and_then(|ufvk| ufvk.orchard().map(|k| k.to_bytes()));
let sapling_item = args
.ufvk
let sapling_item = viewing_key
.ufvk()
.and_then(|ufvk| ufvk.sapling().map(|k| k.to_bytes()));
#[cfg(feature = "transparent-inputs")]
let transparent_item = args
.ufvk
let transparent_item = viewing_key
.ufvk()
.and_then(|ufvk| ufvk.transparent().map(|k| k.serialize()));
#[cfg(not(feature = "transparent-inputs"))]
let transparent_item: Option<Vec<u8>> = None;
@ -359,13 +330,13 @@ pub(crate) fn add_account<P: consensus::Parameters>(
let account_id: AccountId = conn.query_row(
r#"
INSERT INTO accounts (
account_type, hd_seed_fingerprint, hd_account_index,
account_kind, hd_seed_fingerprint, hd_account_index,
ufvk, uivk,
orchard_fvk_item_cache, sapling_fvk_item_cache, p2pkh_fvk_item_cache,
birthday_height, recover_until_height
)
VALUES (
:account_type, :hd_seed_fingerprint, :hd_account_index,
:account_kind, :hd_seed_fingerprint, :hd_account_index,
:ufvk, :uivk,
:orchard_fvk_item_cache, :sapling_fvk_item_cache, :p2pkh_fvk_item_cache,
:birthday_height, :recover_until_height
@ -373,11 +344,11 @@ pub(crate) fn add_account<P: consensus::Parameters>(
RETURNING id;
"#,
named_params![
":account_type": args.account_type,
":hd_seed_fingerprint": args.hd_seed_fingerprint,
":hd_account_index": args.hd_account_index,
":ufvk": args.ufvk.map(|ufvk| ufvk.encode(params)),
":uivk": args.uivk.to_uivk().encode(&params.network_type()),
":account_kind": account_kind_code(kind),
":hd_seed_fingerprint": hd_seed_fingerprint.as_ref().map(|fp| fp.as_bytes()),
":hd_account_index": hd_account_index.map(u32::from),
":ufvk": viewing_key.ufvk().map(|ufvk| ufvk.encode(params)),
":uivk": viewing_key.uivk()?.to_uivk().encode(&params.network_type()),
":orchard_fvk_item_cache": orchard_item,
":sapling_fvk_item_cache": sapling_item,
":p2pkh_fvk_item_cache": transparent_item,
@ -387,6 +358,12 @@ pub(crate) fn add_account<P: consensus::Parameters>(
|row| Ok(AccountId(row.get(0)?)),
)?;
let account = Account {
account_id,
kind,
viewing_key,
};
// If a birthday frontier is available, insert it into the note commitment tree. If the
// birthday frontier is the empty frontier, we don't need to do anything.
if let Some(frontier) = birthday.sapling_frontier().value() {
@ -712,14 +689,14 @@ pub(crate) fn get_account_for_ufvk<P: consensus::Parameters>(
conn: &rusqlite::Connection,
params: &P,
ufvk: &UnifiedFullViewingKey,
) -> Result<Option<(AccountId, Option<UnifiedFullViewingKey>)>, SqliteClientError> {
) -> Result<Option<Account>, SqliteClientError> {
#[cfg(feature = "transparent-inputs")]
let transparent_item = ufvk.transparent().map(|k| k.serialize());
#[cfg(not(feature = "transparent-inputs"))]
let transparent_item: Option<Vec<u8>> = None;
let mut stmt = conn.prepare(
"SELECT id, ufvk
"SELECT id, account_kind, hd_seed_fingerprint, hd_account_index, ufvk
FROM accounts
WHERE orchard_fvk_item_cache = :orchard_fvk_item_cache
OR sapling_fvk_item_cache = :sapling_fvk_item_cache
@ -735,18 +712,25 @@ pub(crate) fn get_account_for_ufvk<P: consensus::Parameters>(
],
|row| {
let account_id = row.get::<_, u32>(0).map(AccountId)?;
Ok((
account_id,
row.get::<_, Option<String>>(1)?
.map(|ufvk_str| UnifiedFullViewingKey::decode(params, &ufvk_str))
.transpose()
.map_err(|e| {
let kind = parse_account_kind(row.get(1)?, row.get(2)?, row.get(3)?)?;
// We looked up the account by FVK components, so the UFVK column must be
// non-null.
let ufvk_str: String = row.get(4)?;
let viewing_key = ViewingKey::Full(Box::new(
UnifiedFullViewingKey::decode(params, &ufvk_str).map_err(|e| {
SqliteClientError::CorruptedData(format!(
"Could not decode unified full viewing key for account {:?}: {}",
account_id, e
))
})?,
))
));
Ok(Account {
account_id,
kind,
viewing_key,
})
},
)?
.collect::<Result<Vec<_>, _>>()?;
@ -762,12 +746,12 @@ pub(crate) fn get_account_for_ufvk<P: consensus::Parameters>(
/// Returns the account id corresponding to a given [`HdSeedFingerprint`]
/// and [`zip32::AccountId`], if any.
pub(crate) fn get_seed_account<P: consensus::Parameters>(
pub(crate) fn get_derived_account<P: consensus::Parameters>(
conn: &rusqlite::Connection,
params: &P,
seed: &HdSeedFingerprint,
account_id: zip32::AccountId,
) -> Result<Option<(AccountId, Option<UnifiedFullViewingKey>)>, SqliteClientError> {
account_index: zip32::AccountId,
) -> Result<Option<Account>, SqliteClientError> {
let mut stmt = conn.prepare(
"SELECT id, ufvk
FROM accounts
@ -778,22 +762,30 @@ pub(crate) fn get_seed_account<P: consensus::Parameters>(
let mut accounts = stmt.query_and_then::<_, SqliteClientError, _, _>(
named_params![
":hd_seed_fingerprint": seed.as_bytes(),
":hd_account_index": u32::from(account_id),
":hd_account_index": u32::from(account_index),
],
|row| {
let account_id = row.get::<_, u32>(0).map(AccountId)?;
Ok((
let ufvk = match row.get::<_, Option<String>>(1)? {
None => Err(SqliteClientError::CorruptedData(format!(
"Missing unified full viewing key for derived account {:?}",
account_id,
row.get::<_, Option<String>>(1)?
.map(|ufvk_str| UnifiedFullViewingKey::decode(params, &ufvk_str))
.transpose()
.map_err(|e| {
))),
Some(ufvk_str) => UnifiedFullViewingKey::decode(params, &ufvk_str).map_err(|e| {
SqliteClientError::CorruptedData(format!(
"Could not decode unified full viewing key for account {:?}: {}",
account_id, e
))
})?,
))
}),
}?;
Ok(Account {
account_id,
kind: AccountKind::Derived {
seed_fingerprint: *seed,
account_index,
},
viewing_key: ViewingKey::Full(Box::new(ufvk)),
})
},
)?;
@ -1481,13 +1473,14 @@ pub(crate) fn block_height_extrema(
})
}
pub(crate) fn get_account<C: Borrow<rusqlite::Connection>, P: Parameters>(
db: &WalletDb<C, P>,
pub(crate) fn get_account<P: Parameters>(
conn: &rusqlite::Connection,
params: &P,
account_id: AccountId,
) -> Result<Option<Account>, SqliteClientError> {
let mut sql = db.conn.borrow().prepare_cached(
let mut sql = conn.prepare_cached(
r#"
SELECT account_type, ufvk, uivk, hd_seed_fingerprint, hd_account_index
SELECT account_kind, hd_seed_fingerprint, hd_account_index, ufvk, uivk
FROM accounts
WHERE id = :account_id
"#,
@ -1497,53 +1490,40 @@ pub(crate) fn get_account<C: Borrow<rusqlite::Connection>, P: Parameters>(
let row = result.next()?;
match row {
Some(row) => {
let account_type: AccountType =
row.get::<_, u32>("account_type")?.try_into().map_err(|_| {
SqliteClientError::CorruptedData("Unrecognized account_type".to_string())
})?;
let kind = parse_account_kind(
row.get("account_kind")?,
row.get("hd_seed_fingerprint")?,
row.get("hd_account_index")?,
)?;
let ufvk_str: Option<String> = row.get("ufvk")?;
let ufvk = if let Some(ufvk_str) = ufvk_str {
Some(
UnifiedFullViewingKey::decode(&db.params, &ufvk_str[..])
let viewing_key = if let Some(ufvk_str) = ufvk_str {
ViewingKey::Full(Box::new(
UnifiedFullViewingKey::decode(params, &ufvk_str[..])
.map_err(SqliteClientError::BadAccountData)?,
)
))
} else {
None
};
let uivk_str: String = row.get("uivk")?;
let (network, uivk) = Uivk::decode(&uivk_str).map_err(|e| {
SqliteClientError::CorruptedData(format!("Failure to decode UIVK: {e}"))
})?;
if network != db.params.network_type() {
if network != params.network_type() {
return Err(SqliteClientError::CorruptedData(
"UIVK network type does not match wallet network type".to_string(),
));
}
let uivk = UnifiedIncomingViewingKey::from_uivk(&uivk)
.map_err(|e| SqliteClientError::CorruptedData(e.to_string()))?;
match account_type {
AccountType::Zip32 => Ok(Some(Account::Zip32(HdSeedAccount::new(
HdSeedFingerprint::from_bytes(row.get("hd_seed_fingerprint")?),
zip32::AccountId::try_from(row.get::<_, u32>("hd_account_index")?).map_err(
|_| {
SqliteClientError::CorruptedData(
"ZIP-32 account ID from db is out of range.".to_string(),
)
},
)?,
ufvk.ok_or_else(|| {
SqliteClientError::CorruptedData(
"ZIP-32 account is missing a full viewing key".to_string(),
)
ViewingKey::Incoming(Box::new(
UnifiedIncomingViewingKey::from_uivk(&uivk).map_err(|e| {
SqliteClientError::CorruptedData(format!("Failure to decode UIVK: {e}"))
})?,
)))),
AccountType::Imported => Ok(Some(Account::Imported(if let Some(ufvk) = ufvk {
ImportedAccount::Full(Box::new(ufvk))
} else {
ImportedAccount::Incoming(Box::new(uivk))
}))),
}
))
};
Ok(Some(Account {
account_id,
kind,
viewing_key,
}))
}
None => Ok(None),
}
@ -2703,12 +2683,11 @@ mod tests {
use std::num::NonZeroU32;
use sapling::zip32::ExtendedSpendingKey;
use zcash_client_backend::data_api::{AccountBirthday, WalletRead};
use zcash_client_backend::data_api::{AccountBirthday, AccountKind, WalletRead};
use zcash_primitives::{block::BlockHash, transaction::components::amount::NonNegativeAmount};
use crate::{
testing::{AddressType, BlockCache, TestBuilder, TestState},
wallet::{get_account, Account},
AccountId,
};
@ -2856,12 +2835,12 @@ mod tests {
.with_test_account(AccountBirthday::from_sapling_activation)
.build();
let account_id = st.test_account().unwrap().0;
let account_parameters = get_account(st.wallet(), account_id).unwrap().unwrap();
let account_parameters = st.wallet().get_account(account_id).unwrap().unwrap();
let expected_account_index = zip32::AccountId::try_from(0).unwrap();
assert_matches!(
account_parameters,
Account::Zip32(hdaccount) if hdaccount.account_index() == expected_account_index
account_parameters.kind,
AccountKind::Derived{account_index, ..} if account_index == expected_account_index
);
}

View File

@ -224,7 +224,7 @@ mod tests {
let expected_tables = vec![
r#"CREATE TABLE "accounts" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
account_type INTEGER NOT NULL DEFAULT 0,
account_kind INTEGER NOT NULL DEFAULT 0,
hd_seed_fingerprint BLOB,
hd_account_index INTEGER,
ufvk TEXT,
@ -234,7 +234,7 @@ mod tests {
p2pkh_fvk_item_cache BLOB,
birthday_height INTEGER NOT NULL,
recover_until_height INTEGER,
CHECK ( (account_type = 0 AND hd_seed_fingerprint IS NOT NULL AND hd_account_index IS NOT NULL AND ufvk IS NOT NULL) OR (account_type = 1 AND hd_seed_fingerprint IS NULL AND hd_account_index IS NULL) )
CHECK ( (account_kind = 0 AND hd_seed_fingerprint IS NOT NULL AND hd_account_index IS NOT NULL AND ufvk IS NOT NULL) OR (account_kind = 1 AND hd_seed_fingerprint IS NULL AND hd_account_index IS NULL) )
)"#,
r#"CREATE TABLE "addresses" (
account_id INTEGER NOT NULL,
@ -1282,9 +1282,7 @@ mod tests {
#[test]
#[cfg(feature = "transparent-inputs")]
fn account_produces_expected_ua_sequence() {
use zcash_client_backend::data_api::AccountBirthday;
use crate::wallet::{get_account, Account};
use zcash_client_backend::data_api::{AccountBirthday, AccountKind, WalletRead};
let network = Network::MainNetwork;
let data_file = NamedTempFile::new().unwrap();
@ -1300,8 +1298,11 @@ mod tests {
.create_account(&Secret::new(seed.to_vec()), birthday)
.unwrap();
assert_matches!(
get_account(&db_data, account_id),
Ok(Some(Account::Zip32(hdaccount))) if hdaccount.account_index() == zip32::AccountId::ZERO
db_data.get_account(account_id),
Ok(Some(account)) if matches!(
account.kind,
AccountKind::Derived{account_index, ..} if account_index == zip32::AccountId::ZERO,
)
);
for tv in &test_vectors::UNIFIED[..3] {

View File

@ -1,12 +1,12 @@
use std::{collections::HashSet, rc::Rc};
use crate::wallet::{init::WalletMigrationError, AccountType};
use crate::wallet::{account_kind_code, init::WalletMigrationError};
use rusqlite::{named_params, Transaction};
use schemer_rusqlite::RusqliteMigration;
use secrecy::{ExposeSecret, SecretVec};
use uuid::Uuid;
use zcash_address::unified::Encoding;
use zcash_client_backend::keys::UnifiedSpendingKey;
use zcash_client_backend::{data_api::AccountKind, keys::UnifiedSpendingKey};
use zcash_keys::keys::{HdSeedFingerprint, UnifiedFullViewingKey};
use zcash_primitives::consensus;
@ -45,8 +45,11 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
type Error = WalletMigrationError;
fn up(&self, transaction: &Transaction) -> Result<(), WalletMigrationError> {
let account_type_zip32 = u32::from(AccountType::Zip32);
let account_type_imported = u32::from(AccountType::Imported);
let account_kind_derived = account_kind_code(AccountKind::Derived {
seed_fingerprint: HdSeedFingerprint::from_bytes([0; 32]),
account_index: zip32::AccountId::ZERO,
});
let account_kind_imported = account_kind_code(AccountKind::Imported);
transaction.execute_batch(
&format!(r#"
PRAGMA foreign_keys = OFF;
@ -54,7 +57,7 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
CREATE TABLE accounts_new (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
account_type INTEGER NOT NULL DEFAULT {account_type_zip32},
account_kind INTEGER NOT NULL DEFAULT {account_kind_derived},
hd_seed_fingerprint BLOB,
hd_account_index INTEGER,
ufvk TEXT,
@ -65,9 +68,9 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
birthday_height INTEGER NOT NULL,
recover_until_height INTEGER,
CHECK (
(account_type = {account_type_zip32} AND hd_seed_fingerprint IS NOT NULL AND hd_account_index IS NOT NULL AND ufvk IS NOT NULL)
(account_kind = {account_kind_derived} AND hd_seed_fingerprint IS NOT NULL AND hd_account_index IS NOT NULL AND ufvk IS NOT NULL)
OR
(account_type = {account_type_imported} AND hd_seed_fingerprint IS NULL AND hd_account_index IS NULL)
(account_kind = {account_kind_imported} AND hd_seed_fingerprint IS NULL AND hd_account_index IS NULL)
)
);
CREATE UNIQUE INDEX hd_account ON accounts_new (hd_seed_fingerprint, hd_account_index);
@ -86,7 +89,6 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
let mut rows = q.query([])?;
while let Some(row) = rows.next()? {
let account_index: u32 = row.get("account")?;
let account_type = u32::from(AccountType::Zip32);
let birthday_height: u32 = row.get("birthday_height")?;
let recover_until_height: Option<u32> = row.get("recover_until_height")?;
@ -132,13 +134,13 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
transaction.execute(
r#"
INSERT INTO accounts_new (
id, account_type, hd_seed_fingerprint, hd_account_index,
id, account_kind, hd_seed_fingerprint, hd_account_index,
ufvk, uivk,
orchard_fvk_item_cache, sapling_fvk_item_cache, p2pkh_fvk_item_cache,
birthday_height, recover_until_height
)
VALUES (
:account_id, :account_type, :seed_id, :account_index,
:account_id, :account_kind, :seed_id, :account_index,
:ufvk, :uivk,
:orchard_fvk_item_cache, :sapling_fvk_item_cache, :p2pkh_fvk_item_cache,
:birthday_height, :recover_until_height
@ -146,7 +148,7 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
"#,
named_params![
":account_id": account_id,
":account_type": account_type,
":account_kind": account_kind_derived,
":seed_id": seed_id.as_bytes(),
":account_index": account_index,
":ufvk": ufvk,

View File

@ -514,7 +514,6 @@ pub enum AddressGenerationError {
/// A Unified address cannot be generated without at least one shielded receiver being
/// included.
ShieldedReceiverRequired,
// An error occurred while deriving a key or address from an HD wallet.
Derivation(DerivationError),
}
@ -739,7 +738,7 @@ impl UnifiedFullViewingKey {
})
.transpose(),
#[cfg(not(feature = "orchard"))]
unified::Fvk::Orchard(data) => Some(Ok::<_, UnifiedError>((
unified::Fvk::Orchard(data) => Some(Ok::<_, DerivationError>((
u32::from(unified::Typecode::Orchard),
data.to_vec(),
))),
@ -756,7 +755,7 @@ impl UnifiedFullViewingKey {
.transpose()
}
#[cfg(not(feature = "sapling"))]
unified::Fvk::Sapling(data) => Some(Ok::<_, UnifiedError>((
unified::Fvk::Sapling(data) => Some(Ok::<_, DerivationError>((
u32::from(unified::Typecode::Sapling),
data.to_vec(),
))),
@ -769,7 +768,7 @@ impl UnifiedFullViewingKey {
})
.transpose(),
#[cfg(not(feature = "transparent-inputs"))]
unified::Fvk::P2pkh(data) => Some(Ok::<_, UnifiedError>((
unified::Fvk::P2pkh(data) => Some(Ok::<_, DerivationError>((
u32::from(unified::Typecode::P2pkh),
data.to_vec(),
))),