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` - `Account`
- `AccountBalance::with_orchard_balance_mut` - `AccountBalance::with_orchard_balance_mut`
- `AccountBirthday::orchard_frontier` - `AccountBirthday::orchard_frontier`
- `AccountKind`
- `BlockMetadata::orchard_tree_size` - `BlockMetadata::orchard_tree_size`
- `DecryptedTransaction::{new, tx(), orchard_outputs()}` - `DecryptedTransaction::{new, tx(), orchard_outputs()}`
- `ScannedBlock::orchard` - `ScannedBlock::orchard`
@ -24,12 +25,14 @@ and this library adheres to Rust's notion of
- `ORCHARD_SHARD_HEIGHT` - `ORCHARD_SHARD_HEIGHT`
- `BlockMetadata::orchard_tree_size` - `BlockMetadata::orchard_tree_size`
- `WalletSummary::next_orchard_subtree_index` - `WalletSummary::next_orchard_subtree_index`
- `chain::ChainState`
- `chain::ScanSummary::{spent_orchard_note_count, received_orchard_note_count}` - `chain::ScanSummary::{spent_orchard_note_count, received_orchard_note_count}`
- `zcash_client_backend::fees`: - `zcash_client_backend::fees`:
- `orchard` - `orchard`
- `ChangeValue::orchard` - `ChangeValue::orchard`
- `zcash_client_backend::proto`: - `zcash_client_backend::proto`:
- `service::TreeState::orchard_tree` - `service::TreeState::orchard_tree`
- `service::TreeState::to_chain_state`
- `impl TryFrom<&CompactOrchardAction> for CompactAction` - `impl TryFrom<&CompactOrchardAction> for CompactAction`
- `CompactOrchardAction::{cmx, nf, ephemeral_key}` - `CompactOrchardAction::{cmx, nf, ephemeral_key}`
- `zcash_client_backend::scanning`: - `zcash_client_backend::scanning`:
@ -51,10 +54,11 @@ and this library adheres to Rust's notion of
- Arguments to `ScannedBlock::from_parts` have changed. - Arguments to `ScannedBlock::from_parts` have changed.
- Changes to the `WalletRead` trait: - Changes to the `WalletRead` trait:
- Added `Account` associated type. - 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. - 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: - Changes to the `InputSource` trait:
- `select_spendable_notes` now takes its `target_value` argument as a - `select_spendable_notes` now takes its `target_value` argument as a
`NonNegativeAmount`. Also, the values of the returned map are also `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. /// A set of capabilities that a client account must provide.
pub trait Account<AccountId: Copy> { pub trait Account<AccountId: Copy> {
/// Returns the unique identifier for the account. /// Returns the unique identifier for the account.
fn id(&self) -> AccountId; 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. /// 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>; fn ufvk(&self) -> Option<&UnifiedFullViewingKey>;
} }
@ -329,6 +349,10 @@ impl<A: Copy> Account<A> for (A, UnifiedFullViewingKey) {
self.0 self.0
} }
fn kind(&self) -> AccountKind {
AccountKind::Imported
}
fn ufvk(&self) -> Option<&UnifiedFullViewingKey> { fn ufvk(&self) -> Option<&UnifiedFullViewingKey> {
Some(&self.1) Some(&self.1)
} }
@ -339,6 +363,10 @@ impl<A: Copy> Account<A> for (A, Option<UnifiedFullViewingKey>) {
self.0 self.0
} }
fn kind(&self) -> AccountKind {
AccountKind::Imported
}
fn ufvk(&self) -> Option<&UnifiedFullViewingKey> { fn ufvk(&self) -> Option<&UnifiedFullViewingKey> {
self.1.as_ref() self.1.as_ref()
} }
@ -543,6 +571,23 @@ pub trait WalletRead {
/// The concrete account type used by this wallet backend. /// The concrete account type used by this wallet backend.
type Account: Account<Self::AccountId>; 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. /// Verifies that the given seed corresponds to the viewing key for the specified account.
/// ///
/// Returns: /// Returns:
@ -558,12 +603,50 @@ pub trait WalletRead {
seed: &SecretVec<u8>, seed: &SecretVec<u8>,
) -> Result<bool, Self::Error>; ) -> 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 /// Returns the height of the chain as known to the wallet as of the most recent call to
/// [`WalletWrite::update_chain_tip`]. /// [`WalletWrite::update_chain_tip`].
/// ///
/// This will return `Ok(None)` if the height of the current consensus chain tip is unknown. /// 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>; 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. /// 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>; fn block_metadata(&self, height: BlockHeight) -> Result<Option<BlockMetadata>, Self::Error>;
@ -576,6 +659,11 @@ pub trait WalletRead {
/// block. /// block.
fn block_fully_scanned(&self) -> Result<Option<BlockMetadata>, Self::Error>; 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. /// 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`; /// 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. /// Returns the minimum block height corresponding to an unspent note in the wallet.
fn get_min_unspent_height(&self) -> Result<Option<BlockHeight>, Self::Error>; 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 /// Returns the block height in which the specified transaction was mined, or `Ok(None)` if the
/// transaction is not in the main chain. /// transaction is not in the main chain.
fn get_tx_height(&self, txid: TxId) -> Result<Option<BlockHeight>, Self::Error>; 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. /// Returns all unified full viewing keys known to this wallet.
fn get_unified_full_viewing_keys( fn get_unified_full_viewing_keys(
&self, &self,
) -> Result<HashMap<Self::AccountId, UnifiedFullViewingKey>, Self::Error>; ) -> 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 the memo for a note.
/// ///
/// Returns `Ok(None)` if the note is known to the wallet but memo data has not yet been /// 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> { ) -> Result<HashMap<TransparentAddress, NonNegativeAmount>, Self::Error> {
Ok(HashMap::new()) 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. /// 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, request: UnifiedAddressRequest,
) -> Result<Option<UnifiedAddress>, Self::Error>; ) -> 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, /// 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 /// along with the note commitments that were detected when scanning the block for transactions
/// pertaining to this wallet. /// pertaining to this wallet.
@ -1313,14 +1356,11 @@ pub trait WalletWrite: WalletRead {
blocks: Vec<ScannedBlock<Self::AccountId>>, blocks: Vec<ScannedBlock<Self::AccountId>>,
) -> Result<(), Self::Error>; ) -> Result<(), Self::Error>;
/// Updates the wallet's view of the blockchain. /// Adds a transparent UTXO received by the wallet to the data store.
/// fn put_received_transparent_utxo(
/// This method is used to provide the wallet with information about the state of the &mut self,
/// blockchain, and detect any previously scanned data that needs to be re-validated output: &WalletTransparentOutput,
/// before proceeding with scanning. It should be called at wallet startup prior to calling ) -> Result<Self::UtxoRef, Self::Error>;
/// [`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>;
/// Caches a decrypted transaction in the persistent wallet store. /// Caches a decrypted transaction in the persistent wallet store.
fn store_decrypted_tx( 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. /// 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>; 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. /// This trait describes a capability for manipulating wallet note commitment trees.
@ -1521,6 +1555,25 @@ pub mod testing {
type AccountId = u32; type AccountId = u32;
type Account = (Self::AccountId, UnifiedFullViewingKey); 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( fn validate_seed(
&self, &self,
_account_id: Self::AccountId, _account_id: Self::AccountId,
@ -1529,10 +1582,49 @@ pub mod testing {
Ok(false) 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> { fn chain_height(&self) -> Result<Option<BlockHeight>, Self::Error> {
Ok(None) Ok(None)
} }
fn get_block_hash(
&self,
_block_height: BlockHeight,
) -> Result<Option<BlockHash>, Self::Error> {
Ok(None)
}
fn block_metadata( fn block_metadata(
&self, &self,
_height: BlockHeight, _height: BlockHeight,
@ -1544,6 +1636,10 @@ pub mod testing {
Ok(None) 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> { fn block_max_scanned(&self) -> Result<Option<BlockMetadata>, Self::Error> {
Ok(None) Ok(None)
} }
@ -1563,67 +1659,16 @@ pub mod testing {
Ok(None) 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> { fn get_tx_height(&self, _txid: TxId) -> Result<Option<BlockHeight>, Self::Error> {
Ok(None) 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( fn get_unified_full_viewing_keys(
&self, &self,
) -> Result<HashMap<Self::AccountId, UnifiedFullViewingKey>, Self::Error> { ) -> Result<HashMap<Self::AccountId, UnifiedFullViewingKey>, Self::Error> {
Ok(HashMap::new()) 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> { fn get_memo(&self, _id_note: NoteId) -> Result<Option<Memo>, Self::Error> {
Ok(None) Ok(None)
} }
@ -1664,10 +1709,6 @@ pub mod testing {
) -> Result<HashMap<TransparentAddress, NonNegativeAmount>, Self::Error> { ) -> Result<HashMap<TransparentAddress, NonNegativeAmount>, Self::Error> {
Ok(HashMap::new()) Ok(HashMap::new())
} }
fn get_account_ids(&self) -> Result<Vec<Self::AccountId>, Self::Error> {
Ok(Vec::new())
}
} }
impl WalletWrite for MockWalletDb { impl WalletWrite for MockWalletDb {

View File

@ -20,7 +20,7 @@ use zcash_primitives::{
}; };
use crate::{ use crate::{
data_api::InputSource, data_api::{chain::ChainState, InputSource},
fees::{ChangeValue, TransactionBalance}, fees::{ChangeValue, TransactionBalance},
proposal::{Proposal, ProposalError, ShieldedInputs, Step, StepOutput, StepOutputIndex}, proposal::{Proposal, ProposalError, ShieldedInputs, Step, StepOutput, StepOutputIndex},
zip321::{TransactionRequest, Zip321Error}, zip321::{TransactionRequest, Zip321Error},
@ -290,6 +290,20 @@ impl service::TreeState {
&orchard_tree_bytes[..], &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. /// 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 - A new `orchard` feature flag has been added to make it possible to
build client code without `orchard` dependendencies. build client code without `orchard` dependendencies.
- `zcash_client_sqlite::AccountId` - `zcash_client_sqlite::AccountId`
- `zcash_client_sqlite::wallet::Account`
- `impl From<zcash_keys::keys::AddressGenerationError> for SqliteClientError` - `impl From<zcash_keys::keys::AddressGenerationError> for SqliteClientError`
### Changed ### Changed

View File

@ -60,9 +60,9 @@ use zcash_client_backend::{
self, self,
chain::{BlockSource, ChainState, CommitmentTreeRoot}, chain::{BlockSource, ChainState, CommitmentTreeRoot},
scanning::{ScanPriority, ScanRange}, scanning::{ScanPriority, ScanRange},
AccountBirthday, BlockMetadata, DecryptedTransaction, InputSource, NullifierQuery, Account, AccountBirthday, AccountKind, BlockMetadata, DecryptedTransaction, InputSource,
ScannedBlock, SentTransaction, WalletCommitmentTrees, WalletRead, WalletSummary, NullifierQuery, ScannedBlock, SentTransaction, WalletCommitmentTrees, WalletRead,
WalletWrite, SAPLING_SHARD_HEIGHT, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT,
}, },
keys::{ keys::{
AddressGenerationError, UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey, AddressGenerationError, UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey,
@ -100,7 +100,7 @@ pub mod error;
pub mod wallet; pub mod wallet;
use wallet::{ use wallet::{
commitment_tree::{self, put_shard_roots}, commitment_tree::{self, put_shard_roots},
Account, HdSeedAccount, SubtreeScanProgress, SubtreeScanProgress,
}; };
#[cfg(test)] #[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> { impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for WalletDb<C, P> {
type Error = SqliteClientError; type Error = SqliteClientError;
type AccountId = AccountId; 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( fn validate_seed(
&self, &self,
account_id: Self::AccountId, account_id: Self::AccountId,
seed: &SecretVec<u8>, seed: &SecretVec<u8>,
) -> Result<bool, Self::Error> { ) -> Result<bool, Self::Error> {
if let Some(account) = wallet::get_account(self, account_id)? { if let Some(account) = self.get_account(account_id)? {
if let Account::Zip32(hdaccount) = account { if let AccountKind::Derived {
let seed_fingerprint_match = seed_fingerprint,
HdSeedFingerprint::from_seed(seed) == *hdaccount.hd_seed_fingerprint(); account_index,
} = account.kind()
{
let seed_fingerprint_match = HdSeedFingerprint::from_seed(seed) == seed_fingerprint;
let usk = UnifiedSpendingKey::from_seed( let usk = UnifiedSpendingKey::from_seed(
&self.params, &self.params,
&seed.expose_secret()[..], &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 // 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. // 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 Ok(usk
.to_unified_full_viewing_key() .to_unified_full_viewing_key()
.default_address(ua_request)? .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> { fn chain_height(&self) -> Result<Option<BlockHeight>, Self::Error> {
wallet::scan_queue_extrema(self.conn.borrow()) wallet::scan_queue_extrema(self.conn.borrow())
.map(|h| h.map(|range| *range.end())) .map(|h| h.map(|range| *range.end()))
.map_err(SqliteClientError::from) .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> { fn block_metadata(&self, height: BlockHeight) -> Result<Option<BlockMetadata>, Self::Error> {
wallet::block_metadata(self.conn.borrow(), &self.params, height) 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) 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> { fn block_max_scanned(&self) -> Result<Option<BlockMetadata>, Self::Error> {
wallet::block_max_scanned(self.conn.borrow(), &self.params) 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) 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> { fn get_tx_height(&self, txid: TxId) -> Result<Option<BlockHeight>, Self::Error> {
wallet::get_tx_height(self.conn.borrow(), txid).map_err(SqliteClientError::from) 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( fn get_unified_full_viewing_keys(
&self, &self,
) -> Result<HashMap<AccountId, UnifiedFullViewingKey>, Self::Error> { ) -> Result<HashMap<AccountId, UnifiedFullViewingKey>, Self::Error> {
wallet::get_unified_full_viewing_keys(self.conn.borrow(), &self.params) 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> { fn get_memo(&self, note_id: NoteId) -> Result<Option<Memo>, Self::Error> {
let sent_memo = wallet::get_sent_memo(self.conn.borrow(), note_id)?; let sent_memo = wallet::get_sent_memo(self.conn.borrow(), note_id)?;
if sent_memo.is_some() { 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> { ) -> Result<HashMap<TransparentAddress, NonNegativeAmount>, Self::Error> {
wallet::get_transparent_balances(self.conn.borrow(), &self.params, account, max_height) 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> { 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, birthday: AccountBirthday,
) -> Result<(AccountId, UnifiedSpendingKey), Self::Error> { ) -> Result<(AccountId, UnifiedSpendingKey), Self::Error> {
self.transactionally(|wdb| { self.transactionally(|wdb| {
let seed_id = HdSeedFingerprint::from_seed(seed); let seed_fingerprint = HdSeedFingerprint::from_seed(seed);
let account_index = wallet::max_zip32_account_index(wdb.conn.0, &seed_id)? let account_index = wallet::max_zip32_account_index(wdb.conn.0, &seed_fingerprint)?
.map(|a| a.next().ok_or(SqliteClientError::AccountIdOutOfRange)) .map(|a| a.next().ok_or(SqliteClientError::AccountIdOutOfRange))
.transpose()? .transpose()?
.unwrap_or(zip32::AccountId::ZERO); .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))?; .map_err(|_| SqliteClientError::KeyDerivationError(account_index))?;
let ufvk = usk.to_unified_full_viewing_key(); 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(
let account_id = wallet::add_account(wdb.conn.0, &wdb.params, account, birthday)?; wdb.conn.0,
&wdb.params,
AccountKind::Derived {
seed_fingerprint,
account_index,
},
wallet::ViewingKey::Full(Box::new(ufvk)),
birthday,
)?;
Ok((account_id, usk)) 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()))))] #[tracing::instrument(skip_all, fields(height = blocks.first().map(|b| u32::from(b.height()))))]
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
fn put_blocks( 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> { fn put_received_transparent_utxo(
let tx = self.conn.transaction()?; &mut self,
wallet::scanning::update_chain_tip(&tx, &self.params, tip_height)?; _output: &WalletTransparentOutput,
tx.commit()?; ) -> Result<Self::UtxoRef, Self::Error> {
Ok(()) #[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( 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) 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> { impl<P: consensus::Parameters> WalletCommitmentTrees for WalletDb<rusqlite::Connection, P> {

View File

@ -67,7 +67,7 @@
use incrementalmerkletree::Retention; use incrementalmerkletree::Retention;
use rusqlite::{self, named_params, params, OptionalExtension}; use rusqlite::{self, named_params, params, OptionalExtension};
use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree}; use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree};
use std::borrow::Borrow;
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::TryFrom; use std::convert::TryFrom;
use std::io::{self, Cursor}; use std::io::{self, Cursor};
@ -83,7 +83,7 @@ use zcash_client_backend::{
address::{Address, UnifiedAddress}, address::{Address, UnifiedAddress},
data_api::{ data_api::{
scanning::{ScanPriority, ScanRange}, scanning::{ScanPriority, ScanRange},
AccountBalance, AccountBirthday, BlockMetadata, Ratio, SentTransactionOutput, AccountBalance, AccountBirthday, AccountKind, BlockMetadata, Ratio, SentTransactionOutput,
WalletSummary, SAPLING_SHARD_HEIGHT, WalletSummary, SAPLING_SHARD_HEIGHT,
}, },
encoding::AddressCodec, encoding::AddressCodec,
@ -140,84 +140,59 @@ pub(crate) mod scanning;
pub(crate) const BLOCK_SAPLING_FRONTIER_ABSENT: &[u8] = &[0x0]; pub(crate) const BLOCK_SAPLING_FRONTIER_ABSENT: &[u8] = &[0x0];
/// This tracks the allowed values of the `account_type` column of the `accounts` table fn parse_account_kind(
/// and should not be made public. account_kind: u32,
enum AccountType { hd_seed_fingerprint: Option<[u8; 32]>,
Zip32, hd_account_index: Option<u32>,
Imported, ) -> 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 { fn account_kind_code(value: AccountKind) -> u32 {
type Error = ();
fn try_from(value: u32) -> Result<Self, Self::Error> {
match value { match value {
0 => Ok(AccountType::Zip32), AccountKind::Derived { .. } => 0,
1 => Ok(AccountType::Imported), AccountKind::Imported => 1,
_ => Err(()),
}
} }
} }
impl From<AccountType> for u32 { /// The viewing key that an [`Account`] has available to it.
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.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(crate) struct HdSeedAccount( pub(crate) enum ViewingKey {
HdSeedFingerprint, /// A full viewing key.
zip32::AccountId, ///
Box<UnifiedFullViewingKey>, /// This is available to derived accounts, as well as accounts directly imported as
); /// full viewing keys.
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.
Full(Box<UnifiedFullViewingKey>), 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>), 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)] #[derive(Debug, Clone)]
pub(crate) enum Account { pub struct Account {
/// Inputs for a ZIP-32 HD account. account_id: AccountId,
Zip32(HdSeedAccount), kind: AccountKind,
/// Inputs for an imported account. viewing_key: ViewingKey,
Imported(ImportedAccount),
} }
impl Account { impl Account {
@ -226,14 +201,45 @@ impl Account {
/// ///
/// The diversifier index may be non-zero if the Unified Address includes a Sapling /// 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. /// receiver, and there was no valid Sapling receiver at diversifier index zero.
pub fn default_address( pub(crate) fn default_address(
&self, &self,
request: UnifiedAddressRequest, request: UnifiedAddressRequest,
) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> { ) -> 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 { match self {
Account::Zip32(HdSeedAccount(_, _, ufvk)) => ufvk.default_address(request), ViewingKey::Full(ufvk) => Some(ufvk),
Account::Imported(ImportedAccount::Full(ufvk)) => ufvk.default_address(request), ViewingKey::Incoming(_) => None,
Account::Imported(ImportedAccount::Incoming(uivk)) => uivk.default_address(request), }
}
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>( pub(crate) fn add_account<P: consensus::Parameters>(
conn: &rusqlite::Transaction, conn: &rusqlite::Transaction,
params: &P, params: &P,
account: Account, kind: AccountKind,
viewing_key: ViewingKey,
birthday: AccountBirthday, birthday: AccountBirthday,
) -> Result<AccountId, SqliteClientError> { ) -> 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 let orchard_item = viewing_key
.ufvk .ufvk()
.and_then(|ufvk| ufvk.orchard().map(|k| k.to_bytes())); .and_then(|ufvk| ufvk.orchard().map(|k| k.to_bytes()));
let sapling_item = args let sapling_item = viewing_key
.ufvk .ufvk()
.and_then(|ufvk| ufvk.sapling().map(|k| k.to_bytes())); .and_then(|ufvk| ufvk.sapling().map(|k| k.to_bytes()));
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
let transparent_item = args let transparent_item = viewing_key
.ufvk .ufvk()
.and_then(|ufvk| ufvk.transparent().map(|k| k.serialize())); .and_then(|ufvk| ufvk.transparent().map(|k| k.serialize()));
#[cfg(not(feature = "transparent-inputs"))] #[cfg(not(feature = "transparent-inputs"))]
let transparent_item: Option<Vec<u8>> = None; 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( let account_id: AccountId = conn.query_row(
r#" r#"
INSERT INTO accounts ( INSERT INTO accounts (
account_type, hd_seed_fingerprint, hd_account_index, account_kind, hd_seed_fingerprint, hd_account_index,
ufvk, uivk, ufvk, uivk,
orchard_fvk_item_cache, sapling_fvk_item_cache, p2pkh_fvk_item_cache, orchard_fvk_item_cache, sapling_fvk_item_cache, p2pkh_fvk_item_cache,
birthday_height, recover_until_height birthday_height, recover_until_height
) )
VALUES ( VALUES (
:account_type, :hd_seed_fingerprint, :hd_account_index, :account_kind, :hd_seed_fingerprint, :hd_account_index,
:ufvk, :uivk, :ufvk, :uivk,
:orchard_fvk_item_cache, :sapling_fvk_item_cache, :p2pkh_fvk_item_cache, :orchard_fvk_item_cache, :sapling_fvk_item_cache, :p2pkh_fvk_item_cache,
:birthday_height, :recover_until_height :birthday_height, :recover_until_height
@ -373,11 +344,11 @@ pub(crate) fn add_account<P: consensus::Parameters>(
RETURNING id; RETURNING id;
"#, "#,
named_params![ named_params![
":account_type": args.account_type, ":account_kind": account_kind_code(kind),
":hd_seed_fingerprint": args.hd_seed_fingerprint, ":hd_seed_fingerprint": hd_seed_fingerprint.as_ref().map(|fp| fp.as_bytes()),
":hd_account_index": args.hd_account_index, ":hd_account_index": hd_account_index.map(u32::from),
":ufvk": args.ufvk.map(|ufvk| ufvk.encode(params)), ":ufvk": viewing_key.ufvk().map(|ufvk| ufvk.encode(params)),
":uivk": args.uivk.to_uivk().encode(&params.network_type()), ":uivk": viewing_key.uivk()?.to_uivk().encode(&params.network_type()),
":orchard_fvk_item_cache": orchard_item, ":orchard_fvk_item_cache": orchard_item,
":sapling_fvk_item_cache": sapling_item, ":sapling_fvk_item_cache": sapling_item,
":p2pkh_fvk_item_cache": transparent_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)?)), |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 // 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. // birthday frontier is the empty frontier, we don't need to do anything.
if let Some(frontier) = birthday.sapling_frontier().value() { 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, conn: &rusqlite::Connection,
params: &P, params: &P,
ufvk: &UnifiedFullViewingKey, ufvk: &UnifiedFullViewingKey,
) -> Result<Option<(AccountId, Option<UnifiedFullViewingKey>)>, SqliteClientError> { ) -> Result<Option<Account>, SqliteClientError> {
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
let transparent_item = ufvk.transparent().map(|k| k.serialize()); let transparent_item = ufvk.transparent().map(|k| k.serialize());
#[cfg(not(feature = "transparent-inputs"))] #[cfg(not(feature = "transparent-inputs"))]
let transparent_item: Option<Vec<u8>> = None; let transparent_item: Option<Vec<u8>> = None;
let mut stmt = conn.prepare( let mut stmt = conn.prepare(
"SELECT id, ufvk "SELECT id, account_kind, hd_seed_fingerprint, hd_account_index, ufvk
FROM accounts FROM accounts
WHERE orchard_fvk_item_cache = :orchard_fvk_item_cache WHERE orchard_fvk_item_cache = :orchard_fvk_item_cache
OR sapling_fvk_item_cache = :sapling_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| { |row| {
let account_id = row.get::<_, u32>(0).map(AccountId)?; let account_id = row.get::<_, u32>(0).map(AccountId)?;
Ok(( let kind = parse_account_kind(row.get(1)?, row.get(2)?, row.get(3)?)?;
account_id,
row.get::<_, Option<String>>(1)? // We looked up the account by FVK components, so the UFVK column must be
.map(|ufvk_str| UnifiedFullViewingKey::decode(params, &ufvk_str)) // non-null.
.transpose() let ufvk_str: String = row.get(4)?;
.map_err(|e| { let viewing_key = ViewingKey::Full(Box::new(
UnifiedFullViewingKey::decode(params, &ufvk_str).map_err(|e| {
SqliteClientError::CorruptedData(format!( SqliteClientError::CorruptedData(format!(
"Could not decode unified full viewing key for account {:?}: {}", "Could not decode unified full viewing key for account {:?}: {}",
account_id, e account_id, e
)) ))
})?, })?,
)) ));
Ok(Account {
account_id,
kind,
viewing_key,
})
}, },
)? )?
.collect::<Result<Vec<_>, _>>()?; .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`] /// Returns the account id corresponding to a given [`HdSeedFingerprint`]
/// and [`zip32::AccountId`], if any. /// 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, conn: &rusqlite::Connection,
params: &P, params: &P,
seed: &HdSeedFingerprint, seed: &HdSeedFingerprint,
account_id: zip32::AccountId, account_index: zip32::AccountId,
) -> Result<Option<(AccountId, Option<UnifiedFullViewingKey>)>, SqliteClientError> { ) -> Result<Option<Account>, SqliteClientError> {
let mut stmt = conn.prepare( let mut stmt = conn.prepare(
"SELECT id, ufvk "SELECT id, ufvk
FROM accounts FROM accounts
@ -778,22 +762,30 @@ pub(crate) fn get_seed_account<P: consensus::Parameters>(
let mut accounts = stmt.query_and_then::<_, SqliteClientError, _, _>( let mut accounts = stmt.query_and_then::<_, SqliteClientError, _, _>(
named_params![ named_params![
":hd_seed_fingerprint": seed.as_bytes(), ":hd_seed_fingerprint": seed.as_bytes(),
":hd_account_index": u32::from(account_id), ":hd_account_index": u32::from(account_index),
], ],
|row| { |row| {
let account_id = row.get::<_, u32>(0).map(AccountId)?; 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, account_id,
row.get::<_, Option<String>>(1)? ))),
.map(|ufvk_str| UnifiedFullViewingKey::decode(params, &ufvk_str)) Some(ufvk_str) => UnifiedFullViewingKey::decode(params, &ufvk_str).map_err(|e| {
.transpose()
.map_err(|e| {
SqliteClientError::CorruptedData(format!( SqliteClientError::CorruptedData(format!(
"Could not decode unified full viewing key for account {:?}: {}", "Could not decode unified full viewing key for account {:?}: {}",
account_id, e 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>( pub(crate) fn get_account<P: Parameters>(
db: &WalletDb<C, P>, conn: &rusqlite::Connection,
params: &P,
account_id: AccountId, account_id: AccountId,
) -> Result<Option<Account>, SqliteClientError> { ) -> Result<Option<Account>, SqliteClientError> {
let mut sql = db.conn.borrow().prepare_cached( let mut sql = conn.prepare_cached(
r#" 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 FROM accounts
WHERE id = :account_id WHERE id = :account_id
"#, "#,
@ -1497,53 +1490,40 @@ pub(crate) fn get_account<C: Borrow<rusqlite::Connection>, P: Parameters>(
let row = result.next()?; let row = result.next()?;
match row { match row {
Some(row) => { Some(row) => {
let account_type: AccountType = let kind = parse_account_kind(
row.get::<_, u32>("account_type")?.try_into().map_err(|_| { row.get("account_kind")?,
SqliteClientError::CorruptedData("Unrecognized account_type".to_string()) row.get("hd_seed_fingerprint")?,
})?; row.get("hd_account_index")?,
)?;
let ufvk_str: Option<String> = row.get("ufvk")?; let ufvk_str: Option<String> = row.get("ufvk")?;
let ufvk = if let Some(ufvk_str) = ufvk_str { let viewing_key = if let Some(ufvk_str) = ufvk_str {
Some( ViewingKey::Full(Box::new(
UnifiedFullViewingKey::decode(&db.params, &ufvk_str[..]) UnifiedFullViewingKey::decode(params, &ufvk_str[..])
.map_err(SqliteClientError::BadAccountData)?, .map_err(SqliteClientError::BadAccountData)?,
) ))
} else { } else {
None
};
let uivk_str: String = row.get("uivk")?; let uivk_str: String = row.get("uivk")?;
let (network, uivk) = Uivk::decode(&uivk_str).map_err(|e| { let (network, uivk) = Uivk::decode(&uivk_str).map_err(|e| {
SqliteClientError::CorruptedData(format!("Failure to decode UIVK: {e}")) SqliteClientError::CorruptedData(format!("Failure to decode UIVK: {e}"))
})?; })?;
if network != db.params.network_type() { if network != params.network_type() {
return Err(SqliteClientError::CorruptedData( return Err(SqliteClientError::CorruptedData(
"UIVK network type does not match wallet network type".to_string(), "UIVK network type does not match wallet network type".to_string(),
)); ));
} }
let uivk = UnifiedIncomingViewingKey::from_uivk(&uivk) ViewingKey::Incoming(Box::new(
.map_err(|e| SqliteClientError::CorruptedData(e.to_string()))?; UnifiedIncomingViewingKey::from_uivk(&uivk).map_err(|e| {
SqliteClientError::CorruptedData(format!("Failure to decode UIVK: {e}"))
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(),
)
})?, })?,
)))), ))
AccountType::Imported => Ok(Some(Account::Imported(if let Some(ufvk) = ufvk { };
ImportedAccount::Full(Box::new(ufvk))
} else { Ok(Some(Account {
ImportedAccount::Incoming(Box::new(uivk)) account_id,
}))), kind,
} viewing_key,
}))
} }
None => Ok(None), None => Ok(None),
} }
@ -2703,12 +2683,11 @@ mod tests {
use std::num::NonZeroU32; use std::num::NonZeroU32;
use sapling::zip32::ExtendedSpendingKey; 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 zcash_primitives::{block::BlockHash, transaction::components::amount::NonNegativeAmount};
use crate::{ use crate::{
testing::{AddressType, BlockCache, TestBuilder, TestState}, testing::{AddressType, BlockCache, TestBuilder, TestState},
wallet::{get_account, Account},
AccountId, AccountId,
}; };
@ -2856,12 +2835,12 @@ mod tests {
.with_test_account(AccountBirthday::from_sapling_activation) .with_test_account(AccountBirthday::from_sapling_activation)
.build(); .build();
let account_id = st.test_account().unwrap().0; 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(); let expected_account_index = zip32::AccountId::try_from(0).unwrap();
assert_matches!( assert_matches!(
account_parameters, account_parameters.kind,
Account::Zip32(hdaccount) if hdaccount.account_index() == expected_account_index AccountKind::Derived{account_index, ..} if account_index == expected_account_index
); );
} }

View File

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

View File

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

View File

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