use nonempty::NonEmpty;
use secrecy::SecretVec;
use std::{
collections::{HashMap, HashSet},
fmt::{self, Debug},
hash::Hash,
io,
num::{NonZeroU32, TryFromIntError},
};
use incrementalmerkletree::{Retention, frontier::Frontier};
use shardtree::{ShardTree, error::ShardTreeError, store::ShardStore};
use zcash_keys::{
address::{Address, UnifiedAddress},
keys::{
UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedIncomingViewingKey, UnifiedSpendingKey,
},
};
use zcash_primitives::{block::BlockHash, transaction::Transaction};
use zcash_protocol::{
PoolType, ShieldedPool, TxId,
consensus::{self, BlockHeight, TxIndex},
memo::{Memo, MemoBytes},
value::{BalanceError, Zatoshis},
};
use zip32::{DiversifierIndex, fingerprint::SeedFingerprint};
use self::{
chain::{ChainState, CommitmentTreeRoot},
scanning::{ScanPriority, ScanRange},
};
use crate::{
data_api::{
error::RewindError,
wallet::{ConfirmationsPolicy, TargetHeight, input_selection::LockFilter},
},
decrypt::DecryptedOutput,
proto::service::TreeState,
wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput, WalletTx},
};
#[cfg(feature = "transparent-inputs")]
use {
crate::{fees::StandardFeeRule, wallet::TransparentAddressMetadata},
getset::{CopyGetters, Getters},
std::time::SystemTime,
transparent::{address::TransparentAddress, bundle::OutPoint, keys::TransparentKeyScope},
};
#[cfg(all(
feature = "transparent-inputs",
any(test, feature = "test-dependencies")
))]
use {std::ops::Range, transparent::keys::NonHardenedChildIndex};
#[cfg(feature = "zcashd-compat")]
use zcash_keys::keys::zcashd;
#[cfg(feature = "test-dependencies")]
use ambassador::delegatable_trait;
#[cfg(any(test, feature = "test-dependencies"))]
use zcash_protocol::consensus::NetworkUpgrade;
pub mod anchor_retention;
pub mod chain;
pub mod defaults;
pub mod error;
pub mod ll;
pub mod locking;
pub use locking::OutputLockStore;
#[cfg(feature = "test-dependencies")]
pub use locking::ambassador_impl_OutputLockStore;
pub mod scanning;
pub mod wallet;
#[cfg(feature = "orchard")]
pub mod zip318;
#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing;
#[cfg(feature = "transparent-inputs")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TransparentKeyOrigin {
Imported,
Derived { scope: TransparentKeyScope },
}
#[cfg(feature = "transparent-inputs")]
pub type TransparentBalances = HashMap<TransparentAddress, (TransparentKeyOrigin, Balance)>;
pub const SAPLING_SHARD_HEIGHT: u8 = sapling::NOTE_COMMITMENT_TREE_DEPTH / 2;
#[cfg(feature = "orchard")]
pub const ORCHARD_SHARD_HEIGHT: u8 = { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 } / 2;
#[cfg(feature = "orchard")]
pub const IRONWOOD_SHARD_HEIGHT: u8 = { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 } / 2;
pub enum NullifierQuery {
Unspent,
All,
}
#[derive(Debug, Clone, Copy)]
pub enum TargetValue {
AtLeast(Zatoshis),
AllFunds(MaxSpendMode),
}
#[derive(Debug, Clone, Copy)]
pub enum MaxSpendMode {
MaxSpendable,
Everything,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Balance {
spendable_value: Zatoshis,
locked_value: Zatoshis,
change_pending_confirmation: Zatoshis,
value_pending_spendability: Zatoshis,
uneconomic_value: Zatoshis,
}
impl Balance {
pub const ZERO: Self = Self {
spendable_value: Zatoshis::ZERO,
locked_value: Zatoshis::ZERO,
change_pending_confirmation: Zatoshis::ZERO,
value_pending_spendability: Zatoshis::ZERO,
uneconomic_value: Zatoshis::ZERO,
};
fn check_total_adding(&self, value: Zatoshis) -> Result<Zatoshis, BalanceError> {
(self.spendable_value
+ self.locked_value
+ self.change_pending_confirmation
+ self.value_pending_spendability
+ value)
.ok_or(BalanceError::Overflow)
}
pub fn spendable_value(&self) -> Zatoshis {
self.spendable_value
}
pub fn locked_value(&self) -> Zatoshis {
self.locked_value
}
pub fn add_spendable_value(&mut self, value: Zatoshis) -> Result<(), BalanceError> {
self.check_total_adding(value)?;
self.spendable_value = (self.spendable_value + value).unwrap();
Ok(())
}
pub fn add_locked_value(&mut self, value: Zatoshis) -> Result<(), BalanceError> {
self.check_total_adding(value)?;
self.locked_value = (self.locked_value + value).unwrap();
Ok(())
}
pub fn change_pending_confirmation(&self) -> Zatoshis {
self.change_pending_confirmation
}
pub fn add_pending_change_value(&mut self, value: Zatoshis) -> Result<(), BalanceError> {
self.check_total_adding(value)?;
self.change_pending_confirmation = (self.change_pending_confirmation + value).unwrap();
Ok(())
}
pub fn value_pending_spendability(&self) -> Zatoshis {
self.value_pending_spendability
}
pub fn add_pending_spendable_value(&mut self, value: Zatoshis) -> Result<(), BalanceError> {
self.check_total_adding(value)?;
self.value_pending_spendability = (self.value_pending_spendability + value).unwrap();
Ok(())
}
pub fn uneconomic_value(&self) -> Zatoshis {
self.uneconomic_value
}
pub fn add_uneconomic_value(&mut self, value: Zatoshis) -> Result<(), BalanceError> {
self.uneconomic_value = (self.uneconomic_value + value).ok_or(BalanceError::Overflow)?;
Ok(())
}
pub fn total(&self) -> Zatoshis {
(self.spendable_value
+ self.locked_value
+ self.change_pending_confirmation
+ self.value_pending_spendability)
.expect("Balance cannot overflow MAX_MONEY")
}
}
impl core::ops::Add<Balance> for Balance {
type Output = Result<Balance, BalanceError>;
fn add(self, rhs: Balance) -> Self::Output {
let result = Balance {
spendable_value: (self.spendable_value + rhs.spendable_value)
.ok_or(BalanceError::Overflow)?,
locked_value: (self.locked_value + rhs.locked_value).ok_or(BalanceError::Overflow)?,
change_pending_confirmation: (self.change_pending_confirmation
+ rhs.change_pending_confirmation)
.ok_or(BalanceError::Overflow)?,
value_pending_spendability: (self.value_pending_spendability
+ rhs.value_pending_spendability)
.ok_or(BalanceError::Overflow)?,
uneconomic_value: (self.uneconomic_value + rhs.uneconomic_value)
.ok_or(BalanceError::Overflow)?,
};
result.check_total_adding(Zatoshis::ZERO)?;
Ok(result)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AccountBalance {
sapling_balance: Balance,
orchard_balance: Balance,
ironwood_balance: Balance,
unshielded_regular_balance: Balance,
unshielded_coinbase_balance: Balance,
}
impl AccountBalance {
pub const ZERO: Self = Self {
sapling_balance: Balance::ZERO,
orchard_balance: Balance::ZERO,
ironwood_balance: Balance::ZERO,
unshielded_regular_balance: Balance::ZERO,
unshielded_coinbase_balance: Balance::ZERO,
};
fn check_total(&self) -> Result<Zatoshis, BalanceError> {
(self.sapling_balance.total()
+ self.orchard_balance.total()
+ self.ironwood_balance.total()
+ self.unshielded_regular_balance.total()
+ self.unshielded_coinbase_balance.total())
.ok_or(BalanceError::Overflow)
}
pub fn sapling_balance(&self) -> &Balance {
&self.sapling_balance
}
pub fn with_sapling_balance_mut<A, E: From<BalanceError>>(
&mut self,
f: impl FnOnce(&mut Balance) -> Result<A, E>,
) -> Result<A, E> {
let result = f(&mut self.sapling_balance)?;
self.check_total()?;
Ok(result)
}
pub fn orchard_balance(&self) -> &Balance {
&self.orchard_balance
}
pub fn with_orchard_balance_mut<A, E: From<BalanceError>>(
&mut self,
f: impl FnOnce(&mut Balance) -> Result<A, E>,
) -> Result<A, E> {
let result = f(&mut self.orchard_balance)?;
self.check_total()?;
Ok(result)
}
pub fn ironwood_balance(&self) -> &Balance {
&self.ironwood_balance
}
pub fn with_ironwood_balance_mut<A, E: From<BalanceError>>(
&mut self,
f: impl FnOnce(&mut Balance) -> Result<A, E>,
) -> Result<A, E> {
let result = f(&mut self.ironwood_balance)?;
self.check_total()?;
Ok(result)
}
#[deprecated(
note = "this function is deprecated. Please use [`AccountBalance::unshielded_regular_balance`] and [`AccountBalance::unshielded_coinbase_balance`] instead."
)]
pub fn unshielded(&self) -> Zatoshis {
(self.unshielded_regular_balance.total() + self.unshielded_coinbase_balance.total())
.expect("Account balance cannot overflow MAX_MONEY")
}
pub fn unshielded_balance(&self) -> Balance {
(self.unshielded_regular_balance + self.unshielded_coinbase_balance)
.expect("Account balance cannot overflow MAX_MONEY")
}
pub fn unshielded_regular_balance(&self) -> &Balance {
&self.unshielded_regular_balance
}
pub fn with_unshielded_regular_balance_mut<A, E: From<BalanceError>>(
&mut self,
f: impl FnOnce(&mut Balance) -> Result<A, E>,
) -> Result<A, E> {
let result = f(&mut self.unshielded_regular_balance)?;
self.check_total()?;
Ok(result)
}
pub fn unshielded_coinbase_balance(&self) -> &Balance {
&self.unshielded_coinbase_balance
}
pub fn with_unshielded_coinbase_balance_mut<A, E: From<BalanceError>>(
&mut self,
f: impl FnOnce(&mut Balance) -> Result<A, E>,
) -> Result<A, E> {
let result = f(&mut self.unshielded_coinbase_balance)?;
self.check_total()?;
Ok(result)
}
pub fn total(&self) -> Zatoshis {
(self.sapling_balance.total()
+ self.orchard_balance.total()
+ self.ironwood_balance.total()
+ self.unshielded_regular_balance.total()
+ self.unshielded_coinbase_balance.total())
.expect("Account balance cannot overflow MAX_MONEY")
}
pub fn spendable_value(&self) -> Zatoshis {
(self.sapling_balance.spendable_value
+ self.orchard_balance.spendable_value
+ self.ironwood_balance.spendable_value)
.expect("Account balance cannot overflow MAX_MONEY")
}
pub fn locked_value(&self) -> Zatoshis {
(self.sapling_balance.locked_value()
+ self.orchard_balance.locked_value()
+ self.ironwood_balance.locked_value()
+ self.unshielded_regular_balance.locked_value()
+ self.unshielded_coinbase_balance.locked_value())
.expect("Account balance cannot overflow MAX_MONEY")
}
pub fn change_pending_confirmation(&self) -> Zatoshis {
(self.sapling_balance.change_pending_confirmation
+ self.orchard_balance.change_pending_confirmation
+ self.ironwood_balance.change_pending_confirmation)
.expect("Account balance cannot overflow MAX_MONEY")
}
pub fn value_pending_spendability(&self) -> Zatoshis {
(self.sapling_balance.value_pending_spendability
+ self.orchard_balance.value_pending_spendability
+ self.ironwood_balance.value_pending_spendability)
.expect("Account balance cannot overflow MAX_MONEY")
}
pub fn uneconomic_value(&self) -> Zatoshis {
(self.sapling_balance.uneconomic_value
+ self.orchard_balance.uneconomic_value
+ self.ironwood_balance.uneconomic_value
+ self.unshielded_regular_balance.uneconomic_value
+ self.unshielded_coinbase_balance.uneconomic_value)
.expect("Account balance cannot overflow MAX_MONEY")
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Zip32Derivation {
seed_fingerprint: SeedFingerprint,
account_index: zip32::AccountId,
#[cfg(feature = "zcashd-compat")]
legacy_address_index: Option<zcashd::LegacyAddressIndex>,
}
impl Zip32Derivation {
pub fn new(
seed_fingerprint: SeedFingerprint,
account_index: zip32::AccountId,
#[cfg(feature = "zcashd-compat")] legacy_address_index: Option<zcashd::LegacyAddressIndex>,
) -> Self {
Self {
seed_fingerprint,
account_index,
#[cfg(feature = "zcashd-compat")]
legacy_address_index,
}
}
pub fn seed_fingerprint(&self) -> &SeedFingerprint {
&self.seed_fingerprint
}
pub fn account_index(&self) -> zip32::AccountId {
self.account_index
}
#[cfg(feature = "zcashd-compat")]
pub fn legacy_address_index(&self) -> Option<zcashd::LegacyAddressIndex> {
self.legacy_address_index
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum AccountPurpose {
Spending { derivation: Option<Zip32Derivation> },
ViewOnly,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum AccountSource {
Derived {
derivation: Zip32Derivation,
key_source: Option<String>,
},
Imported {
purpose: AccountPurpose,
key_source: Option<String>,
},
}
impl AccountSource {
pub fn key_derivation(&self) -> Option<&Zip32Derivation> {
match self {
AccountSource::Derived { derivation, .. } => Some(derivation),
AccountSource::Imported {
purpose: AccountPurpose::Spending { derivation },
..
} => derivation.as_ref(),
_ => None,
}
}
pub fn key_source(&self) -> Option<&str> {
match self {
AccountSource::Derived { key_source, .. } => key_source.as_ref().map(|s| s.as_str()),
AccountSource::Imported { key_source, .. } => key_source.as_ref().map(|s| s.as_str()),
}
}
}
pub trait Account {
type AccountId: Copy;
fn id(&self) -> Self::AccountId;
fn name(&self) -> Option<&str>;
fn birthday_height(&self) -> BlockHeight;
fn source(&self) -> &AccountSource;
fn purpose(&self) -> AccountPurpose {
match self.source() {
AccountSource::Derived { derivation, .. } => AccountPurpose::Spending {
derivation: Some(derivation.clone()),
},
AccountSource::Imported { purpose, .. } => purpose.clone(),
}
}
fn ufvk(&self) -> Option<&UnifiedFullViewingKey>;
fn uivk(&self) -> UnifiedIncomingViewingKey;
}
#[cfg(any(test, feature = "test-dependencies"))]
impl<A: Copy> Account for (A, UnifiedFullViewingKey, BlockHeight) {
type AccountId = A;
fn id(&self) -> A {
self.0
}
fn name(&self) -> Option<&str> {
None
}
fn birthday_height(&self) -> BlockHeight {
self.2
}
fn source(&self) -> &AccountSource {
&AccountSource::Imported {
purpose: AccountPurpose::ViewOnly,
key_source: None,
}
}
fn ufvk(&self) -> Option<&UnifiedFullViewingKey> {
Some(&self.1)
}
fn uivk(&self) -> UnifiedIncomingViewingKey {
self.1.to_unified_incoming_viewing_key()
}
}
#[cfg(any(test, feature = "test-dependencies"))]
impl<A: Copy> Account for (A, UnifiedIncomingViewingKey, BlockHeight) {
type AccountId = A;
fn id(&self) -> A {
self.0
}
fn name(&self) -> Option<&str> {
None
}
fn birthday_height(&self) -> BlockHeight {
self.2
}
fn source(&self) -> &AccountSource {
&AccountSource::Imported {
purpose: AccountPurpose::ViewOnly,
key_source: None,
}
}
fn ufvk(&self) -> Option<&UnifiedFullViewingKey> {
None
}
fn uivk(&self) -> UnifiedIncomingViewingKey {
self.1.clone()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AddressSource {
Derived {
diversifier_index: DiversifierIndex,
#[cfg(feature = "transparent-inputs")]
transparent_key_scope: Option<TransparentKeyScope>,
},
#[cfg(feature = "transparent-key-import")]
Standalone,
}
impl AddressSource {
#[cfg(feature = "transparent-inputs")]
pub fn transparent_key_scope(&self) -> Option<&TransparentKeyScope> {
match self {
AddressSource::Derived {
transparent_key_scope,
..
} => transparent_key_scope.as_ref(),
#[cfg(feature = "transparent-key-import")]
AddressSource::Standalone => None,
}
}
}
#[derive(Clone)]
pub struct AddressInfo {
address: Address,
source: AddressSource,
}
impl AddressInfo {
pub fn from_parts(address: Address, source: AddressSource) -> Option<Self> {
#[cfg(feature = "transparent-inputs")]
let valid = source.transparent_key_scope().is_none()
|| matches!(address, Address::Transparent(_) | Address::Tex(_));
#[cfg(not(feature = "transparent-inputs"))]
let valid = true;
valid.then_some(Self { address, source })
}
pub fn address(&self) -> &Address {
&self.address
}
pub fn source(&self) -> AddressSource {
self.source
}
#[cfg(feature = "transparent-inputs")]
#[deprecated(
since = "0.20.0",
note = "use AddressSource::transparent_key_scope instead"
)]
pub fn transparent_key_scope(&self) -> Option<&TransparentKeyScope> {
self.source.transparent_key_scope()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Ratio<T> {
numerator: T,
denominator: T,
}
impl<T> Ratio<T> {
pub fn new(numerator: T, denominator: T) -> Self {
Self {
numerator,
denominator,
}
}
pub fn numerator(&self) -> &T {
&self.numerator
}
pub fn denominator(&self) -> &T {
&self.denominator
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Progress {
scan: Ratio<u64>,
recovery: Option<Ratio<u64>>,
}
impl Progress {
pub fn new(scan: Ratio<u64>, recovery: Option<Ratio<u64>>) -> Self {
Self { scan, recovery }
}
pub fn scan(&self) -> Ratio<u64> {
self.scan
}
pub fn recovery(&self) -> Option<Ratio<u64>> {
self.recovery
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WalletSummary<AccountId: Eq + Hash> {
account_balances: HashMap<AccountId, AccountBalance>,
chain_tip_height: BlockHeight,
fully_scanned_height: BlockHeight,
progress: Progress,
next_sapling_subtree_index: u64,
#[cfg(feature = "orchard")]
next_orchard_subtree_index: u64,
#[cfg(feature = "orchard")]
next_ironwood_subtree_index: u64,
}
impl<AccountId: Eq + Hash> WalletSummary<AccountId> {
pub fn new(
account_balances: HashMap<AccountId, AccountBalance>,
chain_tip_height: BlockHeight,
fully_scanned_height: BlockHeight,
progress: Progress,
next_sapling_subtree_index: u64,
#[cfg(feature = "orchard")] next_orchard_subtree_index: u64,
#[cfg(feature = "orchard")] next_ironwood_subtree_index: u64,
) -> Self {
Self {
account_balances,
chain_tip_height,
fully_scanned_height,
progress,
next_sapling_subtree_index,
#[cfg(feature = "orchard")]
next_orchard_subtree_index,
#[cfg(feature = "orchard")]
next_ironwood_subtree_index,
}
}
pub fn account_balances(&self) -> &HashMap<AccountId, AccountBalance> {
&self.account_balances
}
pub fn chain_tip_height(&self) -> BlockHeight {
self.chain_tip_height
}
pub fn fully_scanned_height(&self) -> BlockHeight {
self.fully_scanned_height
}
pub fn progress(&self) -> Progress {
self.progress
}
pub fn next_sapling_subtree_index(&self) -> u64 {
self.next_sapling_subtree_index
}
#[cfg(feature = "orchard")]
pub fn next_orchard_subtree_index(&self) -> u64 {
self.next_orchard_subtree_index
}
#[cfg(feature = "orchard")]
pub fn next_ironwood_subtree_index(&self) -> u64 {
self.next_ironwood_subtree_index
}
pub fn is_synced(&self) -> bool {
self.chain_tip_height == self.fully_scanned_height
}
}
pub trait NoteRetention<NoteRef> {
fn should_retain_sapling(&self, note: &ReceivedNote<NoteRef, sapling::Note>) -> bool;
#[cfg(feature = "orchard")]
fn should_retain_orchard(&self, note: &ReceivedNote<NoteRef, orchard::note::Note>) -> bool;
#[cfg(feature = "orchard")]
fn should_retain_ironwood(&self, note: &ReceivedNote<NoteRef, orchard::note::Note>) -> bool;
}
pub(crate) struct SimpleNoteRetention {
pub(crate) sapling: bool,
#[cfg(feature = "orchard")]
pub(crate) orchard: bool,
#[cfg(feature = "orchard")]
pub(crate) ironwood: bool,
}
impl<NoteRef> NoteRetention<NoteRef> for SimpleNoteRetention {
fn should_retain_sapling(&self, _: &ReceivedNote<NoteRef, sapling::Note>) -> bool {
self.sapling
}
#[cfg(feature = "orchard")]
fn should_retain_orchard(&self, _: &ReceivedNote<NoteRef, orchard::note::Note>) -> bool {
self.orchard
}
#[cfg(feature = "orchard")]
fn should_retain_ironwood(&self, _: &ReceivedNote<NoteRef, orchard::note::Note>) -> bool {
self.ironwood
}
}
#[derive(Debug)]
pub struct ReceivedNotes<NoteRef> {
sapling: Vec<ReceivedNote<NoteRef, sapling::Note>>,
#[cfg(feature = "orchard")]
orchard: Vec<ReceivedNote<NoteRef, orchard::note::Note>>,
#[cfg(feature = "orchard")]
ironwood: Vec<ReceivedNote<NoteRef, orchard::note::Note>>,
}
#[derive(Debug)]
pub struct ConsolidationNotes<NoteRef> {
funding: ReceivedNotes<NoteRef>,
additional: ReceivedNotes<NoteRef>,
}
impl<NoteRef> ConsolidationNotes<NoteRef> {
pub fn from_parts(funding: ReceivedNotes<NoteRef>, additional: ReceivedNotes<NoteRef>) -> Self {
Self {
funding,
additional,
}
}
pub fn into_parts(self) -> (ReceivedNotes<NoteRef>, ReceivedNotes<NoteRef>) {
(self.funding, self.additional)
}
}
impl<NoteRef> ReceivedNotes<NoteRef> {
pub fn empty() -> Self {
Self::new(
vec![],
#[cfg(feature = "orchard")]
vec![],
#[cfg(feature = "orchard")]
vec![],
)
}
pub fn new(
sapling: Vec<ReceivedNote<NoteRef, sapling::Note>>,
#[cfg(feature = "orchard")] orchard: Vec<ReceivedNote<NoteRef, orchard::note::Note>>,
#[cfg(feature = "orchard")] ironwood: Vec<ReceivedNote<NoteRef, orchard::note::Note>>,
) -> Self {
Self {
sapling,
#[cfg(feature = "orchard")]
orchard,
#[cfg(feature = "orchard")]
ironwood,
}
}
pub fn sapling(&self) -> &[ReceivedNote<NoteRef, sapling::Note>] {
self.sapling.as_ref()
}
pub fn take_sapling(self) -> Vec<ReceivedNote<NoteRef, sapling::Note>> {
self.sapling
}
#[cfg(feature = "orchard")]
pub fn orchard(&self) -> &[ReceivedNote<NoteRef, orchard::note::Note>] {
self.orchard.as_ref()
}
#[cfg(feature = "orchard")]
pub fn take_orchard(self) -> Vec<ReceivedNote<NoteRef, orchard::note::Note>> {
self.orchard
}
#[cfg(feature = "orchard")]
pub fn ironwood(&self) -> &[ReceivedNote<NoteRef, orchard::note::Note>] {
self.ironwood.as_ref()
}
#[cfg(feature = "orchard")]
pub fn take_ironwood(self) -> Vec<ReceivedNote<NoteRef, orchard::note::Note>> {
self.ironwood
}
pub fn sapling_value(&self) -> Result<Zatoshis, BalanceError> {
self.sapling.iter().try_fold(Zatoshis::ZERO, |acc, n| {
(acc + n.note_value()?).ok_or(BalanceError::Overflow)
})
}
#[cfg(feature = "orchard")]
pub fn orchard_value(&self) -> Result<Zatoshis, BalanceError> {
self.orchard.iter().try_fold(Zatoshis::ZERO, |acc, n| {
(acc + n.note_value()?).ok_or(BalanceError::Overflow)
})
}
#[cfg(feature = "orchard")]
pub fn ironwood_value(&self) -> Result<Zatoshis, BalanceError> {
self.ironwood.iter().try_fold(Zatoshis::ZERO, |acc, n| {
(acc + n.note_value()?).ok_or(BalanceError::Overflow)
})
}
pub fn total_value(&self) -> Result<Zatoshis, BalanceError> {
#[cfg(not(feature = "orchard"))]
return self.sapling_value();
#[cfg(feature = "orchard")]
return (self.sapling_value()? + self.orchard_value()? + self.ironwood_value()?)
.ok_or(BalanceError::Overflow);
}
pub fn is_empty(&self) -> bool {
#[cfg(not(feature = "orchard"))]
return self.sapling.is_empty();
#[cfg(feature = "orchard")]
return self.sapling.is_empty() && self.orchard.is_empty() && self.ironwood.is_empty();
}
pub(crate) fn append(&mut self, mut other: Self) {
self.sapling.append(&mut other.sapling);
#[cfg(feature = "orchard")]
{
self.orchard.append(&mut other.orchard);
self.ironwood.append(&mut other.ironwood);
}
}
pub fn into_single_covering(mut self, value: Zatoshis, sources: &[ShieldedPool]) -> Self {
fn take_oldest_covering<NoteRef, N>(
notes: &mut Vec<ReceivedNote<NoteRef, N>>,
covers: impl Fn(&ReceivedNote<NoteRef, N>) -> bool,
) -> Option<ReceivedNote<NoteRef, N>> {
let idx = notes
.iter()
.enumerate()
.filter(|(_, n)| covers(n))
.min_by_key(|(_, n)| n.note_commitment_tree_position())
.map(|(idx, _)| idx)?;
Some(notes.swap_remove(idx))
}
for pool in sources {
match pool {
ShieldedPool::Sapling => {
if let Some(note) = take_oldest_covering(&mut self.sapling, |n| {
n.note_value().is_ok_and(|v| v >= value)
}) {
return Self::new(
vec![note],
#[cfg(feature = "orchard")]
vec![],
#[cfg(feature = "orchard")]
vec![],
);
}
}
#[cfg(feature = "orchard")]
ShieldedPool::Orchard => {
if let Some(note) = take_oldest_covering(&mut self.orchard, |n| {
n.note_value().is_ok_and(|v| v >= value)
}) {
return Self::new(vec![], vec![note], vec![]);
}
}
#[cfg(feature = "orchard")]
ShieldedPool::Ironwood => {
if let Some(note) = take_oldest_covering(&mut self.ironwood, |n| {
n.note_value().is_ok_and(|v| v >= value)
}) {
return Self::new(vec![], vec![], vec![note]);
}
}
#[cfg(not(feature = "orchard"))]
ShieldedPool::Orchard | ShieldedPool::Ironwood => {}
}
}
Self::empty()
}
pub fn into_vec(
self,
retention: &impl NoteRetention<NoteRef>,
) -> Vec<ReceivedNote<NoteRef, Note>> {
let iter = self.sapling.into_iter().filter_map(|n| {
retention
.should_retain_sapling(&n)
.then(|| n.map_note(Note::Sapling))
});
#[cfg(feature = "orchard")]
let iter = iter.chain(self.orchard.into_iter().filter_map(|n| {
retention.should_retain_orchard(&n).then(|| {
n.map_note(|note| Note::Orchard {
note,
pool: orchard::ValuePool::Orchard,
})
})
}));
#[cfg(feature = "orchard")]
let iter = iter.chain(self.ironwood.into_iter().filter_map(|n| {
retention.should_retain_ironwood(&n).then(|| {
n.map_note(|note| Note::Orchard {
note,
pool: orchard::ValuePool::Ironwood,
})
})
}));
iter.collect()
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg(feature = "transparent-inputs")]
pub enum TransactionStatusFilter {
Mined,
Mempool,
All,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg(feature = "transparent-inputs")]
pub enum OutputStatusFilter {
Unspent,
All,
}
#[cfg(feature = "transparent-inputs")]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Getters, CopyGetters)]
pub struct TransactionsInvolvingAddress {
#[getset(get_copy = "pub")]
address: TransparentAddress,
#[getset(get_copy = "pub")]
block_range_start: BlockHeight,
#[getset(get_copy = "pub")]
block_range_end: Option<BlockHeight>,
#[getset(get_copy = "pub")]
request_at: Option<SystemTime>,
#[getset(get = "pub")]
tx_status_filter: TransactionStatusFilter,
#[getset(get = "pub")]
output_status_filter: OutputStatusFilter,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum TransactionDataRequest {
GetStatus(TxId),
Enhancement(TxId),
#[cfg(feature = "transparent-inputs")]
TransactionsInvolvingAddress(TransactionsInvolvingAddress),
#[cfg(feature = "spend-index")]
GetSpendingTx(OutPoint),
}
impl TransactionDataRequest {
#[cfg(feature = "transparent-inputs")]
pub fn transactions_involving_address(
address: TransparentAddress,
block_range_start: BlockHeight,
block_range_end: Option<BlockHeight>,
request_at: Option<SystemTime>,
tx_status_filter: TransactionStatusFilter,
output_status_filter: OutputStatusFilter,
) -> Self {
TransactionDataRequest::TransactionsInvolvingAddress(TransactionsInvolvingAddress {
address,
block_range_start,
block_range_end,
request_at,
tx_status_filter,
output_status_filter,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TransactionStatus {
TxidNotRecognized,
NotInMainChain,
Mined(BlockHeight),
}
#[derive(Debug, Clone)]
pub struct PoolMeta {
note_count: usize,
value: Zatoshis,
}
impl PoolMeta {
pub fn new(note_count: usize, value: Zatoshis) -> Self {
Self { note_count, value }
}
pub fn note_count(&self) -> usize {
self.note_count
}
pub fn value(&self) -> Zatoshis {
self.value
}
}
#[derive(Debug, Clone)]
pub struct AccountMeta {
sapling: Option<PoolMeta>,
orchard: Option<PoolMeta>,
ironwood: Option<PoolMeta>,
}
impl AccountMeta {
pub fn new(
sapling: Option<PoolMeta>,
orchard: Option<PoolMeta>,
ironwood: Option<PoolMeta>,
) -> Self {
Self {
sapling,
orchard,
ironwood,
}
}
pub fn sapling(&self) -> Option<&PoolMeta> {
self.sapling.as_ref()
}
pub fn orchard(&self) -> Option<&PoolMeta> {
self.orchard.as_ref()
}
pub fn ironwood(&self) -> Option<&PoolMeta> {
self.ironwood.as_ref()
}
fn sapling_note_count(&self) -> Option<usize> {
self.sapling.as_ref().map(|m| m.note_count)
}
fn orchard_note_count(&self) -> Option<usize> {
self.orchard.as_ref().map(|m| m.note_count)
}
fn ironwood_note_count(&self) -> Option<usize> {
self.ironwood.as_ref().map(|m| m.note_count)
}
pub fn note_count(&self, protocol: ShieldedPool) -> Option<usize> {
match protocol {
ShieldedPool::Sapling => self.sapling_note_count(),
ShieldedPool::Orchard => self.orchard_note_count(),
ShieldedPool::Ironwood => self.ironwood_note_count(),
}
}
pub fn total_note_count(&self) -> Option<usize> {
[
self.sapling_note_count(),
self.orchard_note_count(),
self.ironwood_note_count(),
]
.into_iter()
.flatten()
.reduce(|a, b| a + b)
}
fn sapling_value(&self) -> Option<Zatoshis> {
self.sapling.as_ref().map(|m| m.value)
}
fn orchard_value(&self) -> Option<Zatoshis> {
self.orchard.as_ref().map(|m| m.value)
}
fn ironwood_value(&self) -> Option<Zatoshis> {
self.ironwood.as_ref().map(|m| m.value)
}
pub fn total_value(&self) -> Option<Zatoshis> {
[
self.sapling_value(),
self.orchard_value(),
self.ironwood_value(),
]
.into_iter()
.flatten()
.reduce(|a, b| (a + b).expect("Does not overflow Zcash maximum value."))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct BoundedU8<const MAX: u8>(u8);
impl<const MAX: u8> BoundedU8<MAX> {
pub const fn new_const(value: u8) -> Self {
assert!(value <= MAX);
Self(value)
}
pub fn new(value: u8) -> Option<Self> {
if value <= MAX {
Some(Self(value))
} else {
None
}
}
pub fn value(&self) -> u8 {
self.0
}
}
impl<const MAX: u8> From<BoundedU8<MAX>> for u8 {
fn from(value: BoundedU8<MAX>) -> Self {
value.0
}
}
impl<const MAX: u8> From<BoundedU8<MAX>> for usize {
fn from(value: BoundedU8<MAX>) -> Self {
usize::from(value.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum NoteFilter {
ExceedsMinValue(Zatoshis),
ExceedsPriorSendPercentile(BoundedU8<99>),
ExceedsBalancePercentage(BoundedU8<99>),
Combine(Box<NoteFilter>, Box<NoteFilter>),
Attempt {
condition: Box<NoteFilter>,
fallback: Box<NoteFilter>,
},
}
impl NoteFilter {
pub fn combine(l: NoteFilter, r: NoteFilter) -> Self {
Self::Combine(Box::new(l), Box::new(r))
}
pub fn attempt(condition: NoteFilter, fallback: NoteFilter) -> Self {
Self::Attempt {
condition: Box::new(condition),
fallback: Box::new(fallback),
}
}
}
#[cfg(feature = "transparent-inputs")]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum CoinbaseFilter {
#[default]
AllTransparentOutputs,
CoinbaseOnly,
NonCoinbaseOnly,
}
#[cfg_attr(feature = "test-dependencies", delegatable_trait)]
pub trait InputSource {
type Error: Debug;
type AccountId: Copy + Debug + Eq + Hash;
type NoteRef: Copy + Debug + Eq + Ord;
fn get_spendable_note(
&self,
txid: &TxId,
protocol: ShieldedPool,
index: u32,
target_height: TargetHeight,
lock_filter: LockFilter<'_>,
) -> Result<Option<ReceivedNote<Self::NoteRef, Note>>, Self::Error>;
fn anchor_computable(
&self,
protocol: ShieldedPool,
height: BlockHeight,
) -> Result<bool, Self::Error>;
#[allow(clippy::too_many_arguments)]
fn select_spendable_notes(
&self,
account: Self::AccountId,
target_value: TargetValue,
sources: &[ShieldedPool],
target_height: TargetHeight,
confirmations_policy: ConfirmationsPolicy,
exclude: &[Self::NoteRef],
lock_filter: LockFilter<'_>,
) -> Result<ReceivedNotes<Self::NoteRef>, Self::Error>;
#[allow(clippy::too_many_arguments)]
fn select_spendable_notes_for_consolidation(
&self,
account: Self::AccountId,
value: Zatoshis,
source: ShieldedPool,
target_height: TargetHeight,
confirmations_policy: ConfirmationsPolicy,
exclude: &[Self::NoteRef],
lock_filter: LockFilter<'_>,
_max_additional_notes: usize,
) -> Result<ConsolidationNotes<Self::NoteRef>, Self::Error> {
self.select_spendable_notes(
account,
TargetValue::AtLeast(value),
&[source],
target_height,
confirmations_policy,
exclude,
lock_filter,
)
.map(|funding| ConsolidationNotes::from_parts(funding, ReceivedNotes::empty()))
}
#[allow(clippy::too_many_arguments)]
fn select_single_spendable_note(
&self,
account: Self::AccountId,
value: Zatoshis,
sources: &[ShieldedPool],
target_height: TargetHeight,
confirmations_policy: ConfirmationsPolicy,
exclude: &[Self::NoteRef],
lock_filter: LockFilter<'_>,
) -> Result<ReceivedNotes<Self::NoteRef>, Self::Error> {
self.select_spendable_notes(
account,
TargetValue::AtLeast(value),
sources,
target_height,
confirmations_policy,
exclude,
lock_filter,
)
.map(|notes| notes.into_single_covering(value, sources))
}
fn select_unspent_notes(
&self,
account: Self::AccountId,
sources: &[ShieldedPool],
target_height: TargetHeight,
exclude: &[Self::NoteRef],
lock_filter: LockFilter<'_>,
) -> Result<ReceivedNotes<Self::NoteRef>, Self::Error>;
fn get_account_metadata(
&self,
account: Self::AccountId,
selector: &NoteFilter,
target_height: TargetHeight,
exclude: &[Self::NoteRef],
lock_filter: LockFilter<'_>,
) -> Result<AccountMeta, Self::Error>;
#[cfg(feature = "transparent-inputs")]
fn get_unspent_transparent_output(
&self,
_outpoint: &OutPoint,
_target_height: TargetHeight,
) -> Result<Option<WalletTransparentOutput<Self::AccountId>>, Self::Error> {
unimplemented!(
"InputSource::get_spendable_transparent_output must be overridden for wallets to use the `transparent-inputs` feature"
)
}
#[cfg(feature = "transparent-inputs")]
fn get_spendable_transparent_outputs(
&self,
_address: &TransparentAddress,
_target_height: TargetHeight,
_confirmations_policy: ConfirmationsPolicy,
_output_filter: CoinbaseFilter,
_lock_filter: LockFilter<'_>,
) -> Result<Vec<WalletTransparentOutput<Self::AccountId>>, Self::Error> {
unimplemented!(
"InputSource::get_spendable_transparent_outputs must be overridden for wallets to use the `transparent-inputs` feature"
)
}
#[cfg(feature = "transparent-inputs")]
fn get_spendable_transparent_outputs_for_addresses(
&self,
addresses: &[TransparentAddress],
target_height: TargetHeight,
confirmations_policy: ConfirmationsPolicy,
output_filter: CoinbaseFilter,
lock_filter: LockFilter<'_>,
) -> Result<Vec<WalletTransparentOutput<Self::AccountId>>, Self::Error> {
let mut outputs = Vec::new();
for address in addresses {
outputs.extend(self.get_spendable_transparent_outputs(
address,
target_height,
confirmations_policy,
output_filter,
lock_filter,
)?);
}
Ok(outputs)
}
#[cfg(feature = "transparent-inputs")]
#[allow(clippy::too_many_arguments)]
fn select_spendable_transparent_outputs(
&self,
account: Self::AccountId,
target_height: TargetHeight,
confirmations_policy: ConfirmationsPolicy,
output_filter: CoinbaseFilter,
address_allow_list: Option<&[TransparentAddress]>,
target_value: TargetValue,
max_inputs: usize,
fee_rule: &StandardFeeRule,
lock_filter: LockFilter<'_>,
) -> Result<Vec<WalletTransparentOutput<Self::AccountId>>, Self::Error> {
let _ = (
account,
target_height,
confirmations_policy,
output_filter,
address_allow_list,
target_value,
max_inputs,
fee_rule,
lock_filter,
);
unimplemented!(
"InputSource::select_spendable_transparent_outputs must be overridden for \
wallets to use the value-bounded transparent input gather in propose_transaction"
)
}
}
#[cfg_attr(feature = "test-dependencies", delegatable_trait)]
pub trait WalletRead {
type Error: Debug;
type AccountId: Copy + Debug + Eq + Hash;
type Account: Account<AccountId = Self::AccountId>;
fn get_account_ids(&self) -> Result<Vec<Self::AccountId>, Self::Error>;
fn get_account(
&self,
account_id: Self::AccountId,
) -> Result<Option<Self::Account>, Self::Error>;
fn get_derived_account(
&self,
derivation: &Zip32Derivation,
) -> Result<Option<Self::Account>, Self::Error>;
fn validate_seed(
&self,
account_id: Self::AccountId,
seed: &SecretVec<u8>,
) -> Result<bool, Self::Error>;
fn seed_relevance_to_derived_accounts(
&self,
seed: &SecretVec<u8>,
) -> Result<SeedRelevance<Self::AccountId>, Self::Error>;
fn get_account_for_ufvk(
&self,
ufvk: &UnifiedFullViewingKey,
) -> Result<Option<Self::Account>, Self::Error>;
fn list_addresses(&self, account: Self::AccountId) -> Result<Vec<AddressInfo>, Self::Error>;
fn find_account_for_address<P: consensus::Parameters>(
&self,
params: &P,
address: &zcash_keys::address::Address,
) -> Result<Option<Self::AccountId>, error::FindAccountForAddressError<Self::Error>>;
fn get_last_generated_address_matching(
&self,
account: Self::AccountId,
address_filter: UnifiedAddressRequest,
) -> Result<Option<UnifiedAddress>, Self::Error>;
fn get_account_birthday(&self, account: Self::AccountId) -> Result<BlockHeight, Self::Error>;
fn get_wallet_birthday(&self) -> Result<Option<BlockHeight>, Self::Error>;
fn get_wallet_recover_until(&self) -> Result<Option<BlockHeight>, Self::Error>;
fn get_wallet_summary(
&self,
confirmations_policy: ConfirmationsPolicy,
) -> Result<Option<WalletSummary<Self::AccountId>>, Self::Error>;
fn chain_height(&self) -> Result<Option<BlockHeight>, Self::Error>;
fn anchor_retention_interval(&self) -> anchor_retention::AnchorRetentionInterval {
anchor_retention::AnchorRetentionInterval::ZIP_318
}
fn pool_migration_params(&self) -> anchor_retention::PoolMigrationParams {
anchor_retention::PoolMigrationParams::new(self.anchor_retention_interval())
}
fn get_block_hash(&self, block_height: BlockHeight) -> Result<Option<BlockHash>, Self::Error>;
fn block_metadata(&self, height: BlockHeight) -> Result<Option<BlockMetadata>, Self::Error>;
fn block_fully_scanned(&self) -> Result<Option<BlockMetadata>, Self::Error>;
fn get_max_height_hash(&self) -> Result<Option<(BlockHeight, BlockHash)>, Self::Error>;
fn block_max_scanned(&self) -> Result<Option<BlockMetadata>, Self::Error>;
fn suggest_scan_ranges(&self) -> Result<Vec<ScanRange>, Self::Error>;
fn get_target_and_anchor_heights(
&self,
min_confirmations: NonZeroU32,
) -> Result<Option<(TargetHeight, BlockHeight)>, Self::Error>;
fn get_tx_height(&self, txid: TxId) -> Result<Option<BlockHeight>, Self::Error>;
fn get_unified_full_viewing_keys(
&self,
) -> Result<HashMap<Self::AccountId, UnifiedFullViewingKey>, Self::Error>;
fn get_memo(&self, note_id: NoteId) -> Result<Option<Memo>, Self::Error>;
fn get_transaction(&self, txid: TxId) -> Result<Option<Transaction>, Self::Error>;
fn get_sapling_nullifiers(
&self,
query: NullifierQuery,
) -> Result<Vec<(Self::AccountId, sapling::Nullifier)>, Self::Error>;
#[cfg(feature = "orchard")]
fn get_orchard_nullifiers(
&self,
_query: NullifierQuery,
) -> Result<Vec<(Self::AccountId, orchard::note::Nullifier)>, Self::Error> {
unimplemented!(
"WalletRead::get_orchard_nullifiers must be overridden for wallets to use the `orchard` feature"
)
}
#[cfg(feature = "orchard")]
fn get_ironwood_nullifiers(
&self,
query: NullifierQuery,
) -> Result<Vec<(Self::AccountId, orchard::note::Nullifier)>, Self::Error>;
#[cfg(feature = "transparent-inputs")]
fn get_transparent_receivers(
&self,
_account: Self::AccountId,
_include_change: bool,
_include_standalone: bool,
) -> Result<HashMap<TransparentAddress, TransparentAddressMetadata>, Self::Error> {
unimplemented!(
"WalletRead::get_transparent_receivers must be overridden for wallets to use the `transparent-inputs` feature"
)
}
#[cfg(feature = "transparent-inputs")]
fn get_ephemeral_transparent_receivers(
&self,
_account: Self::AccountId,
_exposure_depth: u32,
_exclude_used: bool,
) -> Result<HashMap<TransparentAddress, TransparentAddressMetadata>, Self::Error> {
unimplemented!(
"WalletRead::get_ephemeral_transparent_receivers must be overridden for wallets to use the `transparent-inputs` feature"
)
}
#[cfg(feature = "transparent-inputs")]
fn get_transparent_balances(
&self,
_account: Self::AccountId,
_target_height: TargetHeight,
_confirmations_policy: ConfirmationsPolicy,
) -> Result<TransparentBalances, Self::Error> {
unimplemented!(
"WalletRead::get_transparent_balances must be overridden for wallets to use the `transparent-inputs` feature"
)
}
#[cfg(feature = "transparent-inputs")]
fn get_transparent_address_metadata(
&self,
_account: Self::AccountId,
_address: &TransparentAddress,
) -> Result<Option<TransparentAddressMetadata>, Self::Error> {
unimplemented!(
"WalletRead::get_transparent_address_metadata must be overridden for wallets to use the `transparent-inputs` feature"
)
}
#[cfg(feature = "transparent-inputs")]
fn utxo_query_height(&self, _account: Self::AccountId) -> Result<BlockHeight, Self::Error> {
unimplemented!(
"WalletRead::utxo_query_height must be overridden for wallets to use the `transparent-inputs` feature"
)
}
fn transaction_data_requests(&self) -> Result<Vec<TransactionDataRequest>, Self::Error>;
fn get_received_outputs(
&self,
txid: TxId,
target_height: TargetHeight,
confirmations_policy: ConfirmationsPolicy,
) -> Result<Vec<ReceivedTransactionOutput>, Self::Error>;
}
#[cfg(any(test, feature = "test-dependencies"))]
#[cfg_attr(feature = "test-dependencies", delegatable_trait)]
pub trait WalletTest: InputSource + WalletRead {
fn get_tx_history(
&self,
) -> Result<
Vec<testing::TransactionSummary<<Self as WalletRead>::AccountId>>,
<Self as WalletRead>::Error,
>;
fn get_sent_note_ids(
&self,
_txid: &TxId,
_protocol: ShieldedPool,
) -> Result<Vec<NoteId>, <Self as WalletRead>::Error>;
#[allow(clippy::type_complexity)]
fn get_sent_outputs(
&self,
txid: &TxId,
) -> Result<Vec<OutputOfSentTx>, <Self as WalletRead>::Error>;
#[allow(clippy::type_complexity)]
fn get_checkpoint_history(
&self,
protocol: &ShieldedPool,
) -> Result<
Vec<(BlockHeight, Option<incrementalmerkletree::Position>)>,
<Self as WalletRead>::Error,
>;
#[cfg(feature = "transparent-inputs")]
fn get_transparent_output(
&self,
_outpoint: &OutPoint,
_spendable_as_of: Option<TargetHeight>,
) -> Result<
Option<WalletTransparentOutput<<Self as WalletRead>::AccountId>>,
<Self as InputSource>::Error,
> {
unimplemented!(
"WalletTest::get_transparent_output must be overridden for wallets to use the `transparent-inputs` feature"
)
}
fn get_notes(
&self,
protocol: ShieldedPool,
) -> Result<Vec<ReceivedNote<Self::NoteRef, Note>>, <Self as InputSource>::Error>;
#[cfg(feature = "transparent-inputs")]
fn get_known_ephemeral_addresses(
&self,
_account: <Self as WalletRead>::AccountId,
_index_range: Option<Range<NonHardenedChildIndex>>,
) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, <Self as WalletRead>::Error>
{
unimplemented!(
"WalletRead::get_known_ephemeral_addresses must be overridden for wallets to use the `transparent-inputs` feature"
)
}
#[cfg(feature = "transparent-inputs")]
fn find_account_for_ephemeral_address(
&self,
address: &TransparentAddress,
) -> Result<Option<<Self as WalletRead>::AccountId>, <Self as WalletRead>::Error> {
for account_id in self.get_account_ids()? {
if self
.get_known_ephemeral_addresses(account_id, None)?
.into_iter()
.any(|(known_addr, _)| &known_addr == address)
{
return Ok(Some(account_id));
}
}
Ok(None)
}
fn finally(&self) {}
}
#[cfg(any(test, feature = "test-dependencies"))]
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct OutputOfSentTx {
value: Zatoshis,
external_recipient: Option<Address>,
#[cfg(feature = "transparent-inputs")]
ephemeral_address: Option<(Address, NonHardenedChildIndex)>,
}
#[cfg(any(test, feature = "test-dependencies"))]
impl OutputOfSentTx {
pub fn from_parts(
value: Zatoshis,
external_recipient: Option<Address>,
#[cfg(feature = "transparent-inputs")] ephemeral_address: Option<(
Address,
NonHardenedChildIndex,
)>,
) -> Self {
Self {
value,
external_recipient,
#[cfg(feature = "transparent-inputs")]
ephemeral_address,
}
}
pub fn value(&self) -> Zatoshis {
self.value
}
pub fn external_recipient(&self) -> Option<&Address> {
self.external_recipient.as_ref()
}
#[cfg(feature = "transparent-inputs")]
pub fn ephemeral_address(&self) -> Option<&(Address, NonHardenedChildIndex)> {
self.ephemeral_address.as_ref()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SeedRelevance<A: Copy> {
Relevant { account_ids: NonEmpty<A> },
NotRelevant,
NoDerivedAccounts,
NoAccounts,
}
#[derive(Debug, Clone, Copy)]
pub struct BlockMetadata {
block_height: BlockHeight,
block_hash: BlockHash,
sapling_tree_size: Option<u32>,
#[cfg(feature = "orchard")]
orchard_tree_size: Option<u32>,
#[cfg(feature = "orchard")]
ironwood_tree_size: Option<u32>,
}
impl BlockMetadata {
pub fn from_parts(
block_height: BlockHeight,
block_hash: BlockHash,
sapling_tree_size: Option<u32>,
#[cfg(feature = "orchard")] orchard_tree_size: Option<u32>,
#[cfg(feature = "orchard")] ironwood_tree_size: Option<u32>,
) -> Self {
Self {
block_height,
block_hash,
sapling_tree_size,
#[cfg(feature = "orchard")]
orchard_tree_size,
#[cfg(feature = "orchard")]
ironwood_tree_size,
}
}
pub fn block_height(&self) -> BlockHeight {
self.block_height
}
pub fn block_hash(&self) -> BlockHash {
self.block_hash
}
pub fn sapling_tree_size(&self) -> Option<u32> {
self.sapling_tree_size
}
#[cfg(feature = "orchard")]
pub fn orchard_tree_size(&self) -> Option<u32> {
self.orchard_tree_size
}
#[cfg(feature = "orchard")]
pub fn ironwood_tree_size(&self) -> Option<u32> {
self.ironwood_tree_size
}
}
pub struct ScannedBundles<NoteCommitment, NF> {
final_tree_size: u32,
commitments: Vec<(NoteCommitment, Retention<BlockHeight>)>,
nullifier_map: Vec<(TxIndex, TxId, Vec<NF>)>,
}
impl<NoteCommitment, NF> ScannedBundles<NoteCommitment, NF> {
pub(crate) fn new(
final_tree_size: u32,
commitments: Vec<(NoteCommitment, Retention<BlockHeight>)>,
nullifier_map: Vec<(TxIndex, TxId, Vec<NF>)>,
) -> Self {
Self {
final_tree_size,
nullifier_map,
commitments,
}
}
pub fn final_tree_size(&self) -> u32 {
self.final_tree_size
}
pub fn nullifier_map(&self) -> &[(TxIndex, TxId, Vec<NF>)] {
&self.nullifier_map
}
pub fn commitments(&self) -> &[(NoteCommitment, Retention<BlockHeight>)] {
&self.commitments
}
}
pub struct ScannedBlockCommitments {
pub sapling: Vec<(sapling::Node, Retention<BlockHeight>)>,
#[cfg(feature = "orchard")]
pub orchard: Vec<(orchard::tree::MerkleHashOrchard, Retention<BlockHeight>)>,
#[cfg(feature = "orchard")]
pub ironwood: Vec<(orchard::tree::MerkleHashOrchard, Retention<BlockHeight>)>,
}
pub struct ScannedBlock<AccountId> {
block_height: BlockHeight,
block_hash: BlockHash,
block_time: u32,
transactions: Vec<WalletTx<AccountId>>,
sapling: ScannedBundles<sapling::Node, sapling::Nullifier>,
#[cfg(feature = "orchard")]
orchard: ScannedBundles<orchard::tree::MerkleHashOrchard, orchard::note::Nullifier>,
#[cfg(feature = "orchard")]
ironwood: ScannedBundles<orchard::tree::MerkleHashOrchard, orchard::note::Nullifier>,
}
impl<AccountId> ScannedBlock<AccountId> {
pub(crate) fn from_parts(
block_height: BlockHeight,
block_hash: BlockHash,
block_time: u32,
transactions: Vec<WalletTx<AccountId>>,
sapling: ScannedBundles<sapling::Node, sapling::Nullifier>,
#[cfg(feature = "orchard")] orchard: ScannedBundles<
orchard::tree::MerkleHashOrchard,
orchard::note::Nullifier,
>,
#[cfg(feature = "orchard")] ironwood: ScannedBundles<
orchard::tree::MerkleHashOrchard,
orchard::note::Nullifier,
>,
) -> Self {
Self {
block_height,
block_hash,
block_time,
transactions,
sapling,
#[cfg(feature = "orchard")]
orchard,
#[cfg(feature = "orchard")]
ironwood,
}
}
pub fn height(&self) -> BlockHeight {
self.block_height
}
pub fn block_hash(&self) -> BlockHash {
self.block_hash
}
pub fn block_time(&self) -> u32 {
self.block_time
}
pub fn transactions(&self) -> &[WalletTx<AccountId>] {
&self.transactions
}
pub fn sapling(&self) -> &ScannedBundles<sapling::Node, sapling::Nullifier> {
&self.sapling
}
#[cfg(feature = "orchard")]
pub fn orchard(
&self,
) -> &ScannedBundles<orchard::tree::MerkleHashOrchard, orchard::note::Nullifier> {
&self.orchard
}
#[cfg(feature = "orchard")]
pub fn ironwood(
&self,
) -> &ScannedBundles<orchard::tree::MerkleHashOrchard, orchard::note::Nullifier> {
&self.ironwood
}
pub fn into_commitments(self) -> ScannedBlockCommitments {
ScannedBlockCommitments {
sapling: self.sapling.commitments,
#[cfg(feature = "orchard")]
orchard: self.orchard.commitments,
#[cfg(feature = "orchard")]
ironwood: self.ironwood.commitments,
}
}
pub fn to_block_metadata(&self) -> BlockMetadata {
BlockMetadata {
block_height: self.block_height,
block_hash: self.block_hash,
sapling_tree_size: Some(self.sapling.final_tree_size),
#[cfg(feature = "orchard")]
orchard_tree_size: Some(self.orchard.final_tree_size),
#[cfg(feature = "orchard")]
ironwood_tree_size: Some(self.ironwood.final_tree_size),
}
}
}
pub trait DecryptableTransaction<AccountId> {
type DecryptedSaplingOutput;
#[cfg(feature = "orchard")]
type DecryptedOrchardOutput;
}
impl<AccountId> DecryptableTransaction<AccountId> for Transaction {
type DecryptedSaplingOutput = DecryptedOutput<sapling::Note, AccountId>;
#[cfg(feature = "orchard")]
type DecryptedOrchardOutput = DecryptedOutput<(orchard::Note, orchard::ValuePool), AccountId>;
}
pub struct DecryptedTransaction<'a, Tx: DecryptableTransaction<AccountId>, AccountId> {
mined_height: Option<BlockHeight>,
tx: &'a Tx,
sapling_outputs: Vec<Tx::DecryptedSaplingOutput>,
#[cfg(feature = "orchard")]
orchard_outputs: Vec<Tx::DecryptedOrchardOutput>,
#[cfg(feature = "orchard")]
ironwood_outputs: Vec<Tx::DecryptedOrchardOutput>,
}
impl<'a, Tx: DecryptableTransaction<AccountId>, AccountId> DecryptedTransaction<'a, Tx, AccountId> {
pub fn new(
mined_height: Option<BlockHeight>,
tx: &'a Tx,
sapling_outputs: Vec<Tx::DecryptedSaplingOutput>,
#[cfg(feature = "orchard")] orchard_outputs: Vec<Tx::DecryptedOrchardOutput>,
#[cfg(feature = "orchard")] ironwood_outputs: Vec<Tx::DecryptedOrchardOutput>,
) -> Self {
Self {
mined_height,
tx,
sapling_outputs,
#[cfg(feature = "orchard")]
orchard_outputs,
#[cfg(feature = "orchard")]
ironwood_outputs,
}
}
pub fn mined_height(&self) -> Option<BlockHeight> {
self.mined_height
}
pub fn tx(&self) -> &Tx {
self.tx
}
pub fn sapling_outputs(&self) -> &[Tx::DecryptedSaplingOutput] {
&self.sapling_outputs
}
#[cfg(feature = "orchard")]
pub fn orchard_outputs(&self) -> &[Tx::DecryptedOrchardOutput] {
&self.orchard_outputs
}
#[cfg(feature = "orchard")]
pub fn ironwood_outputs(&self) -> &[Tx::DecryptedOrchardOutput] {
&self.ironwood_outputs
}
pub fn has_decrypted_outputs(&self) -> bool {
let has_sapling = !self.sapling_outputs.is_empty();
#[cfg(feature = "orchard")]
let has_orchard = !self.orchard_outputs.is_empty() || !self.ironwood_outputs.is_empty();
#[cfg(not(feature = "orchard"))]
let has_orchard = false;
has_sapling || has_orchard
}
}
pub struct SentTransaction<'a, AccountId> {
tx: &'a Transaction,
created: time::OffsetDateTime,
target_height: TargetHeight,
funding_account: AccountId,
outputs: &'a [SentTransactionOutput<AccountId>],
fee_amount: Zatoshis,
#[cfg(feature = "transparent-inputs")]
utxos_spent: &'a [OutPoint],
}
impl<'a, AccountId> SentTransaction<'a, AccountId> {
pub fn new(
tx: &'a Transaction,
created: time::OffsetDateTime,
target_height: TargetHeight,
funding_account: AccountId,
outputs: &'a [SentTransactionOutput<AccountId>],
fee_amount: Zatoshis,
#[cfg(feature = "transparent-inputs")] utxos_spent: &'a [OutPoint],
) -> Self {
Self {
tx,
created,
target_height,
funding_account,
outputs,
fee_amount,
#[cfg(feature = "transparent-inputs")]
utxos_spent,
}
}
pub fn tx(&self) -> &Transaction {
self.tx
}
pub fn created(&self) -> time::OffsetDateTime {
self.created
}
pub fn funding_account(&self) -> &AccountId {
&self.funding_account
}
pub fn outputs(&self) -> &[SentTransactionOutput<AccountId>] {
self.outputs
}
pub fn fee_amount(&self) -> Zatoshis {
self.fee_amount
}
#[cfg(feature = "transparent-inputs")]
pub fn utxos_spent(&self) -> &[OutPoint] {
self.utxos_spent
}
pub fn target_height(&self) -> TargetHeight {
self.target_height
}
}
pub struct ReceivedTransactionOutput {
pool_type: PoolType,
output_index: usize,
value: Zatoshis,
confirmations_until_spendable: u32,
}
impl ReceivedTransactionOutput {
pub fn from_parts(
pool_type: PoolType,
output_index: usize,
value: Zatoshis,
confirmations_until_spendable: u32,
) -> Self {
Self {
pool_type,
output_index,
value,
confirmations_until_spendable,
}
}
pub fn pool_type(&self) -> PoolType {
self.pool_type
}
pub fn output_index(&self) -> usize {
self.output_index
}
pub fn value(&self) -> Zatoshis {
self.value
}
pub fn confirmations_until_spendable(&self) -> u32 {
self.confirmations_until_spendable
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum NoteCommitmentTree {
Sapling,
#[cfg(feature = "orchard")]
Orchard,
#[cfg(feature = "orchard")]
Ironwood,
}
pub struct SentTransactionOutput<AccountId> {
output_index: usize,
note_commitment_tree: Option<NoteCommitmentTree>,
recipient: Recipient<AccountId>,
value: Zatoshis,
memo: Option<MemoBytes>,
}
impl<AccountId> SentTransactionOutput<AccountId> {
pub fn from_parts(
output_index: usize,
recipient: Recipient<AccountId>,
value: Zatoshis,
memo: Option<MemoBytes>,
) -> Self {
Self {
output_index,
note_commitment_tree: None,
recipient,
value,
memo,
}
}
pub(crate) fn from_parts_in_tree(
note_commitment_tree: Option<NoteCommitmentTree>,
output_index: usize,
recipient: Recipient<AccountId>,
value: Zatoshis,
memo: Option<MemoBytes>,
) -> Self {
Self {
output_index,
note_commitment_tree,
recipient,
value,
memo,
}
}
pub fn output_index(&self) -> usize {
self.output_index
}
pub fn note_commitment_tree(&self) -> Option<NoteCommitmentTree> {
self.note_commitment_tree
}
pub fn recipient(&self) -> &Recipient<AccountId> {
&self.recipient
}
pub fn value(&self) -> Zatoshis {
self.value
}
pub fn memo(&self) -> Option<&MemoBytes> {
self.memo.as_ref()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AccountBirthday {
prior_chain_state: ChainState,
recover_until: Option<BlockHeight>,
}
#[derive(Debug)]
#[non_exhaustive]
pub enum BirthdayError {
HeightInvalid(TryFromIntError),
Decode(io::Error),
}
impl fmt::Display for BirthdayError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BirthdayError::HeightInvalid(e) => {
write!(f, "Invalid block height for account birthday: {e}")
}
BirthdayError::Decode(e) => write!(
f,
"Failed to decode the note commitment tree state for the account birthday: {e}"
),
}
}
}
impl std::error::Error for BirthdayError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
BirthdayError::HeightInvalid(e) => Some(e),
BirthdayError::Decode(e) => Some(e),
}
}
}
impl From<TryFromIntError> for BirthdayError {
fn from(value: TryFromIntError) -> Self {
Self::HeightInvalid(value)
}
}
impl From<io::Error> for BirthdayError {
fn from(value: io::Error) -> Self {
Self::Decode(value)
}
}
impl AccountBirthday {
pub fn from_parts(prior_chain_state: ChainState, recover_until: Option<BlockHeight>) -> Self {
Self {
prior_chain_state,
recover_until,
}
}
pub fn from_treestate(
treestate: TreeState,
recover_until: Option<BlockHeight>,
) -> Result<Self, BirthdayError> {
Ok(Self {
prior_chain_state: treestate.to_chain_state()?,
recover_until,
})
}
pub fn sapling_frontier(
&self,
) -> &Frontier<sapling::Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }> {
self.prior_chain_state.final_sapling_tree()
}
#[cfg(feature = "orchard")]
pub fn orchard_frontier(
&self,
) -> &Frontier<orchard::tree::MerkleHashOrchard, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }>
{
self.prior_chain_state.final_orchard_tree()
}
pub fn height(&self) -> BlockHeight {
self.prior_chain_state.block_height() + 1
}
pub fn recover_until(&self) -> Option<BlockHeight> {
self.recover_until
}
pub fn prior_chain_state(&self) -> &ChainState {
&self.prior_chain_state
}
#[cfg(any(test, feature = "test-dependencies"))]
pub fn from_activation<P: zcash_protocol::consensus::Parameters>(
params: &P,
network_upgrade: NetworkUpgrade,
prior_block_hash: BlockHash,
) -> AccountBirthday {
AccountBirthday::from_parts(
ChainState::empty(
params.activation_height(network_upgrade).unwrap() - 1,
prior_block_hash,
),
None,
)
}
#[cfg(any(test, feature = "test-dependencies"))]
pub fn from_sapling_activation<P: zcash_protocol::consensus::Parameters>(
params: &P,
prior_block_hash: BlockHash,
) -> AccountBirthday {
Self::from_activation(params, NetworkUpgrade::Sapling, prior_block_hash)
}
}
#[cfg_attr(feature = "test-dependencies", delegatable_trait)]
pub trait WalletWrite:
WalletRead
+ OutputLockStore<
AccountId = <Self as WalletRead>::AccountId,
Error = <Self as WalletRead>::Error,
>
{
type UtxoRef;
fn create_account(
&mut self,
account_name: &str,
seed: &SecretVec<u8>,
birthday: &AccountBirthday,
key_source: Option<&str>,
) -> Result<(<Self as WalletRead>::AccountId, UnifiedSpendingKey), <Self as WalletRead>::Error>;
fn import_account_hd(
&mut self,
account_name: &str,
seed: &SecretVec<u8>,
account_index: zip32::AccountId,
birthday: &AccountBirthday,
key_source: Option<&str>,
) -> Result<(Self::Account, UnifiedSpendingKey), <Self as WalletRead>::Error>;
fn import_account_ufvk(
&mut self,
account_name: &str,
unified_key: &UnifiedFullViewingKey,
birthday: &AccountBirthday,
purpose: AccountPurpose,
key_source: Option<&str>,
) -> Result<Self::Account, <Self as WalletRead>::Error>;
fn delete_account(
&mut self,
account: <Self as WalletRead>::AccountId,
) -> Result<(), <Self as WalletRead>::Error>;
#[cfg(feature = "transparent-key-import")]
fn import_standalone_transparent_pubkey(
&mut self,
_account: <Self as WalletRead>::AccountId,
_pubkey: secp256k1::PublicKey,
) -> Result<(), <Self as WalletRead>::Error> {
unimplemented!(
"WalletWrite::import_standalone_transparent_pubkey must be overridden for wallets to use the `transparent-key-import` feature"
)
}
#[cfg(feature = "transparent-key-import")]
fn import_standalone_transparent_pubkeys(
&mut self,
account: <Self as WalletRead>::AccountId,
pubkeys: &[secp256k1::PublicKey],
) -> Result<(), <Self as WalletRead>::Error> {
for pubkey in pubkeys {
self.import_standalone_transparent_pubkey(account, *pubkey)?;
}
Ok(())
}
#[cfg(feature = "transparent-key-import")]
fn import_standalone_transparent_script(
&mut self,
_account: <Self as WalletRead>::AccountId,
_script: zcash_script::script::Redeem,
) -> Result<(), <Self as WalletRead>::Error> {
unimplemented!(
"WalletWrite::import_standalone_transparent_script must be overridden for wallets to use the `transparent-key-import` feature"
)
}
fn get_next_available_address(
&mut self,
account: <Self as WalletRead>::AccountId,
request: UnifiedAddressRequest,
) -> Result<Option<(UnifiedAddress, DiversifierIndex)>, <Self as WalletRead>::Error>;
fn get_address_for_index(
&mut self,
account: <Self as WalletRead>::AccountId,
diversifier_index: DiversifierIndex,
request: UnifiedAddressRequest,
) -> Result<Option<UnifiedAddress>, <Self as WalletRead>::Error>;
fn update_chain_tip(
&mut self,
tip_height: BlockHeight,
) -> Result<(), <Self as WalletRead>::Error>;
fn prune_scan_queue_below(
&mut self,
height: BlockHeight,
retain_with_priority: Option<ScanPriority>,
) -> Result<u64, <Self as WalletRead>::Error>;
fn put_blocks(
&mut self,
from_state: &ChainState,
blocks: Vec<ScannedBlock<<Self as WalletRead>::AccountId>>,
) -> Result<(), <Self as WalletRead>::Error>;
fn put_received_transparent_utxo(
&mut self,
output: &WalletTransparentOutput<<Self as WalletRead>::AccountId>,
) -> Result<Self::UtxoRef, <Self as WalletRead>::Error>;
fn store_decrypted_tx(
&mut self,
received_tx: DecryptedTransaction<Transaction, <Self as WalletRead>::AccountId>,
) -> Result<(), <Self as WalletRead>::Error>;
fn set_tx_trust(
&mut self,
txid: TxId,
trusted: bool,
) -> Result<(), <Self as WalletRead>::Error>;
fn store_transactions_to_be_sent(
&mut self,
transactions: &[SentTransaction<<Self as WalletRead>::AccountId>],
) -> Result<(), <Self as WalletRead>::Error>;
fn truncate_to_height(
&mut self,
max_height: BlockHeight,
) -> Result<BlockHeight, <Self as WalletRead>::Error>;
fn truncate_to_chain_state(
&mut self,
chain_state: ChainState,
) -> Result<(), <Self as WalletRead>::Error>;
fn rewind_to_chain_state(
&mut self,
chain_state: ChainState,
reset_account_birthdays: HashSet<<Self as WalletRead>::AccountId>,
) -> Result<(), RewindError<<Self as WalletRead>::AccountId, <Self as WalletRead>::Error>>;
#[cfg(feature = "transparent-inputs")]
fn reserve_next_n_ephemeral_addresses(
&mut self,
_account_id: <Self as WalletRead>::AccountId,
_n: usize,
) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, <Self as WalletRead>::Error>
{
unimplemented!(
"WalletWrite::reserve_next_n_ephemeral_addresses must be overridden for wallets to use the `transparent-inputs` feature"
)
}
#[cfg(feature = "transparent-inputs")]
fn reserve_next_n_internal_addresses(
&mut self,
_account_id: <Self as WalletRead>::AccountId,
_n: usize,
) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, <Self as WalletRead>::Error>
{
unimplemented!(
"WalletWrite::reserve_next_n_internal_addresses must be overridden for wallets to \
create transactions that produce transparent change"
)
}
fn set_transaction_status(
&mut self,
_txid: TxId,
_status: TransactionStatus,
) -> Result<(), <Self as WalletRead>::Error>;
#[cfg(feature = "transparent-inputs")]
fn schedule_next_check(
&mut self,
_address: &TransparentAddress,
_offset_seconds: u32,
) -> Result<Option<SystemTime>, <Self as WalletRead>::Error> {
unimplemented!(
"WalletWrite::schedule_next_check must be overridden for wallets to use the `transparent-inputs` feature"
)
}
#[cfg(feature = "transparent-inputs")]
fn mark_transparent_addresses_exposed(
&mut self,
_exposures: &[(TransparentAddress, BlockHeight)],
) -> Result<(), <Self as WalletRead>::Error> {
unimplemented!(
"WalletWrite::mark_transparent_addresses_exposed must be overridden for wallets to use the `transparent-inputs` feature"
)
}
#[cfg(feature = "transparent-inputs")]
fn notify_address_checked(
&mut self,
_request: TransactionsInvolvingAddress,
_as_of_height: BlockHeight,
) -> Result<(), <Self as WalletRead>::Error> {
unimplemented!(
"WalletWrite::notify_address_checked must be overridden for wallets to use the `transparent-inputs` feature"
)
}
#[cfg(feature = "spend-index")]
fn notify_output_verified_unspent(
&mut self,
_outpoint: OutPoint,
_as_of_height: BlockHeight,
) -> Result<(), <Self as WalletRead>::Error> {
unimplemented!(
"WalletWrite::notify_output_verified_unspent must be overridden for wallets to use the `spend-index` feature"
)
}
}
fn apply_tree_changes<H, S, const DEPTH: u8, const SHARD_HEIGHT: u8>(
tree: &mut ShardTree<S, DEPTH, SHARD_HEIGHT>,
shards: &[shardtree::LocatedPrunableTree<H>],
cap: Option<&shardtree::PrunableTree<H>>,
checkpoints_remove: &[BlockHeight],
checkpoints_add: &[(BlockHeight, shardtree::store::Checkpoint)],
) -> Result<(), ShardTreeError<S::Error>>
where
H: incrementalmerkletree::Hashable + Clone + PartialEq,
S: ShardStore<H = H, CheckpointId = BlockHeight>,
{
for shard in shards {
tree.store_mut()
.put_shard(shard.clone())
.map_err(ShardTreeError::Storage)?;
}
if let Some(cap) = cap {
tree.store_mut()
.put_cap(cap.clone())
.map_err(ShardTreeError::Storage)?;
}
for height in checkpoints_remove {
tree.store_mut()
.remove_checkpoint(height)
.map_err(ShardTreeError::Storage)?;
}
for (height, checkpoint) in checkpoints_add {
tree.store_mut()
.add_checkpoint(*height, checkpoint.clone())
.map_err(ShardTreeError::Storage)?;
}
Ok(())
}
#[cfg_attr(feature = "test-dependencies", delegatable_trait)]
pub trait WalletCommitmentTrees {
type Error: Debug;
type SaplingShardStore<'a>: ShardStore<H = sapling::Node, CheckpointId = BlockHeight, Error = Self::Error>;
fn with_sapling_tree_mut<F, A, E>(&mut self, callback: F) -> Result<A, E>
where
for<'a> F: FnMut(
&'a mut ShardTree<
Self::SaplingShardStore<'a>,
{ sapling::NOTE_COMMITMENT_TREE_DEPTH },
SAPLING_SHARD_HEIGHT,
>,
) -> Result<A, E>,
E: From<ShardTreeError<Self::Error>>;
fn put_sapling_subtree_roots(
&mut self,
start_index: u64,
roots: &[CommitmentTreeRoot<sapling::Node>],
) -> Result<(), ShardTreeError<Self::Error>>;
fn get_sapling_subtree_root(
&mut self,
index: u64,
) -> Result<Option<sapling::Node>, ShardTreeError<Self::Error>>;
#[cfg(feature = "orchard")]
type OrchardShardStore<'a>: ShardStore<
H = orchard::tree::MerkleHashOrchard,
CheckpointId = BlockHeight,
Error = Self::Error,
>;
#[cfg(feature = "orchard")]
fn with_orchard_tree_mut<F, A, E>(&mut self, callback: F) -> Result<A, E>
where
for<'a> F: FnMut(
&'a mut ShardTree<
Self::OrchardShardStore<'a>,
{ ORCHARD_SHARD_HEIGHT * 2 },
ORCHARD_SHARD_HEIGHT,
>,
) -> Result<A, E>,
E: From<ShardTreeError<Self::Error>>;
#[cfg(feature = "orchard")]
fn put_orchard_subtree_roots(
&mut self,
start_index: u64,
roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
) -> Result<(), ShardTreeError<Self::Error>>;
#[cfg(feature = "orchard")]
fn get_orchard_subtree_root(
&mut self,
index: u64,
) -> Result<Option<orchard::tree::MerkleHashOrchard>, ShardTreeError<Self::Error>>;
#[cfg(feature = "orchard")]
fn with_ironwood_tree_mut<F, A, E>(&mut self, _callback: F) -> Result<Option<A>, E>
where
for<'a> F: FnMut(
&'a mut ShardTree<
Self::OrchardShardStore<'a>,
{ ORCHARD_SHARD_HEIGHT * 2 },
ORCHARD_SHARD_HEIGHT,
>,
) -> Result<A, E>,
E: From<ShardTreeError<Self::Error>>,
{
Ok(None)
}
#[cfg(feature = "orchard")]
fn put_ironwood_subtree_roots(
&mut self,
_start_index: u64,
_roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
) -> Result<(), ShardTreeError<Self::Error>> {
Ok(())
}
#[cfg(feature = "orchard")]
fn get_ironwood_subtree_root(
&mut self,
_index: u64,
) -> Result<Option<orchard::tree::MerkleHashOrchard>, ShardTreeError<Self::Error>> {
Ok(None)
}
fn put_sapling_shards(
&mut self,
shards: &[shardtree::LocatedPrunableTree<sapling::Node>],
cap: Option<&shardtree::PrunableTree<sapling::Node>>,
checkpoints_remove: &[BlockHeight],
checkpoints_add: &[(BlockHeight, shardtree::store::Checkpoint)],
) -> Result<(), ShardTreeError<Self::Error>> {
self.with_sapling_tree_mut(|tree| {
apply_tree_changes(tree, shards, cap, checkpoints_remove, checkpoints_add)
})
}
#[cfg(feature = "orchard")]
fn put_orchard_shards(
&mut self,
shards: &[shardtree::LocatedPrunableTree<orchard::tree::MerkleHashOrchard>],
cap: Option<&shardtree::PrunableTree<orchard::tree::MerkleHashOrchard>>,
checkpoints_remove: &[BlockHeight],
checkpoints_add: &[(BlockHeight, shardtree::store::Checkpoint)],
) -> Result<(), ShardTreeError<Self::Error>> {
self.with_orchard_tree_mut(|tree| {
apply_tree_changes(tree, shards, cap, checkpoints_remove, checkpoints_add)
})
}
#[cfg(feature = "orchard")]
fn put_ironwood_shards(
&mut self,
shards: &[shardtree::LocatedPrunableTree<orchard::tree::MerkleHashOrchard>],
cap: Option<&shardtree::PrunableTree<orchard::tree::MerkleHashOrchard>>,
checkpoints_remove: &[BlockHeight],
checkpoints_add: &[(BlockHeight, shardtree::store::Checkpoint)],
) -> Result<(), ShardTreeError<Self::Error>> {
self.with_ironwood_tree_mut(|tree| {
apply_tree_changes(tree, shards, cap, checkpoints_remove, checkpoints_add)
})?;
Ok(())
}
fn remove_retained_checkpoints_below(
&mut self,
max_height: BlockHeight,
) -> Result<(), ShardTreeError<Self::Error>> {
self.with_sapling_tree_mut(|tree| {
for height in tree
.store()
.retained_checkpoints()
.map_err(ShardTreeError::Storage)?
{
if height < max_height {
tree.remove_retained_checkpoint(&height)?;
}
}
Ok::<_, ShardTreeError<Self::Error>>(())
})?;
#[cfg(feature = "orchard")]
self.with_orchard_tree_mut(|tree| {
for height in tree
.store()
.retained_checkpoints()
.map_err(ShardTreeError::Storage)?
{
if height < max_height {
tree.remove_retained_checkpoint(&height)?;
}
}
Ok::<_, ShardTreeError<Self::Error>>(())
})?;
#[cfg(feature = "orchard")]
self.with_ironwood_tree_mut(|tree| {
for height in tree
.store()
.retained_checkpoints()
.map_err(ShardTreeError::Storage)?
{
if height < max_height {
tree.remove_retained_checkpoint(&height)?;
}
}
Ok::<_, ShardTreeError<Self::Error>>(())
})?;
Ok(())
}
}
#[cfg(test)]
mod balance_tests {
use proptest::prelude::*;
use zcash_protocol::value::{BalanceError, MAX_MONEY, Zatoshis};
use super::Balance;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Bucket {
Spendable = 0,
Locked = 1,
PendingChange = 2,
PendingSpendable = 3,
Uneconomic = 4,
}
use Bucket::*;
const ALL_BUCKETS: [Bucket; 5] = [
Spendable,
Locked,
PendingChange,
PendingSpendable,
Uneconomic,
];
const TOTAL_BUCKETS: [Bucket; 4] = [Spendable, Locked, PendingChange, PendingSpendable];
fn apply(balance: &mut Balance, bucket: Bucket, value: Zatoshis) -> Result<(), BalanceError> {
match bucket {
Spendable => balance.add_spendable_value(value),
Locked => balance.add_locked_value(value),
PendingChange => balance.add_pending_change_value(value),
PendingSpendable => balance.add_pending_spendable_value(value),
Uneconomic => balance.add_uneconomic_value(value),
}
}
fn get(balance: &Balance, bucket: Bucket) -> Zatoshis {
match bucket {
Spendable => balance.spendable_value(),
Locked => balance.locked_value(),
PendingChange => balance.change_pending_confirmation(),
PendingSpendable => balance.value_pending_spendability(),
Uneconomic => balance.uneconomic_value(),
}
}
fn arb_bucket() -> impl Strategy<Value = Bucket> {
prop_oneof![
Just(Spendable),
Just(Locked),
Just(PendingChange),
Just(PendingSpendable),
Just(Uneconomic),
]
}
fn arb_add() -> impl Strategy<Value = (Bucket, u64)> {
(
arb_bucket(),
prop_oneof![
3 => 0u64..=1_000_000,
1 => 0u64..=MAX_MONEY,
],
)
}
proptest! {
#[test]
fn add_total_consistency(adds in proptest::collection::vec(arb_add(), 0..12)) {
let mut balance = Balance::ZERO;
let mut model = [0u64; 5];
for (bucket, v) in adds {
let value = Zatoshis::from_u64(v).unwrap();
let before = balance;
let result = apply(&mut balance, bucket, value);
let total: u64 = TOTAL_BUCKETS.iter().map(|b| model[*b as usize]).sum();
let expect_ok = match bucket {
Uneconomic => model[Uneconomic as usize] + v <= MAX_MONEY,
_ => total + v <= MAX_MONEY,
};
if expect_ok {
prop_assert!(result.is_ok());
model[bucket as usize] += v;
} else {
prop_assert!(result.is_err());
prop_assert_eq!(
balance, before,
"a failed add must leave the balance unchanged"
);
}
let total: u64 = TOTAL_BUCKETS.iter().map(|b| model[*b as usize]).sum();
prop_assert_eq!(balance.total(), Zatoshis::from_u64(total).unwrap());
for b in ALL_BUCKETS {
prop_assert_eq!(
get(&balance, b),
Zatoshis::from_u64(model[b as usize]).unwrap()
);
}
}
}
#[test]
fn balance_addition_is_componentwise(
a in proptest::collection::vec(arb_add(), 0..6),
b in proptest::collection::vec(arb_add(), 0..6),
) {
let build = |adds: &[(Bucket, u64)]| {
let mut balance = Balance::ZERO;
for (bucket, v) in adds {
let _ = apply(&mut balance, *bucket, Zatoshis::from_u64(*v).unwrap());
}
balance
};
let ba = build(&a);
let bb = build(&b);
let combined_total = u64::from(ba.total()) + u64::from(bb.total());
let combined_uneconomic =
u64::from(ba.uneconomic_value()) + u64::from(bb.uneconomic_value());
match ba + bb {
Ok(sum) => {
prop_assert!(combined_total <= MAX_MONEY);
prop_assert!(combined_uneconomic <= MAX_MONEY);
for bucket in ALL_BUCKETS {
prop_assert_eq!(
u64::from(get(&sum, bucket)),
u64::from(get(&ba, bucket)) + u64::from(get(&bb, bucket))
);
}
prop_assert_eq!(u64::from(sum.total()), combined_total);
}
Err(_) => {
prop_assert!(
combined_total > MAX_MONEY || combined_uneconomic > MAX_MONEY,
"balance addition failed although no component overflows"
);
}
}
}
}
}
#[cfg(test)]
mod tests {
use incrementalmerkletree::{
Address as TreeAddress, Hashable, Level, Marking, Position, Retention,
};
use shardtree::store::{Checkpoint, memory::MemoryShardStore};
use zcash_keys::{
address::{Address, UnifiedAddress},
keys::UnifiedAddressRequest,
};
use super::*;
#[cfg(feature = "orchard")]
use crate::data_api::error::FindAccountForAddressError;
use crate::data_api::testing::{
MockWalletDb, pool::ShieldedPoolTester, sapling::SaplingPoolTester,
};
use transparent::address::TransparentAddress;
use zip32::DiversifierIndex;
#[test]
fn put_sapling_shards_flushes_through_the_interface() {
let mut db = MockWalletDb::new(zcash_protocol::consensus::Network::TestNetwork);
let leaf = <sapling::Node as Hashable>::empty_leaf();
let checkpoint_height = BlockHeight::from(3);
let commitments = (0u64..4).map(|i| {
(
leaf,
if i == 3 {
Retention::Checkpoint {
id: checkpoint_height,
marking: Marking::None,
}
} else {
Retention::Ephemeral
},
)
});
let built = shardtree::LocatedTree::from_iter(
Position::from(0)..Position::from(4),
Level::from(SAPLING_SHARD_HEIGHT),
commitments,
)
.expect("commitments produce a subtree");
let checkpoints_add = built
.checkpoints
.iter()
.map(|(height, position)| (*height, Checkpoint::at_position(*position)))
.collect::<Vec<_>>();
db.put_sapling_shards(&[built.subtree], None, &[], &checkpoints_add)
.expect("bulk flush succeeds");
db.with_sapling_tree_mut(|tree| {
assert!(
tree.store()
.get_shard(TreeAddress::from_parts(
Level::from(SAPLING_SHARD_HEIGHT),
0
))
.map_err(ShardTreeError::Storage)?
.is_some()
);
assert_eq!(
tree.store()
.max_checkpoint_id()
.map_err(ShardTreeError::Storage)?,
Some(checkpoint_height)
);
Ok::<_, ShardTreeError<_>>(())
})
.expect("tree reads succeed");
let new_height = BlockHeight::from(7);
db.put_sapling_shards(
&[],
None,
&[checkpoint_height],
&[(new_height, Checkpoint::tree_empty())],
)
.expect("checkpoint replacement succeeds");
db.with_sapling_tree_mut(|tree| {
assert_eq!(
tree.store()
.max_checkpoint_id()
.map_err(ShardTreeError::Storage)?,
Some(new_height)
);
assert_eq!(
tree.store()
.checkpoint_count()
.map_err(ShardTreeError::Storage)?,
1
);
Ok::<_, ShardTreeError<_>>(())
})
.expect("tree reads succeed");
}
fn check_apply_tree_changes<H>()
where
H: incrementalmerkletree::Hashable + Clone + PartialEq + core::fmt::Debug,
{
let mut tree: ShardTree<
MemoryShardStore<H, BlockHeight>,
{ SAPLING_SHARD_HEIGHT * 2 },
SAPLING_SHARD_HEIGHT,
> = ShardTree::new(MemoryShardStore::empty(), 100);
let leaf = H::empty_leaf();
let checkpoint_height = BlockHeight::from(3);
let commitments = (0u64..4).map(|i| {
(
leaf.clone(),
if i == 3 {
Retention::Checkpoint {
id: checkpoint_height,
marking: Marking::None,
}
} else {
Retention::Ephemeral
},
)
});
let built = shardtree::LocatedTree::from_iter(
Position::from(0)..Position::from(4),
Level::from(SAPLING_SHARD_HEIGHT),
commitments,
)
.expect("commitments produce a subtree");
let checkpoints_add = built
.checkpoints
.iter()
.map(|(height, position)| (*height, Checkpoint::at_position(*position)))
.collect::<Vec<_>>();
apply_tree_changes(&mut tree, &[built.subtree], None, &[], &checkpoints_add)
.expect("bulk flush succeeds");
assert!(
tree.store()
.get_shard(TreeAddress::from_parts(
Level::from(SAPLING_SHARD_HEIGHT),
0
))
.expect("shard read succeeds")
.is_some()
);
assert_eq!(
tree.store()
.max_checkpoint_id()
.expect("checkpoint read succeeds"),
Some(checkpoint_height)
);
let new_height = BlockHeight::from(7);
apply_tree_changes(
&mut tree,
&[],
None,
&[checkpoint_height],
&[(new_height, Checkpoint::tree_empty())],
)
.expect("checkpoint replacement succeeds");
assert_eq!(
tree.store()
.max_checkpoint_id()
.expect("checkpoint read succeeds"),
Some(new_height)
);
assert_eq!(
tree.store()
.checkpoint_count()
.expect("checkpoint read succeeds"),
1
);
}
#[test]
fn apply_tree_changes_supports_every_pool_node_type() {
check_apply_tree_changes::<sapling::Node>();
#[cfg(feature = "orchard")]
check_apply_tree_changes::<orchard::tree::MerkleHashOrchard>();
}
#[cfg(feature = "orchard")]
#[test]
fn put_ironwood_shards_is_ignored_without_an_ironwood_tree() {
let mut db = MockWalletDb::new(zcash_protocol::consensus::Network::TestNetwork);
db.put_ironwood_shards(
&[],
None,
&[],
&[(BlockHeight::from(1), Checkpoint::tree_empty())],
)
.expect("ignored on backends without an Ironwood tree");
}
#[test]
fn account_meta_totals_include_ironwood() {
let meta = AccountMeta::new(
Some(PoolMeta::new(2, Zatoshis::const_from_u64(200))),
Some(PoolMeta::new(3, Zatoshis::const_from_u64(300))),
Some(PoolMeta::new(5, Zatoshis::const_from_u64(500))),
);
assert_eq!(meta.note_count(ShieldedPool::Ironwood), Some(5));
assert_eq!(meta.total_note_count(), Some(10));
assert_eq!(meta.total_value(), Some(Zatoshis::const_from_u64(1000)));
let ironwood_only = AccountMeta::new(
None,
None,
Some(PoolMeta::new(4, Zatoshis::const_from_u64(400))),
);
assert_eq!(ironwood_only.note_count(ShieldedPool::Ironwood), Some(4));
assert_eq!(ironwood_only.total_note_count(), Some(4));
assert_eq!(
ironwood_only.total_value(),
Some(Zatoshis::const_from_u64(400))
);
}
fn derived_source() -> AddressSource {
AddressSource::Derived {
diversifier_index: DiversifierIndex::default(),
#[cfg(feature = "transparent-inputs")]
transparent_key_scope: None,
}
}
fn address_info_of(address: Address) -> AddressInfo {
AddressInfo::from_parts(address, derived_source())
.expect("test address metadata must be valid")
}
fn transparent_address_for_tag(tag: u8) -> TransparentAddress {
TransparentAddress::PublicKeyHash([tag; 20])
}
fn sapling_address_for_tag(tag: u8) -> sapling::PaymentAddress {
match SaplingPoolTester::sk_default_address(&SaplingPoolTester::sk(&[tag; 32])) {
Address::Sapling(pa) => pa,
other => panic!("expected Sapling address, got {other:?}"),
}
}
fn unified_account_with(
transparent: Option<TransparentAddress>,
sapling: Option<sapling::PaymentAddress>,
#[cfg(feature = "orchard")] orchard: Option<orchard::Address>,
) -> Address {
UnifiedAddress::from_receivers(
#[cfg(feature = "orchard")]
Some(orchard).flatten(),
Some(sapling).flatten(),
transparent,
)
.expect("test UA must be valid")
.into()
}
#[test]
fn find_account_for_transparent_address_returns_matching_account() {
let wallet = MockWalletDb::from_account_addresses(
zcash_protocol::consensus::Network::MainNetwork,
[
(
1,
vec![address_info_of(Address::Transparent(
transparent_address_for_tag(1),
))],
),
(
2,
vec![address_info_of(Address::Transparent(
transparent_address_for_tag(2),
))],
),
],
);
let result = wallet.find_account_for_address(
&zcash_protocol::consensus::Network::MainNetwork,
&Address::Transparent(transparent_address_for_tag(1)),
);
assert_eq!(result.unwrap(), Some(1));
}
#[test]
fn find_account_for_transparent_receiver_in_unified_address_returns_matching_account() {
let transparent = transparent_address_for_tag(1);
let sapling_address = sapling_address_for_tag(11);
#[cfg(feature = "orchard")]
{
let wallet = MockWalletDb::from_account_addresses(
zcash_protocol::consensus::Network::MainNetwork,
[(
1,
vec![address_info_of(unified_account_with(
Some(transparent),
Some(sapling_address),
None,
))],
)],
);
let result = wallet.find_account_for_address(
&zcash_protocol::consensus::Network::MainNetwork,
&Address::Transparent(transparent),
);
assert_eq!(result.unwrap(), Some(1));
}
#[cfg(not(feature = "orchard"))]
{
let wallet = MockWalletDb::from_account_addresses(
zcash_protocol::consensus::Network::MainNetwork,
[(
1,
vec![address_info_of(unified_account_with(
Some(transparent),
Some(sapling_address),
))],
)],
);
let result = wallet.find_account_for_address(
&zcash_protocol::consensus::Network::MainNetwork,
&Address::Transparent(transparent),
);
assert_eq!(result.unwrap(), Some(1));
}
}
#[test]
fn find_account_for_address_returns_none_when_simple_address_is_unknown() {
let address = Address::Transparent(transparent_address_for_tag(1));
let wallet = MockWalletDb::from_account_addresses(
zcash_protocol::consensus::Network::MainNetwork,
[(1, vec![address_info_of(address)])],
);
let other_address = Address::Transparent(transparent_address_for_tag(9));
let result = wallet.find_account_for_address(
&zcash_protocol::consensus::Network::MainNetwork,
&other_address,
);
assert_eq!(result.unwrap(), None);
}
fn test_ufvk(seed_tag: u8) -> zcash_keys::keys::UnifiedFullViewingKey {
zcash_keys::keys::UnifiedSpendingKey::from_seed(
&zcash_protocol::consensus::Network::MainNetwork,
&[seed_tag; 32],
zip32::AccountId::ZERO,
)
.expect("valid seed")
.to_unified_full_viewing_key()
}
#[test]
fn find_account_for_unified_address_returns_account_when_receivers_map_to_same_account() {
let ufvk = test_ufvk(1);
let wallet = MockWalletDb::from_account_ufvks(
zcash_protocol::consensus::Network::MainNetwork,
[(1, ufvk.clone())],
);
let (ua, _) = ufvk
.default_address(UnifiedAddressRequest::AllAvailableKeys)
.expect("default address must be derivable");
let result = wallet.find_account_for_address(
&zcash_protocol::consensus::Network::MainNetwork,
&Address::Unified(ua),
);
assert_eq!(result.unwrap(), Some(1));
}
#[test]
fn find_account_for_unified_address_returns_none_when_no_receiver_matches() {
let wallet = MockWalletDb::from_account_ufvks(
zcash_protocol::consensus::Network::MainNetwork,
[(1, test_ufvk(1))],
);
let (ua_from_other_seed, _) = test_ufvk(99)
.default_address(UnifiedAddressRequest::AllAvailableKeys)
.expect("default address must be derivable");
let result = wallet.find_account_for_address(
&zcash_protocol::consensus::Network::MainNetwork,
&Address::Unified(ua_from_other_seed),
);
assert_eq!(result.unwrap(), None);
}
#[test]
fn find_account_for_sapling_address_resolves_via_uivk_algebra_when_not_previously_exposed() {
let ufvk = test_ufvk(1);
let wallet = MockWalletDb::from_account_ufvks(
zcash_protocol::consensus::Network::MainNetwork,
[(1, ufvk.clone())],
);
let (ua, _) = ufvk
.default_address(UnifiedAddressRequest::AllAvailableKeys)
.expect("default address must be derivable");
let sapling_pa = *ua.sapling().expect("sapling receiver");
let result = wallet.find_account_for_address(
&zcash_protocol::consensus::Network::MainNetwork,
&Address::Sapling(sapling_pa),
);
assert_eq!(result.unwrap(), Some(1));
}
#[test]
fn find_account_for_address_returns_none_for_empty_wallet() {
let wallet = MockWalletDb::from_account_addresses(
zcash_protocol::consensus::Network::MainNetwork,
std::iter::empty(),
);
let result = wallet.find_account_for_address(
&zcash_protocol::consensus::Network::MainNetwork,
&Address::Transparent(transparent_address_for_tag(1)),
);
assert_eq!(result.unwrap(), None);
let result = wallet.find_account_for_address(
&zcash_protocol::consensus::Network::MainNetwork,
&Address::Sapling(sapling_address_for_tag(1)),
);
assert_eq!(result.unwrap(), None);
}
#[cfg(feature = "orchard")]
#[test]
fn find_account_for_unified_address_errors_when_receivers_map_to_different_accounts() {
let ufvk1 = test_ufvk(1);
let ufvk2 = test_ufvk(2);
let wallet = MockWalletDb::from_account_ufvks(
zcash_protocol::consensus::Network::MainNetwork,
[(1, ufvk1.clone()), (2, ufvk2.clone())],
);
let (ua1, _) = ufvk1
.default_address(UnifiedAddressRequest::AllAvailableKeys)
.expect("default address must be derivable");
let (ua2, _) = ufvk2
.default_address(UnifiedAddressRequest::AllAvailableKeys)
.expect("default address must be derivable");
let frankenstein = UnifiedAddress::from_receivers(
Some(ua2.orchard().copied().expect("orchard receiver")),
Some(ua1.sapling().copied().expect("sapling receiver")),
None,
)
.expect("sapling+orchard UA must be valid");
let result = wallet.find_account_for_address(
&zcash_protocol::consensus::Network::MainNetwork,
&Address::Unified(frankenstein),
);
assert!(matches!(
result,
Err(FindAccountForAddressError::UnifiedAddressConflict)
));
}
#[test]
fn account_balance_unshielded_split_mutators() {
let mut balance = AccountBalance::ZERO;
let regular_value = Zatoshis::const_from_u64(100_000);
let coinbase_value = Zatoshis::const_from_u64(50_000);
balance
.with_unshielded_regular_balance_mut(|bal| bal.add_spendable_value(regular_value))
.unwrap();
balance
.with_unshielded_coinbase_balance_mut(|bal| {
bal.add_pending_spendable_value(coinbase_value)
})
.unwrap();
assert_eq!(
balance.unshielded_regular_balance().spendable_value(),
regular_value
);
assert_eq!(balance.unshielded_regular_balance().total(), regular_value);
assert_eq!(
balance
.unshielded_regular_balance()
.value_pending_spendability(),
Zatoshis::ZERO
);
assert_eq!(
balance.unshielded_coinbase_balance().spendable_value(),
Zatoshis::ZERO
);
assert_eq!(
balance
.unshielded_coinbase_balance()
.value_pending_spendability(),
coinbase_value
);
assert_eq!(
balance.unshielded_coinbase_balance().total(),
coinbase_value
);
assert_eq!(balance.spendable_value(), Zatoshis::ZERO);
assert_eq!(balance.change_pending_confirmation(), Zatoshis::ZERO);
assert_eq!(balance.value_pending_spendability(), Zatoshis::ZERO);
assert_eq!(balance.sapling_balance(), &Balance::ZERO);
assert_eq!(balance.orchard_balance(), &Balance::ZERO);
assert_eq!(balance.ironwood_balance(), &Balance::ZERO);
}
#[test]
fn account_balance_unshielded_balance_is_sum() {
let mut balance = AccountBalance::ZERO;
let regular_spendable = Zatoshis::const_from_u64(100_000);
let regular_dust = Zatoshis::const_from_u64(100);
let coinbase_pending = Zatoshis::const_from_u64(625_000_000);
let coinbase_dust = Zatoshis::const_from_u64(42);
balance
.with_unshielded_regular_balance_mut(|bal| {
bal.add_spendable_value(regular_spendable)?;
bal.add_uneconomic_value(regular_dust)
})
.unwrap();
balance
.with_unshielded_coinbase_balance_mut(|bal| {
bal.add_pending_spendable_value(coinbase_pending)?;
bal.add_uneconomic_value(coinbase_dust)
})
.unwrap();
let combined = balance.unshielded_balance();
assert_eq!(
combined,
(*balance.unshielded_regular_balance() + *balance.unshielded_coinbase_balance())
.unwrap()
);
assert_eq!(combined.spendable_value(), regular_spendable);
assert_eq!(combined.value_pending_spendability(), coinbase_pending);
assert_eq!(
combined.uneconomic_value(),
(regular_dust + coinbase_dust).unwrap()
);
#[allow(deprecated)]
let unshielded = balance.unshielded();
assert_eq!(
unshielded,
(balance.unshielded_regular_balance().total()
+ balance.unshielded_coinbase_balance().total())
.unwrap()
);
assert_eq!(
balance.total(),
(regular_spendable + coinbase_pending).unwrap()
);
assert_eq!(
balance.uneconomic_value(),
(regular_dust + coinbase_dust).unwrap()
);
}
#[test]
fn account_balance_unshielded_overflow_rejected() {
let max_money = Zatoshis::const_from_u64(zcash_protocol::value::MAX_MONEY);
let mut balance = AccountBalance::ZERO;
balance
.with_unshielded_regular_balance_mut(|bal| bal.add_spendable_value(max_money))
.unwrap();
assert_eq!(balance.total(), max_money);
let result: Result<(), BalanceError> =
balance.with_unshielded_coinbase_balance_mut(|bal| {
bal.add_pending_spendable_value(Zatoshis::const_from_u64(1))
});
assert!(matches!(result, Err(BalanceError::Overflow)));
}
}