Skip to main content

zcash_client_backend/
data_api.rs

1//! # Utilities for Zcash wallet construction
2//!
3//! This module defines a set of APIs for wallet data persistence, and provides a suite of methods
4//! based upon these APIs that can be used to implement a fully functional Zcash wallet. At
5//! present, the interfaces provided here are built primarily around the use of a source of
6//! [`CompactBlock`] data such as the Zcash Light Client Protocol as defined in
7//! [ZIP 307](https://zips.z.cash/zip-0307) but they may be generalized to full-block use cases in
8//! the future.
9//!
10//! ## Important Concepts
11//!
12//! There are several important operations that a Zcash wallet must perform that distinguish Zcash
13//! wallet design from wallets for other cryptocurrencies.
14//!
15//! * Viewing Keys: Wallets based upon this module are built around the capabilities of Zcash
16//!   [`UnifiedFullViewingKey`]s; the wallet backend provides no facilities for the storage
17//!   of spending keys, and spending keys must be provided by the caller (or delegated to external
18//!   devices via the provided [`pczt`] functionality) in order to perform transaction creation
19//!   operations.
20//! * Blockchain Scanning: A Zcash wallet must download and trial-decrypt each transaction on the
21//!   Zcash blockchain using one or more Viewing Keys in order to find new shielded transaction
22//!   outputs (generally termed "notes") belonging to the wallet. The primary entrypoint for this
23//!   functionality is the [`scan_cached_blocks`] method. See the [`chain`] module for additional
24//!   details.
25//! * Witness Updates: In order to spend a shielded note, the wallet must be able to compute the
26//!   Merkle path to that note in the global note commitment tree. When [`scan_cached_blocks`] is
27//!   used to process a range of blocks, the note commitment tree is updated with the note
28//!   commitments for the blocks in that range.
29//! * Transaction Construction: The [`wallet`] module provides functions for creating Zcash
30//!   transactions that spend funds belonging to the wallet. Input selection for transaction
31//!   construction is performed automatically. When spending funds received at transparent
32//!   addresses, the caller is required to explicitly specify the set of addresses from which to
33//!   spend funds, in order to prevent inadvertent commingling of funds received at different
34//!   addresses; allowing such commingling would enable chain observers to identify those
35//!   addresses as belonging to the same user.
36//!
37//! ## Core Traits
38//!
39//! The utility functions described above depend upon four important traits defined in this
40//! module, which between them encompass the data storage requirements of a light wallet.
41//! The relevant traits are [`InputSource`], [`WalletRead`], [`WalletWrite`], and
42//! [`WalletCommitmentTrees`]. A complete implementation of the data storage layer for a wallet
43//! will include an implementation of all four of these traits. See the [`zcash_client_sqlite`]
44//! crate for a complete example of the implementation of these traits.
45//!
46//! ## Accounts
47//!
48//! The operation of the [`InputSource`], [`WalletRead`] and [`WalletWrite`] traits is built around
49//! the concept of a wallet having one or more accounts, with a unique `AccountId` for each
50//! account.
51//!
52//! An account identifier corresponds to at most a single [`UnifiedSpendingKey`]'s worth of spend
53//! authority, with the received and spent notes of that account tracked via the corresponding
54//! [`UnifiedFullViewingKey`]. Both received notes and change spendable by that spending authority
55//! (both the external and internal parts of that key, as defined by
56//! [ZIP 316](https://zips.z.cash/zip-0316)) will be interpreted as belonging to that account.
57//!
58//! [`CompactBlock`]: crate::proto::compact_formats::CompactBlock
59//! [`scan_cached_blocks`]: crate::data_api::chain::scan_cached_blocks
60//! [`zcash_client_sqlite`]: https://docs.rs/zcash_client_sqlite
61//! [`TransactionRequest`]: crate::zip321::TransactionRequest
62//! [`propose_shielding`]: crate::data_api::wallet::propose_shielding
63//! [`pczt`]: https://docs.rs/pczt
64
65use nonempty::NonEmpty;
66use secrecy::SecretVec;
67use std::{
68    collections::{HashMap, HashSet},
69    fmt::{self, Debug},
70    hash::Hash,
71    io,
72    num::{NonZeroU32, TryFromIntError},
73};
74
75use incrementalmerkletree::{Retention, frontier::Frontier};
76use shardtree::{ShardTree, error::ShardTreeError, store::ShardStore};
77
78use zcash_keys::{
79    address::{Address, UnifiedAddress},
80    keys::{
81        UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedIncomingViewingKey, UnifiedSpendingKey,
82    },
83};
84use zcash_primitives::{block::BlockHash, transaction::Transaction};
85use zcash_protocol::{
86    PoolType, ShieldedPool, TxId,
87    consensus::{self, BlockHeight, TxIndex},
88    memo::{Memo, MemoBytes},
89    value::{BalanceError, Zatoshis},
90};
91use zip32::{DiversifierIndex, fingerprint::SeedFingerprint};
92
93use self::{
94    chain::{ChainState, CommitmentTreeRoot},
95    scanning::{ScanPriority, ScanRange},
96};
97use crate::{
98    data_api::{
99        error::RewindError,
100        wallet::{ConfirmationsPolicy, TargetHeight, input_selection::LockFilter},
101    },
102    decrypt::DecryptedOutput,
103    proto::service::TreeState,
104    wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput, WalletTx},
105};
106
107#[cfg(feature = "transparent-inputs")]
108use {
109    crate::{fees::StandardFeeRule, wallet::TransparentAddressMetadata},
110    getset::{CopyGetters, Getters},
111    std::time::SystemTime,
112    transparent::{address::TransparentAddress, bundle::OutPoint, keys::TransparentKeyScope},
113};
114
115#[cfg(all(
116    feature = "transparent-inputs",
117    any(test, feature = "test-dependencies")
118))]
119use {std::ops::Range, transparent::keys::NonHardenedChildIndex};
120
121#[cfg(feature = "zcashd-compat")]
122use zcash_keys::keys::zcashd;
123
124#[cfg(feature = "test-dependencies")]
125use ambassador::delegatable_trait;
126
127#[cfg(any(test, feature = "test-dependencies"))]
128use zcash_protocol::consensus::NetworkUpgrade;
129
130pub mod anchor_retention;
131pub mod chain;
132pub mod defaults;
133pub mod error;
134pub mod ll;
135pub mod locking;
136pub use locking::OutputLockStore;
137#[cfg(feature = "test-dependencies")]
138pub use locking::ambassador_impl_OutputLockStore;
139pub mod scanning;
140pub mod wallet;
141#[cfg(feature = "orchard")]
142pub mod zip318;
143
144#[cfg(any(test, feature = "test-dependencies"))]
145pub mod testing;
146
147/// The origin of a transparent address within a wallet.
148#[cfg(feature = "transparent-inputs")]
149#[derive(Debug, Clone, PartialEq, Eq)]
150pub enum TransparentKeyOrigin {
151    /// The address was imported standalone (no HD derivation scope).
152    Imported,
153    /// The address was derived from the account's HD key tree.
154    Derived { scope: TransparentKeyScope },
155}
156
157/// A mapping from transparent addresses to their key origin and balance.
158#[cfg(feature = "transparent-inputs")]
159pub type TransparentBalances = HashMap<TransparentAddress, (TransparentKeyOrigin, Balance)>;
160
161/// The height of subtree roots in the Sapling note commitment tree.
162///
163/// This conforms to the structure of subtree data returned by
164/// `lightwalletd` when using the `GetSubtreeRoots` GRPC call.
165pub const SAPLING_SHARD_HEIGHT: u8 = sapling::NOTE_COMMITMENT_TREE_DEPTH / 2;
166
167/// The height of subtree roots in the Orchard note commitment tree.
168///
169/// This conforms to the structure of subtree data returned by
170/// `lightwalletd` when using the `GetSubtreeRoots` GRPC call.
171#[cfg(feature = "orchard")]
172pub const ORCHARD_SHARD_HEIGHT: u8 = { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 } / 2;
173
174/// The height of subtree roots in the Ironwood note commitment tree.
175///
176/// This conforms to the structure of subtree data returned by
177/// `lightwalletd` when using the `GetSubtreeRoots` GRPC call.
178#[cfg(feature = "orchard")]
179pub const IRONWOOD_SHARD_HEIGHT: u8 = { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 } / 2;
180
181/// An enumeration of constraints that can be applied when querying for nullifiers for notes
182/// belonging to the wallet.
183pub enum NullifierQuery {
184    Unspent,
185    All,
186}
187
188/// An intent of representing spendable value to reach a certain targeted
189/// amount.
190///
191/// `AtLeast(Zatoshis)` refers to the amount of `Zatoshis` that can cover
192/// at minimum the given zatoshis that is conformed by the sum of spendable notes.
193///
194///
195/// Discussion: why not just use ``Zatoshis``?
196///
197/// the `Zatoshis` value isn't enough to explain intent when seeking to match a
198/// given a given amount. Is the value expressed in `Zatoshis` the ceiling value
199/// or the minimum value of a given spend intent? How would you express that the
200/// value spend intent is "as much as possible" without knowing the value upfront?
201#[derive(Debug, Clone, Copy)]
202pub enum TargetValue {
203    AtLeast(Zatoshis),
204    AllFunds(MaxSpendMode),
205}
206
207/// Specifies how an TargetValue::AllFunds should be evaluated
208#[derive(Debug, Clone, Copy)]
209pub enum MaxSpendMode {
210    /// `MaxSpendable` will target to spend all _currently_ spendable funds where it
211    /// could be the case that the wallet has received other funds that are not
212    /// confirmed and therefore not spendable yet and the caller evaluates that as
213    /// an acceptable scenario.
214    MaxSpendable,
215    /// `Everything` will target to spend **all funds** and will fail if there are
216    /// unspendable funds in the wallet or if the wallet is not yet synced.
217    Everything,
218}
219/// Balance information for a value within a single pool in an account.
220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub struct Balance {
222    spendable_value: Zatoshis,
223    locked_value: Zatoshis,
224    change_pending_confirmation: Zatoshis,
225    value_pending_spendability: Zatoshis,
226    uneconomic_value: Zatoshis,
227}
228
229impl Balance {
230    /// The [`Balance`] value having zero values for all its fields.
231    pub const ZERO: Self = Self {
232        spendable_value: Zatoshis::ZERO,
233        locked_value: Zatoshis::ZERO,
234        change_pending_confirmation: Zatoshis::ZERO,
235        value_pending_spendability: Zatoshis::ZERO,
236        uneconomic_value: Zatoshis::ZERO,
237    };
238
239    fn check_total_adding(&self, value: Zatoshis) -> Result<Zatoshis, BalanceError> {
240        (self.spendable_value
241            + self.locked_value
242            + self.change_pending_confirmation
243            + self.value_pending_spendability
244            + value)
245            .ok_or(BalanceError::Overflow)
246    }
247
248    /// Returns the value in the account that may currently be spent; it is possible to compute
249    /// witnesses for all the notes that comprise this value, and all of this value is confirmed to
250    /// the required confirmation depth.
251    pub fn spendable_value(&self) -> Zatoshis {
252        self.spendable_value
253    }
254
255    /// Returns the value in the account that is currently "locked".
256    ///
257    /// The outputs that comprise this balance are seen by the wallet as being committed to be
258    /// spent by a transaction proposal or PCZT.
259    pub fn locked_value(&self) -> Zatoshis {
260        self.locked_value
261    }
262
263    /// Adds the specified value to the spendable total, checking for overflow.
264    pub fn add_spendable_value(&mut self, value: Zatoshis) -> Result<(), BalanceError> {
265        self.check_total_adding(value)?;
266        self.spendable_value = (self.spendable_value + value).unwrap();
267        Ok(())
268    }
269
270    /// Adds the specified value to the locked total, checking for overflow.
271    pub fn add_locked_value(&mut self, value: Zatoshis) -> Result<(), BalanceError> {
272        self.check_total_adding(value)?;
273        self.locked_value = (self.locked_value + value).unwrap();
274        Ok(())
275    }
276
277    /// Returns the value in the account of shielded change notes that do not yet have sufficient
278    /// confirmations to be spendable.
279    pub fn change_pending_confirmation(&self) -> Zatoshis {
280        self.change_pending_confirmation
281    }
282
283    /// Adds the specified value to the pending change total, checking for overflow.
284    pub fn add_pending_change_value(&mut self, value: Zatoshis) -> Result<(), BalanceError> {
285        self.check_total_adding(value)?;
286        self.change_pending_confirmation = (self.change_pending_confirmation + value).unwrap();
287        Ok(())
288    }
289
290    /// Returns the value in the account of all remaining received notes that either do not have
291    /// sufficient confirmations to be spendable, or for which witnesses cannot yet be constructed
292    /// without additional scanning.
293    pub fn value_pending_spendability(&self) -> Zatoshis {
294        self.value_pending_spendability
295    }
296
297    /// Adds the specified value to the pending spendable total, checking for overflow.
298    pub fn add_pending_spendable_value(&mut self, value: Zatoshis) -> Result<(), BalanceError> {
299        self.check_total_adding(value)?;
300        self.value_pending_spendability = (self.value_pending_spendability + value).unwrap();
301        Ok(())
302    }
303
304    /// Returns the value in the account of notes that have value less than or equal to the marginal
305    /// fee, and consequently cannot be spent except as a grace input.
306    pub fn uneconomic_value(&self) -> Zatoshis {
307        self.uneconomic_value
308    }
309
310    /// Adds the specified value to the uneconomic value total, checking for overflow.
311    pub fn add_uneconomic_value(&mut self, value: Zatoshis) -> Result<(), BalanceError> {
312        self.uneconomic_value = (self.uneconomic_value + value).ok_or(BalanceError::Overflow)?;
313        Ok(())
314    }
315
316    /// Returns the total value of funds represented by this [`Balance`].
317    pub fn total(&self) -> Zatoshis {
318        (self.spendable_value
319            + self.locked_value
320            + self.change_pending_confirmation
321            + self.value_pending_spendability)
322            .expect("Balance cannot overflow MAX_MONEY")
323    }
324}
325
326impl core::ops::Add<Balance> for Balance {
327    type Output = Result<Balance, BalanceError>;
328
329    fn add(self, rhs: Balance) -> Self::Output {
330        let result = Balance {
331            spendable_value: (self.spendable_value + rhs.spendable_value)
332                .ok_or(BalanceError::Overflow)?,
333            locked_value: (self.locked_value + rhs.locked_value).ok_or(BalanceError::Overflow)?,
334            change_pending_confirmation: (self.change_pending_confirmation
335                + rhs.change_pending_confirmation)
336                .ok_or(BalanceError::Overflow)?,
337            value_pending_spendability: (self.value_pending_spendability
338                + rhs.value_pending_spendability)
339                .ok_or(BalanceError::Overflow)?,
340            uneconomic_value: (self.uneconomic_value + rhs.uneconomic_value)
341                .ok_or(BalanceError::Overflow)?,
342        };
343
344        result.check_total_adding(Zatoshis::ZERO)?;
345
346        Ok(result)
347    }
348}
349
350/// Balance information for a single account. The sum of this struct's fields is the total balance
351/// of the wallet.
352#[derive(Debug, Clone, Copy, PartialEq, Eq)]
353pub struct AccountBalance {
354    sapling_balance: Balance,
355    orchard_balance: Balance,
356    ironwood_balance: Balance,
357    unshielded_regular_balance: Balance,
358    unshielded_coinbase_balance: Balance,
359}
360
361impl AccountBalance {
362    /// The [`Balance`] value having zero values for all its fields.
363    pub const ZERO: Self = Self {
364        sapling_balance: Balance::ZERO,
365        orchard_balance: Balance::ZERO,
366        ironwood_balance: Balance::ZERO,
367        unshielded_regular_balance: Balance::ZERO,
368        unshielded_coinbase_balance: Balance::ZERO,
369    };
370
371    fn check_total(&self) -> Result<Zatoshis, BalanceError> {
372        (self.sapling_balance.total()
373            + self.orchard_balance.total()
374            + self.ironwood_balance.total()
375            + self.unshielded_regular_balance.total()
376            + self.unshielded_coinbase_balance.total())
377        .ok_or(BalanceError::Overflow)
378    }
379
380    /// Returns the [`Balance`] of Sapling funds in the account.
381    pub fn sapling_balance(&self) -> &Balance {
382        &self.sapling_balance
383    }
384
385    /// Provides a mutable reference to the [`Balance`] of Sapling funds in the account
386    /// to the specified callback, checking invariants after the callback's action has been
387    /// evaluated.
388    pub fn with_sapling_balance_mut<A, E: From<BalanceError>>(
389        &mut self,
390        f: impl FnOnce(&mut Balance) -> Result<A, E>,
391    ) -> Result<A, E> {
392        let result = f(&mut self.sapling_balance)?;
393        self.check_total()?;
394        Ok(result)
395    }
396
397    /// Returns the [`Balance`] of Orchard funds in the account.
398    pub fn orchard_balance(&self) -> &Balance {
399        &self.orchard_balance
400    }
401
402    /// Provides a mutable reference to the [`Balance`] of Orchard funds in the account
403    /// to the specified callback, checking invariants after the callback's action has been
404    /// evaluated.
405    pub fn with_orchard_balance_mut<A, E: From<BalanceError>>(
406        &mut self,
407        f: impl FnOnce(&mut Balance) -> Result<A, E>,
408    ) -> Result<A, E> {
409        let result = f(&mut self.orchard_balance)?;
410        self.check_total()?;
411        Ok(result)
412    }
413
414    /// Returns the [`Balance`] of Ironwood funds in the account.
415    pub fn ironwood_balance(&self) -> &Balance {
416        &self.ironwood_balance
417    }
418
419    /// Provides a mutable reference to the [`Balance`] of Ironwood funds in the account
420    /// to the specified callback, checking invariants after the callback's action has been
421    /// evaluated.
422    pub fn with_ironwood_balance_mut<A, E: From<BalanceError>>(
423        &mut self,
424        f: impl FnOnce(&mut Balance) -> Result<A, E>,
425    ) -> Result<A, E> {
426        let result = f(&mut self.ironwood_balance)?;
427        self.check_total()?;
428        Ok(result)
429    }
430
431    /// Returns the total value of unspent transparent transaction outputs belonging to the wallet.
432    #[deprecated(
433        note = "this function is deprecated. Please use [`AccountBalance::unshielded_regular_balance`] and [`AccountBalance::unshielded_coinbase_balance`] instead."
434    )]
435    pub fn unshielded(&self) -> Zatoshis {
436        (self.unshielded_regular_balance.total() + self.unshielded_coinbase_balance.total())
437            .expect("Account balance cannot overflow MAX_MONEY")
438    }
439
440    /// Returns the combined [`Balance`] of unshielded funds in the account, computed as the sum
441    /// of the [`unshielded_regular_balance`] and the [`unshielded_coinbase_balance`].
442    ///
443    /// The [`spendable_value`] field of the returned [`Balance`] contains funds that may be spent
444    /// in a shielding transaction: transparent funds that satisfy the wallet's confirmation
445    /// policy, including coinbase funds that have reached maturity. The
446    /// [`value_pending_spendability`] field contains transparent funds that are not yet
447    /// spendable: funds that do not yet have the number of confirmations required by the
448    /// wallet's confirmation policy, and coinbase funds that have not yet reached maturity. The
449    /// [`change_pending_confirmation`] field is currently always zero, because this crate does
450    /// not yet distinguish transparent change from other transparent value awaiting
451    /// confirmation.
452    ///
453    /// [`unshielded_regular_balance`]: AccountBalance::unshielded_regular_balance
454    /// [`unshielded_coinbase_balance`]: AccountBalance::unshielded_coinbase_balance
455    /// [`spendable_value`]: Balance::spendable_value
456    /// [`change_pending_confirmation`]: Balance::change_pending_confirmation
457    /// [`value_pending_spendability`]: Balance::value_pending_spendability
458    pub fn unshielded_balance(&self) -> Balance {
459        (self.unshielded_regular_balance + self.unshielded_coinbase_balance)
460            .expect("Account balance cannot overflow MAX_MONEY")
461    }
462
463    /// Returns the [`Balance`] of regular (non-coinbase) transparent funds in the account.
464    ///
465    /// Transparent outputs whose containing transaction's index within its block is unknown are
466    /// classified as regular (non-coinbase) funds, consistent with the treatment described for
467    /// `CoinbaseFilter`.
468    pub fn unshielded_regular_balance(&self) -> &Balance {
469        &self.unshielded_regular_balance
470    }
471
472    /// Provides a mutable reference to the [`Balance`] of regular (non-coinbase) transparent
473    /// funds in the account to the specified callback, checking invariants after the callback's
474    /// action has been evaluated.
475    pub fn with_unshielded_regular_balance_mut<A, E: From<BalanceError>>(
476        &mut self,
477        f: impl FnOnce(&mut Balance) -> Result<A, E>,
478    ) -> Result<A, E> {
479        let result = f(&mut self.unshielded_regular_balance)?;
480        self.check_total()?;
481        Ok(result)
482    }
483
484    /// Returns the [`Balance`] of funds in coinbase transparent outputs belonging to the
485    /// account.
486    ///
487    /// Coinbase outputs may only be spent by shielding them, and only once they have reached
488    /// coinbase maturity; immature coinbase funds are reported in the
489    /// [`value_pending_spendability`] field of the returned [`Balance`]. Outputs whose
490    /// containing transaction's index within its block is unknown are conservatively classified
491    /// as regular (non-coinbase) funds and do not contribute to this balance; see
492    /// `CoinbaseFilter`.
493    ///
494    /// [`value_pending_spendability`]: Balance::value_pending_spendability
495    pub fn unshielded_coinbase_balance(&self) -> &Balance {
496        &self.unshielded_coinbase_balance
497    }
498
499    /// Provides a mutable reference to the [`Balance`] of transparent coinbase funds in the
500    /// account to the specified callback, checking invariants after the callback's action has
501    /// been evaluated.
502    pub fn with_unshielded_coinbase_balance_mut<A, E: From<BalanceError>>(
503        &mut self,
504        f: impl FnOnce(&mut Balance) -> Result<A, E>,
505    ) -> Result<A, E> {
506        let result = f(&mut self.unshielded_coinbase_balance)?;
507        self.check_total()?;
508        Ok(result)
509    }
510
511    /// Returns the total value of economically relevant notes and UTXOs belonging to the account.
512    pub fn total(&self) -> Zatoshis {
513        (self.sapling_balance.total()
514            + self.orchard_balance.total()
515            + self.ironwood_balance.total()
516            + self.unshielded_regular_balance.total()
517            + self.unshielded_coinbase_balance.total())
518        .expect("Account balance cannot overflow MAX_MONEY")
519    }
520
521    /// Returns the total value of shielded (Sapling, Orchard, and Ironwood) funds that may
522    /// immediately be spent.
523    pub fn spendable_value(&self) -> Zatoshis {
524        (self.sapling_balance.spendable_value
525            + self.orchard_balance.spendable_value
526            + self.ironwood_balance.spendable_value)
527            .expect("Account balance cannot overflow MAX_MONEY")
528    }
529
530    /// Returns the total value of notes and UTXOs that are locked, having been committed to
531    /// an in-flight transaction proposal or PCZT.
532    pub fn locked_value(&self) -> Zatoshis {
533        (self.sapling_balance.locked_value()
534            + self.orchard_balance.locked_value()
535            + self.ironwood_balance.locked_value()
536            + self.unshielded_regular_balance.locked_value()
537            + self.unshielded_coinbase_balance.locked_value())
538        .expect("Account balance cannot overflow MAX_MONEY")
539    }
540
541    /// Returns the total value of change and/or shielding transaction outputs that are awaiting
542    /// sufficient confirmations for spendability.
543    pub fn change_pending_confirmation(&self) -> Zatoshis {
544        (self.sapling_balance.change_pending_confirmation
545            + self.orchard_balance.change_pending_confirmation
546            + self.ironwood_balance.change_pending_confirmation)
547            .expect("Account balance cannot overflow MAX_MONEY")
548    }
549
550    /// Returns the value of shielded funds that are not yet spendable because additional scanning
551    /// is required before it will be possible to derive witnesses for the associated notes.
552    pub fn value_pending_spendability(&self) -> Zatoshis {
553        (self.sapling_balance.value_pending_spendability
554            + self.orchard_balance.value_pending_spendability
555            + self.ironwood_balance.value_pending_spendability)
556            .expect("Account balance cannot overflow MAX_MONEY")
557    }
558
559    /// Returns the value in the account of notes and transparent UTXOs that have value less than
560    /// the marginal fee, and consequently cannot be spent except as a grace input.
561    pub fn uneconomic_value(&self) -> Zatoshis {
562        (self.sapling_balance.uneconomic_value
563            + self.orchard_balance.uneconomic_value
564            + self.ironwood_balance.uneconomic_value
565            + self.unshielded_regular_balance.uneconomic_value
566            + self.unshielded_coinbase_balance.uneconomic_value)
567            .expect("Account balance cannot overflow MAX_MONEY")
568    }
569}
570
571/// Source metadata for a ZIP 32-derived key.
572#[derive(Clone, Debug, PartialEq, Eq, Hash)]
573pub struct Zip32Derivation {
574    seed_fingerprint: SeedFingerprint,
575    account_index: zip32::AccountId,
576    #[cfg(feature = "zcashd-compat")]
577    legacy_address_index: Option<zcashd::LegacyAddressIndex>,
578}
579
580impl Zip32Derivation {
581    /// Constructs new derivation metadata from its constituent parts.
582    pub fn new(
583        seed_fingerprint: SeedFingerprint,
584        account_index: zip32::AccountId,
585        #[cfg(feature = "zcashd-compat")] legacy_address_index: Option<zcashd::LegacyAddressIndex>,
586    ) -> Self {
587        Self {
588            seed_fingerprint,
589            account_index,
590            #[cfg(feature = "zcashd-compat")]
591            legacy_address_index,
592        }
593    }
594
595    /// Returns the seed fingerprint.
596    pub fn seed_fingerprint(&self) -> &SeedFingerprint {
597        &self.seed_fingerprint
598    }
599
600    /// Returns the account-level index in the ZIP 32 derivation path.
601    pub fn account_index(&self) -> zip32::AccountId {
602        self.account_index
603    }
604
605    #[cfg(feature = "zcashd-compat")]
606    pub fn legacy_address_index(&self) -> Option<zcashd::LegacyAddressIndex> {
607        self.legacy_address_index
608    }
609}
610
611/// An enumeration used to control what information is tracked by the wallet for
612/// notes received by a given account.
613#[derive(Clone, Debug, PartialEq, Eq, Hash)]
614pub enum AccountPurpose {
615    /// For spending accounts, the wallet will track information needed to spend
616    /// received notes.
617    Spending { derivation: Option<Zip32Derivation> },
618    /// For view-only accounts, the wallet will not track spend information.
619    ViewOnly,
620}
621
622/// The kinds of accounts supported by `zcash_client_backend`.
623#[derive(Clone, Debug, PartialEq, Eq, Hash)]
624pub enum AccountSource {
625    /// An account derived from a known seed.
626    Derived {
627        derivation: Zip32Derivation,
628        key_source: Option<String>,
629    },
630
631    /// An account imported from a viewing key.
632    Imported {
633        purpose: AccountPurpose,
634        key_source: Option<String>,
635    },
636}
637
638impl AccountSource {
639    /// Returns the key derivation metadata for the account source, if any is available.
640    pub fn key_derivation(&self) -> Option<&Zip32Derivation> {
641        match self {
642            AccountSource::Derived { derivation, .. } => Some(derivation),
643            AccountSource::Imported {
644                purpose: AccountPurpose::Spending { derivation },
645                ..
646            } => derivation.as_ref(),
647            _ => None,
648        }
649    }
650
651    /// Returns the application-level key source identifier.
652    pub fn key_source(&self) -> Option<&str> {
653        match self {
654            AccountSource::Derived { key_source, .. } => key_source.as_ref().map(|s| s.as_str()),
655            AccountSource::Imported { key_source, .. } => key_source.as_ref().map(|s| s.as_str()),
656        }
657    }
658}
659
660/// A set of capabilities that a client account must provide.
661///
662/// An account represents a distinct set of viewing keys within the wallet; the keys for an account
663/// must not be shared with any other account in the wallet, and an application managing wallet
664/// accounts must ensure that it either maintains spending keys that can be used for spending _all_
665/// outputs detectable by the viewing keys of the account, or for none of them (i.e. the account is
666/// view-only.)
667///
668/// Balance information is available for any full-viewing-key based account; for an
669/// incoming-viewing-key only account balance cannot be determined because spends cannot be
670/// detected, and so balance-related APIs and APIs that rely upon spentness checks MUST be
671/// implemented to return errors if invoked for an IVK-only account.
672///
673/// For spending accounts in implementations that support the `transparent-key-import` feature,
674/// care must be taken to ensure that spending keys corresponding to every imported transparent
675/// address in an account are maintained by the application.
676pub trait Account {
677    type AccountId: Copy;
678
679    /// Returns the unique identifier for the account.
680    fn id(&self) -> Self::AccountId;
681
682    /// Returns the human-readable name for the account, if any has been configured.
683    fn name(&self) -> Option<&str>;
684
685    /// Returns the birthday height for the account.
686    fn birthday_height(&self) -> BlockHeight;
687
688    /// Returns whether this account is derived or imported, and the derivation parameters
689    /// if applicable.
690    fn source(&self) -> &AccountSource;
691
692    /// Returns whether the account is a spending account or a view-only account.
693    fn purpose(&self) -> AccountPurpose {
694        match self.source() {
695            AccountSource::Derived { derivation, .. } => AccountPurpose::Spending {
696                derivation: Some(derivation.clone()),
697            },
698            AccountSource::Imported { purpose, .. } => purpose.clone(),
699        }
700    }
701
702    /// Returns the UFVK that the wallet backend has stored for the account, if any.
703    ///
704    /// Accounts for which this returns `None` cannot be used in wallet contexts, because
705    /// they are unable to maintain an accurate balance.
706    fn ufvk(&self) -> Option<&UnifiedFullViewingKey>;
707
708    /// Returns the UIVK that the wallet backend has stored for the account.
709    ///
710    /// All accounts are required to have at least an incoming viewing key. This gives no
711    /// indication about whether an account can be used in a wallet context; for that, use
712    /// [`Account::ufvk`].
713    fn uivk(&self) -> UnifiedIncomingViewingKey;
714}
715
716#[cfg(any(test, feature = "test-dependencies"))]
717impl<A: Copy> Account for (A, UnifiedFullViewingKey, BlockHeight) {
718    type AccountId = A;
719
720    fn id(&self) -> A {
721        self.0
722    }
723
724    fn name(&self) -> Option<&str> {
725        None
726    }
727
728    fn birthday_height(&self) -> BlockHeight {
729        self.2
730    }
731
732    fn source(&self) -> &AccountSource {
733        &AccountSource::Imported {
734            purpose: AccountPurpose::ViewOnly,
735            key_source: None,
736        }
737    }
738
739    fn ufvk(&self) -> Option<&UnifiedFullViewingKey> {
740        Some(&self.1)
741    }
742
743    fn uivk(&self) -> UnifiedIncomingViewingKey {
744        self.1.to_unified_incoming_viewing_key()
745    }
746}
747
748#[cfg(any(test, feature = "test-dependencies"))]
749impl<A: Copy> Account for (A, UnifiedIncomingViewingKey, BlockHeight) {
750    type AccountId = A;
751
752    fn id(&self) -> A {
753        self.0
754    }
755
756    fn name(&self) -> Option<&str> {
757        None
758    }
759
760    fn birthday_height(&self) -> BlockHeight {
761        self.2
762    }
763
764    fn source(&self) -> &AccountSource {
765        &AccountSource::Imported {
766            purpose: AccountPurpose::ViewOnly,
767            key_source: None,
768        }
769    }
770
771    fn ufvk(&self) -> Option<&UnifiedFullViewingKey> {
772        None
773    }
774
775    fn uivk(&self) -> UnifiedIncomingViewingKey {
776        self.1.clone()
777    }
778}
779
780/// Source metadata for an address in the wallet.
781#[derive(Clone, Copy, Debug, PartialEq, Eq)]
782pub enum AddressSource {
783    /// The address was produced by HD derivation via a known path, with the given diversifier
784    /// index.
785    Derived {
786        diversifier_index: DiversifierIndex,
787        #[cfg(feature = "transparent-inputs")]
788        transparent_key_scope: Option<TransparentKeyScope>,
789    },
790    /// No derivation information is available; this is common for imported addresses.
791    #[cfg(feature = "transparent-key-import")]
792    Standalone,
793}
794
795impl AddressSource {
796    /// Returns the transparent key scope at which the address was derived, if this source metadata
797    /// is for a transparent address derived from a UIVK in the wallet.
798    #[cfg(feature = "transparent-inputs")]
799    pub fn transparent_key_scope(&self) -> Option<&TransparentKeyScope> {
800        match self {
801            AddressSource::Derived {
802                transparent_key_scope,
803                ..
804            } => transparent_key_scope.as_ref(),
805            #[cfg(feature = "transparent-key-import")]
806            AddressSource::Standalone => None,
807        }
808    }
809}
810
811/// Information about an address in the wallet.
812#[derive(Clone)]
813pub struct AddressInfo {
814    address: Address,
815    source: AddressSource,
816}
817
818impl AddressInfo {
819    /// Constructs an `AddressInfo` from its constituent parts.
820    pub fn from_parts(address: Address, source: AddressSource) -> Option<Self> {
821        // Only allow `transparent_key_scope` to be set for transparent addresses.
822        #[cfg(feature = "transparent-inputs")]
823        let valid = source.transparent_key_scope().is_none()
824            || matches!(address, Address::Transparent(_) | Address::Tex(_));
825        #[cfg(not(feature = "transparent-inputs"))]
826        let valid = true;
827
828        valid.then_some(Self { address, source })
829    }
830
831    /// Returns the address itself.
832    pub fn address(&self) -> &Address {
833        &self.address
834    }
835
836    /// Returns the source metadata for the address.
837    pub fn source(&self) -> AddressSource {
838        self.source
839    }
840
841    /// Returns the key scope if this is a transparent address.
842    #[cfg(feature = "transparent-inputs")]
843    #[deprecated(
844        since = "0.20.0",
845        note = "use AddressSource::transparent_key_scope instead"
846    )]
847    pub fn transparent_key_scope(&self) -> Option<&TransparentKeyScope> {
848        self.source.transparent_key_scope()
849    }
850}
851
852/// A polymorphic ratio type, usually used for rational numbers.
853#[derive(Clone, Copy, Debug, PartialEq, Eq)]
854pub struct Ratio<T> {
855    numerator: T,
856    denominator: T,
857}
858
859impl<T> Ratio<T> {
860    /// Constructs a new Ratio from a numerator and a denominator.
861    pub fn new(numerator: T, denominator: T) -> Self {
862        Self {
863            numerator,
864            denominator,
865        }
866    }
867
868    /// Returns the numerator of the ratio.
869    pub fn numerator(&self) -> &T {
870        &self.numerator
871    }
872
873    /// Returns the denominator of the ratio.
874    pub fn denominator(&self) -> &T {
875        &self.denominator
876    }
877}
878
879/// A type representing the progress the wallet has made toward detecting all of the funds
880/// belonging to the wallet.
881///
882/// The window over which progress is computed spans from the wallet's birthday to the current
883/// chain tip. It is divided into two regions, the "Scan Window" which covers the region from the
884/// wallet recovery height to the current chain tip, and the "Recovery Window" which covers the
885/// range from the wallet birthday to the wallet recovery height. If no wallet recovery height is
886/// available, the scan window will cover the entire range from the wallet birthday to the chain
887/// tip.
888///
889/// Progress for both scanning and recovery is represented in terms of the ratio between notes
890/// scanned and the total number of notes added to the chain in the relevant window. This ratio
891/// should only be used to compute progress percentages for display, and the numerator and
892/// denominator should not be treated as authoritative note counts. In the case that there are no
893/// notes in a given block range, the denominator of these values will be zero, so callers should always
894/// use checked division when converting the resulting values to percentages.
895#[derive(Debug, Clone, Copy, PartialEq, Eq)]
896pub struct Progress {
897    scan: Ratio<u64>,
898    recovery: Option<Ratio<u64>>,
899}
900
901impl Progress {
902    /// Constructs a new progress value from its constituent parts.
903    pub fn new(scan: Ratio<u64>, recovery: Option<Ratio<u64>>) -> Self {
904        Self { scan, recovery }
905    }
906
907    /// Returns the progress the wallet has made in scanning blocks for shielded notes belonging to
908    /// the wallet between the wallet recovery height (or the wallet birthday if no recovery height
909    /// is set) and the chain tip.
910    pub fn scan(&self) -> Ratio<u64> {
911        self.scan
912    }
913
914    /// Returns the progress the wallet has made in scanning blocks for shielded notes belonging to
915    /// the wallet between the wallet birthday and the block height at which recovery from seed was
916    /// initiated.
917    ///
918    /// Returns `None` if no recovery height is set for the wallet.
919    pub fn recovery(&self) -> Option<Ratio<u64>> {
920        self.recovery
921    }
922}
923
924/// A type representing the potentially-spendable value of unspent outputs in the wallet.
925///
926/// The balances reported using this data structure may overestimate the total spendable value of
927/// the wallet, in the case that the spend of a previously received shielded note has not yet been
928/// detected by the process of scanning the chain. The balances reported using this data structure
929/// can only be certain to be unspent in the case that [`Self::is_synced`] is true, and even in
930/// this circumstance it is possible that a newly created transaction could conflict with a
931/// not-yet-mined transaction in the mempool.
932#[derive(Debug, Clone, PartialEq, Eq)]
933pub struct WalletSummary<AccountId: Eq + Hash> {
934    account_balances: HashMap<AccountId, AccountBalance>,
935    chain_tip_height: BlockHeight,
936    fully_scanned_height: BlockHeight,
937    progress: Progress,
938    next_sapling_subtree_index: u64,
939    #[cfg(feature = "orchard")]
940    next_orchard_subtree_index: u64,
941    #[cfg(feature = "orchard")]
942    next_ironwood_subtree_index: u64,
943}
944
945impl<AccountId: Eq + Hash> WalletSummary<AccountId> {
946    /// Constructs a new [`WalletSummary`] from its constituent parts.
947    pub fn new(
948        account_balances: HashMap<AccountId, AccountBalance>,
949        chain_tip_height: BlockHeight,
950        fully_scanned_height: BlockHeight,
951        progress: Progress,
952        next_sapling_subtree_index: u64,
953        #[cfg(feature = "orchard")] next_orchard_subtree_index: u64,
954        #[cfg(feature = "orchard")] next_ironwood_subtree_index: u64,
955    ) -> Self {
956        Self {
957            account_balances,
958            chain_tip_height,
959            fully_scanned_height,
960            progress,
961            next_sapling_subtree_index,
962            #[cfg(feature = "orchard")]
963            next_orchard_subtree_index,
964            #[cfg(feature = "orchard")]
965            next_ironwood_subtree_index,
966        }
967    }
968
969    /// Returns the balances of accounts in the wallet, keyed by account ID.
970    pub fn account_balances(&self) -> &HashMap<AccountId, AccountBalance> {
971        &self.account_balances
972    }
973
974    /// Returns the height of the current chain tip.
975    pub fn chain_tip_height(&self) -> BlockHeight {
976        self.chain_tip_height
977    }
978
979    /// Returns the height below which all blocks have been scanned by the wallet, ignoring blocks
980    /// below the wallet birthday.
981    pub fn fully_scanned_height(&self) -> BlockHeight {
982        self.fully_scanned_height
983    }
984
985    /// Returns the progress of scanning the chain to bring the wallet up to date.
986    ///
987    /// This progress metric is intended as an indicator of how close the wallet is to
988    /// general usability, including the ability to spend existing funds that were
989    /// previously spendable.
990    ///
991    /// The window over which progress is computed spans from the wallet's birthday to the current
992    /// chain tip. It is divided into two segments: a "recovery" segment, between the wallet
993    /// birthday and the recovery height (currently the height at which recovery from seed was
994    /// initiated, but how this boundary is computed may change in the future), and a "scan"
995    /// segment, between the recovery height and the current chain tip.
996    ///
997    /// When converting the ratios returned here to percentages, checked division must be used in
998    /// order to avoid divide-by-zero errors. A zero denominator in a returned ratio indicates that
999    /// there are no shielded notes to be scanned in the associated block range.
1000    pub fn progress(&self) -> Progress {
1001        self.progress
1002    }
1003
1004    /// Returns the Sapling subtree index that should start the next range of subtree
1005    /// roots passed to [`WalletCommitmentTrees::put_sapling_subtree_roots`].
1006    pub fn next_sapling_subtree_index(&self) -> u64 {
1007        self.next_sapling_subtree_index
1008    }
1009
1010    /// Returns the Orchard subtree index that should start the next range of subtree
1011    /// roots passed to [`WalletCommitmentTrees::put_orchard_subtree_roots`].
1012    #[cfg(feature = "orchard")]
1013    pub fn next_orchard_subtree_index(&self) -> u64 {
1014        self.next_orchard_subtree_index
1015    }
1016
1017    /// Returns the Ironwood subtree index that should start the next range of subtree
1018    /// roots passed to [`WalletCommitmentTrees::put_ironwood_subtree_roots`].
1019    #[cfg(feature = "orchard")]
1020    pub fn next_ironwood_subtree_index(&self) -> u64 {
1021        self.next_ironwood_subtree_index
1022    }
1023
1024    /// Returns whether or not wallet scanning is complete.
1025    pub fn is_synced(&self) -> bool {
1026        self.chain_tip_height == self.fully_scanned_height
1027    }
1028}
1029
1030/// A predicate that can be used to choose whether or not a particular note is retained in note
1031/// selection.
1032pub trait NoteRetention<NoteRef> {
1033    /// Returns whether the specified Sapling note should be retained.
1034    fn should_retain_sapling(&self, note: &ReceivedNote<NoteRef, sapling::Note>) -> bool;
1035    /// Returns whether the specified Orchard note should be retained.
1036    #[cfg(feature = "orchard")]
1037    fn should_retain_orchard(&self, note: &ReceivedNote<NoteRef, orchard::note::Note>) -> bool;
1038    /// Returns whether the specified Ironwood note should be retained. Ironwood notes are
1039    /// Orchard-shaped, so this uses the same note type as Orchard.
1040    #[cfg(feature = "orchard")]
1041    fn should_retain_ironwood(&self, note: &ReceivedNote<NoteRef, orchard::note::Note>) -> bool;
1042}
1043
1044pub(crate) struct SimpleNoteRetention {
1045    pub(crate) sapling: bool,
1046    #[cfg(feature = "orchard")]
1047    pub(crate) orchard: bool,
1048    #[cfg(feature = "orchard")]
1049    pub(crate) ironwood: bool,
1050}
1051
1052impl<NoteRef> NoteRetention<NoteRef> for SimpleNoteRetention {
1053    fn should_retain_sapling(&self, _: &ReceivedNote<NoteRef, sapling::Note>) -> bool {
1054        self.sapling
1055    }
1056
1057    #[cfg(feature = "orchard")]
1058    fn should_retain_orchard(&self, _: &ReceivedNote<NoteRef, orchard::note::Note>) -> bool {
1059        self.orchard
1060    }
1061
1062    #[cfg(feature = "orchard")]
1063    fn should_retain_ironwood(&self, _: &ReceivedNote<NoteRef, orchard::note::Note>) -> bool {
1064        self.ironwood
1065    }
1066}
1067
1068/// Shielded outputs that were received by the wallet.
1069#[derive(Debug)]
1070pub struct ReceivedNotes<NoteRef> {
1071    sapling: Vec<ReceivedNote<NoteRef, sapling::Note>>,
1072    #[cfg(feature = "orchard")]
1073    orchard: Vec<ReceivedNote<NoteRef, orchard::note::Note>>,
1074    // Ironwood notes are Orchard-shaped `orchard::note::Note` values (note plaintext version 3),
1075    // but are tracked as a distinct pool so that Orchard and Ironwood value and bundle action
1076    // counts are accounted for separately.
1077    #[cfg(feature = "orchard")]
1078    ironwood: Vec<ReceivedNote<NoteRef, orchard::note::Note>>,
1079}
1080
1081/// The necessary and optional notes returned for consolidation-aware input selection.
1082///
1083/// `funding` contains the notes selected to fund the requested value. `additional` contains
1084/// disjoint, ordinarily spendable notes that the input selector may add when doing so does not
1085/// change the transaction's fee or observable shape.
1086#[derive(Debug)]
1087pub struct ConsolidationNotes<NoteRef> {
1088    funding: ReceivedNotes<NoteRef>,
1089    additional: ReceivedNotes<NoteRef>,
1090}
1091
1092impl<NoteRef> ConsolidationNotes<NoteRef> {
1093    /// Constructs a consolidation selection from its necessary and optional notes.
1094    ///
1095    /// `funding` and `additional` must be disjoint and contain notes only from the requested
1096    /// source pool. `additional` must also obey the caller's candidate limit and use the same
1097    /// preferred lock tier as `funding`.
1098    pub fn from_parts(funding: ReceivedNotes<NoteRef>, additional: ReceivedNotes<NoteRef>) -> Self {
1099        Self {
1100            funding,
1101            additional,
1102        }
1103    }
1104
1105    /// Consumes this selection and returns its necessary and optional notes.
1106    pub fn into_parts(self) -> (ReceivedNotes<NoteRef>, ReceivedNotes<NoteRef>) {
1107        (self.funding, self.additional)
1108    }
1109}
1110
1111impl<NoteRef> ReceivedNotes<NoteRef> {
1112    /// Construct a new empty [`ReceivedNotes`].
1113    pub fn empty() -> Self {
1114        Self::new(
1115            vec![],
1116            #[cfg(feature = "orchard")]
1117            vec![],
1118            #[cfg(feature = "orchard")]
1119            vec![],
1120        )
1121    }
1122
1123    /// Construct a new [`ReceivedNotes`] from its constituent parts.
1124    pub fn new(
1125        sapling: Vec<ReceivedNote<NoteRef, sapling::Note>>,
1126        #[cfg(feature = "orchard")] orchard: Vec<ReceivedNote<NoteRef, orchard::note::Note>>,
1127        #[cfg(feature = "orchard")] ironwood: Vec<ReceivedNote<NoteRef, orchard::note::Note>>,
1128    ) -> Self {
1129        Self {
1130            sapling,
1131            #[cfg(feature = "orchard")]
1132            orchard,
1133            #[cfg(feature = "orchard")]
1134            ironwood,
1135        }
1136    }
1137
1138    /// Returns the set of spendable Sapling notes.
1139    pub fn sapling(&self) -> &[ReceivedNote<NoteRef, sapling::Note>] {
1140        self.sapling.as_ref()
1141    }
1142
1143    /// Consumes this value and returns the Sapling notes contained within it.
1144    pub fn take_sapling(self) -> Vec<ReceivedNote<NoteRef, sapling::Note>> {
1145        self.sapling
1146    }
1147
1148    /// Returns the set of spendable Orchard notes.
1149    #[cfg(feature = "orchard")]
1150    pub fn orchard(&self) -> &[ReceivedNote<NoteRef, orchard::note::Note>] {
1151        self.orchard.as_ref()
1152    }
1153
1154    /// Consumes this value and returns the Orchard notes contained within it.
1155    #[cfg(feature = "orchard")]
1156    pub fn take_orchard(self) -> Vec<ReceivedNote<NoteRef, orchard::note::Note>> {
1157        self.orchard
1158    }
1159
1160    /// Returns the set of spendable Ironwood notes.
1161    #[cfg(feature = "orchard")]
1162    pub fn ironwood(&self) -> &[ReceivedNote<NoteRef, orchard::note::Note>] {
1163        self.ironwood.as_ref()
1164    }
1165
1166    /// Consumes this value and returns the Ironwood notes contained within it.
1167    #[cfg(feature = "orchard")]
1168    pub fn take_ironwood(self) -> Vec<ReceivedNote<NoteRef, orchard::note::Note>> {
1169        self.ironwood
1170    }
1171
1172    /// Computes the total value of Sapling notes.
1173    pub fn sapling_value(&self) -> Result<Zatoshis, BalanceError> {
1174        self.sapling.iter().try_fold(Zatoshis::ZERO, |acc, n| {
1175            (acc + n.note_value()?).ok_or(BalanceError::Overflow)
1176        })
1177    }
1178
1179    /// Computes the total value of Orchard notes.
1180    #[cfg(feature = "orchard")]
1181    pub fn orchard_value(&self) -> Result<Zatoshis, BalanceError> {
1182        self.orchard.iter().try_fold(Zatoshis::ZERO, |acc, n| {
1183            (acc + n.note_value()?).ok_or(BalanceError::Overflow)
1184        })
1185    }
1186
1187    /// Computes the total value of Ironwood notes.
1188    #[cfg(feature = "orchard")]
1189    pub fn ironwood_value(&self) -> Result<Zatoshis, BalanceError> {
1190        self.ironwood.iter().try_fold(Zatoshis::ZERO, |acc, n| {
1191            (acc + n.note_value()?).ok_or(BalanceError::Overflow)
1192        })
1193    }
1194
1195    /// Computes the total value of spendable inputs
1196    pub fn total_value(&self) -> Result<Zatoshis, BalanceError> {
1197        #[cfg(not(feature = "orchard"))]
1198        return self.sapling_value();
1199
1200        #[cfg(feature = "orchard")]
1201        return (self.sapling_value()? + self.orchard_value()? + self.ironwood_value()?)
1202            .ok_or(BalanceError::Overflow);
1203    }
1204
1205    /// Returns whether the collection contains no notes in any pool.
1206    pub fn is_empty(&self) -> bool {
1207        #[cfg(not(feature = "orchard"))]
1208        return self.sapling.is_empty();
1209
1210        #[cfg(feature = "orchard")]
1211        return self.sapling.is_empty() && self.orchard.is_empty() && self.ironwood.is_empty();
1212    }
1213
1214    /// Appends each pool's notes from `other` to this collection.
1215    pub(crate) fn append(&mut self, mut other: Self) {
1216        self.sapling.append(&mut other.sapling);
1217        #[cfg(feature = "orchard")]
1218        {
1219            self.orchard.append(&mut other.orchard);
1220            self.ironwood.append(&mut other.ironwood);
1221        }
1222    }
1223
1224    /// Consumes this collection, returning one holding only the OLDEST single note whose value
1225    /// alone is at least `value`, drawn from the first pool in `sources` that holds one; the
1226    /// result is empty when no single note qualifies. Age is the note's commitment tree
1227    /// position, which is assigned in strict chain order.
1228    ///
1229    /// This is the best-effort reduction behind the default implementation of
1230    /// [`InputSource::select_single_spendable_note`]: it can only choose among the notes it
1231    /// holds, so a covering note the producing selection did not surface cannot be found here.
1232    pub fn into_single_covering(mut self, value: Zatoshis, sources: &[ShieldedPool]) -> Self {
1233        fn take_oldest_covering<NoteRef, N>(
1234            notes: &mut Vec<ReceivedNote<NoteRef, N>>,
1235            covers: impl Fn(&ReceivedNote<NoteRef, N>) -> bool,
1236        ) -> Option<ReceivedNote<NoteRef, N>> {
1237            let idx = notes
1238                .iter()
1239                .enumerate()
1240                .filter(|(_, n)| covers(n))
1241                .min_by_key(|(_, n)| n.note_commitment_tree_position())
1242                .map(|(idx, _)| idx)?;
1243            Some(notes.swap_remove(idx))
1244        }
1245
1246        for pool in sources {
1247            match pool {
1248                ShieldedPool::Sapling => {
1249                    if let Some(note) = take_oldest_covering(&mut self.sapling, |n| {
1250                        n.note_value().is_ok_and(|v| v >= value)
1251                    }) {
1252                        return Self::new(
1253                            vec![note],
1254                            #[cfg(feature = "orchard")]
1255                            vec![],
1256                            #[cfg(feature = "orchard")]
1257                            vec![],
1258                        );
1259                    }
1260                }
1261                #[cfg(feature = "orchard")]
1262                ShieldedPool::Orchard => {
1263                    if let Some(note) = take_oldest_covering(&mut self.orchard, |n| {
1264                        n.note_value().is_ok_and(|v| v >= value)
1265                    }) {
1266                        return Self::new(vec![], vec![note], vec![]);
1267                    }
1268                }
1269                #[cfg(feature = "orchard")]
1270                ShieldedPool::Ironwood => {
1271                    if let Some(note) = take_oldest_covering(&mut self.ironwood, |n| {
1272                        n.note_value().is_ok_and(|v| v >= value)
1273                    }) {
1274                        return Self::new(vec![], vec![], vec![note]);
1275                    }
1276                }
1277                #[cfg(not(feature = "orchard"))]
1278                ShieldedPool::Orchard | ShieldedPool::Ironwood => {}
1279            }
1280        }
1281        Self::empty()
1282    }
1283
1284    /// Consumes this [`ReceivedNotes`] value and produces a vector of
1285    /// [`ReceivedNote<NoteRef, Note>`] values.
1286    pub fn into_vec(
1287        self,
1288        retention: &impl NoteRetention<NoteRef>,
1289    ) -> Vec<ReceivedNote<NoteRef, Note>> {
1290        let iter = self.sapling.into_iter().filter_map(|n| {
1291            retention
1292                .should_retain_sapling(&n)
1293                .then(|| n.map_note(Note::Sapling))
1294        });
1295
1296        #[cfg(feature = "orchard")]
1297        let iter = iter.chain(self.orchard.into_iter().filter_map(|n| {
1298            retention.should_retain_orchard(&n).then(|| {
1299                n.map_note(|note| Note::Orchard {
1300                    note,
1301                    pool: orchard::ValuePool::Orchard,
1302                })
1303            })
1304        }));
1305
1306        // Ironwood notes are `orchard::note::Note` values, so they are emitted as `Note::Orchard`;
1307        // the transaction builder routes them to the Ironwood bundle by their version 3 plaintext.
1308        #[cfg(feature = "orchard")]
1309        let iter = iter.chain(self.ironwood.into_iter().filter_map(|n| {
1310            retention.should_retain_ironwood(&n).then(|| {
1311                n.map_note(|note| Note::Orchard {
1312                    note,
1313                    pool: orchard::ValuePool::Ironwood,
1314                })
1315            })
1316        }));
1317
1318        iter.collect()
1319    }
1320}
1321
1322/// A type describing the mined-ness of transactions that should be returned in response to a
1323/// [`TransactionDataRequest`].
1324#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1325#[cfg(feature = "transparent-inputs")]
1326pub enum TransactionStatusFilter {
1327    /// Only mined transactions should be returned.
1328    Mined,
1329    /// Only mempool transactions should be returned.
1330    Mempool,
1331    /// Both mined transactions and transactions in the mempool should be returned.
1332    All,
1333}
1334
1335/// A type used to filter transactions to be returned in response to a [`TransactionDataRequest`],
1336/// in terms of the spentness of the transaction's transparent outputs.
1337#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1338#[cfg(feature = "transparent-inputs")]
1339pub enum OutputStatusFilter {
1340    /// Only transactions that have currently-unspent transparent outputs should be returned.
1341    Unspent,
1342    /// All transactions corresponding to the data request should be returned, irrespective of
1343    /// whether or not those transactions produce transparent outputs that are currently unspent.
1344    All,
1345}
1346
1347/// Payload data for [`TransactionDataRequest::TransactionsInvolvingAddress`].
1348///
1349/// Values of this type are not constructed directly, but are instead constructed using
1350/// [`TransactionDataRequest::transactions_involving_address`].
1351#[cfg(feature = "transparent-inputs")]
1352#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Getters, CopyGetters)]
1353pub struct TransactionsInvolvingAddress {
1354    /// The address to request transactions and/or UTXOs for.
1355    #[getset(get_copy = "pub")]
1356    address: TransparentAddress,
1357    /// Only transactions mined at heights greater than or equal to this height should be
1358    /// returned.
1359    #[getset(get_copy = "pub")]
1360    block_range_start: BlockHeight,
1361    /// If set, only transactions mined at heights less than this height should be returned.
1362    #[getset(get_copy = "pub")]
1363    block_range_end: Option<BlockHeight>,
1364    /// If a `request_at` time is set, the caller evaluating this request should attempt to
1365    /// retrieve transaction data related to the specified address at a time that is as close
1366    /// as practical to the specified instant, and in a fashion that decorrelates this request
1367    /// to a light wallet server from other requests made by the same caller.
1368    ///
1369    /// This may be ignored by callers that are able to satisfy the request without exposing
1370    /// correlations between addresses to untrusted parties; for example, a wallet application
1371    /// that uses a private, trusted-for-privacy supplier of chain data can safely ignore this
1372    /// field.
1373    #[getset(get_copy = "pub")]
1374    request_at: Option<SystemTime>,
1375    /// The caller should respond to this request only with transactions that conform to the
1376    /// specified transaction status filter.
1377    #[getset(get = "pub")]
1378    tx_status_filter: TransactionStatusFilter,
1379    /// The caller should respond to this request only with transactions containing outputs
1380    /// that conform to the specified output status filter.
1381    #[getset(get = "pub")]
1382    output_status_filter: OutputStatusFilter,
1383}
1384
1385/// A request for transaction data enhancement, spentness check, or discovery
1386/// of spends from a given transparent address within a specific block range.
1387#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1388pub enum TransactionDataRequest {
1389    /// Information about the chain's view of a transaction is requested.
1390    ///
1391    /// The caller evaluating this request on behalf of the wallet backend should respond to this
1392    /// request by determining the status of the specified transaction with respect to the main
1393    /// chain; if using `lightwalletd` for access to chain data, this may be obtained by
1394    /// interpreting the results of the `GetTransaction` RPC method. It should then call
1395    /// [`WalletWrite::set_transaction_status`] to provide the resulting transaction status
1396    /// information to the wallet backend.
1397    GetStatus(TxId),
1398    /// Transaction enhancement (download of complete raw transaction data) is requested.
1399    ///
1400    /// The caller evaluating this request on behalf of the wallet backend should respond to this
1401    /// request by providing complete data for the specified transaction to
1402    /// [`wallet::decrypt_and_store_transaction`]; if using `lightwalletd` for access to chain
1403    /// state, this may be obtained via the `GetTransaction` RPC method. If no data is available
1404    /// for the specified transaction, this should be reported to the backend using
1405    /// [`WalletWrite::set_transaction_status`]. A [`TransactionDataRequest::Enhancement`] request
1406    /// subsumes any previously existing [`TransactionDataRequest::GetStatus`] request.
1407    Enhancement(TxId),
1408    /// Information about transactions that receive or spend funds belonging to the specified
1409    /// transparent address is requested.
1410    ///
1411    /// Fully transparent transactions, and transactions that do not contain either shielded inputs
1412    /// or shielded outputs belonging to the wallet, may not be discovered by the process of
1413    /// out-of-order chain scanning due to race conditions related to advancing the transparent
1414    /// address gap limit; as a consequence, the wallet must actively query to find transactions
1415    /// that spend such funds. Ideally we'd be able to query by [`OutPoint`] but this is not
1416    /// currently functionality that is supported by the light wallet server; for full-node wallets
1417    /// or other arrangements that allow privacy-preserving retrieval of individually identifying
1418    /// information such as backends that support private information retrieval (PIR) consider
1419    /// enabling the `spend-index` feature.
1420    ///
1421    /// The caller evaluating this request on behalf of the wallet backend should respond to this
1422    /// request by detecting transactions involving the specified address within the provided block
1423    /// range; if using `lightwalletd` for access to chain data, this may be performed using the
1424    /// `GetTaddressTxids` RPC method. It should then call [`wallet::decrypt_and_store_transaction`]
1425    /// for each transaction so detected. If no transactions are detected within the given range,
1426    /// the caller should instead invoke [`WalletWrite::notify_address_checked`] with
1427    /// `block_end_height - 1` as the `as_of_height` argument.
1428    #[cfg(feature = "transparent-inputs")]
1429    TransactionsInvolvingAddress(TransactionsInvolvingAddress),
1430    /// The main-chain transaction that spends of a specific transparent output, or confirmation
1431    /// that the output remains unspent, is requested.
1432    ///
1433    /// When the `spend-index` feature is enabled, `GetSpendingTx` requests will be emitted by the
1434    /// backend instead of [`TransactionDataRequest::TransactionsInvolvingAddress`], in
1435    /// circumstances when spentness determination is needed. This feature should only be enabled
1436    /// for wallets whose chain-data source can resolve the spend of an individual outpoint
1437    /// directly — for example a full node that maintains a spent-outpoint index. It avoids needing
1438    /// to retrieve potentially large amounts of transaction history (in the cas of a
1439    /// heavily-reused address) just to discover whether one output was spent.
1440    ///
1441    /// The caller evaluating this request on behalf of the wallet backend should determine whether
1442    /// `outpoint` has been spent on the main chain. Spentness should be taken from an
1443    /// authoritative source (e.g. the node's UTXO set); the spend index is used only to identify
1444    /// the spending transaction. If the output is spent, the caller should provide the spending
1445    /// transaction to [`wallet::decrypt_and_store_transaction`]. If the output is confirmed
1446    /// unspent as of some height, the caller should invoke
1447    /// [`WalletWrite::notify_output_verified_unspent`] with that height. If the output is known to
1448    /// be spent but the spending transaction cannot yet be resolved (e.g. an index is still being
1449    /// built — see ZcashFoundation/zebra#10806), the caller should do nothing, so that the request
1450    /// is re-issued and retried later.
1451    #[cfg(feature = "spend-index")]
1452    GetSpendingTx(OutPoint),
1453}
1454
1455impl TransactionDataRequest {
1456    /// Constructs a request for Information about transactions that receive or spend funds
1457    /// belonging to the specified transparent address.
1458    ///
1459    /// # Parameters:
1460    /// - `address`: The address to request transactions and/or UTXOs for.
1461    /// - `block_range_start`: Only transactions mined at heights greater than or equal to this
1462    ///   height should be returned.
1463    /// - `block_range_end`: Only transactions mined at heights less than this height should be
1464    ///   returned.
1465    /// - `request_at`: If a `request_at` time is set, the caller evaluating this request should attempt to
1466    ///   retrieve transaction data related to the specified address at a time that is as close
1467    ///   as practical to the specified instant, and in a fashion that decorrelates this request
1468    ///   to a light wallet server from other requests made by the same caller. This may be ignored
1469    ///   by callers that are able to satisfy the request without exposing correlations between
1470    ///   addresses to untrusted parties; for example, a wallet application that uses a private,
1471    ///   trusted-for-privacy supplier of chain data can safely ignore this field.
1472    /// - `tx_status_filter`: The caller should respond to this request only with transactions that
1473    ///   conform to the specified transaction status filter.
1474    /// - `output_status_filter: The caller should respond to this request only with transactions
1475    ///   containing outputs that conform to the specified output status filter.
1476    ///
1477    /// See [`TransactionDataRequest::TransactionsInvolvingAddress`] for more information.
1478    #[cfg(feature = "transparent-inputs")]
1479    pub fn transactions_involving_address(
1480        address: TransparentAddress,
1481        block_range_start: BlockHeight,
1482        block_range_end: Option<BlockHeight>,
1483        request_at: Option<SystemTime>,
1484        tx_status_filter: TransactionStatusFilter,
1485        output_status_filter: OutputStatusFilter,
1486    ) -> Self {
1487        TransactionDataRequest::TransactionsInvolvingAddress(TransactionsInvolvingAddress {
1488            address,
1489            block_range_start,
1490            block_range_end,
1491            request_at,
1492            tx_status_filter,
1493            output_status_filter,
1494        })
1495    }
1496}
1497
1498/// Metadata about the status of a transaction obtained by inspecting the chain state.
1499#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1500pub enum TransactionStatus {
1501    /// The requested transaction ID was not recognized by the node.
1502    TxidNotRecognized,
1503    /// The requested transaction ID corresponds to a transaction that is recognized by the node,
1504    /// but is in the mempool or is otherwise not mined in the main chain (but may have been mined
1505    /// on a fork that was reorged away).
1506    NotInMainChain,
1507    /// The requested transaction ID corresponds to a transaction that has been included in the
1508    /// block at the provided height.
1509    Mined(BlockHeight),
1510}
1511
1512/// Metadata about the structure of unspent outputs in a single pool within a wallet account.
1513///
1514/// This type is often used to represent a filtered view of outputs in the account that were
1515/// selected according to the conditions imposed by a [`NoteFilter`].
1516#[derive(Debug, Clone)]
1517pub struct PoolMeta {
1518    note_count: usize,
1519    value: Zatoshis,
1520}
1521
1522impl PoolMeta {
1523    /// Constructs a new [`PoolMeta`] value from its constituent parts.
1524    pub fn new(note_count: usize, value: Zatoshis) -> Self {
1525        Self { note_count, value }
1526    }
1527
1528    /// Returns the number of unspent outputs in the account, potentially selected in accordance
1529    /// with some [`NoteFilter`].
1530    pub fn note_count(&self) -> usize {
1531        self.note_count
1532    }
1533
1534    /// Returns the total value of unspent outputs in the account that are accounted for in
1535    /// [`Self::note_count`].
1536    pub fn value(&self) -> Zatoshis {
1537        self.value
1538    }
1539}
1540
1541/// Metadata about the structure of the wallet for a particular account.
1542///
1543/// At present this just contains counts of unspent outputs in each pool, but it may be extended in
1544/// the future to contain note values or other more detailed information about wallet structure.
1545///
1546/// Values of this type are intended to be used in selection of change output values. A value of
1547/// this type may represent filtered data, and may therefore not count all of the unspent notes in
1548/// the wallet.
1549///
1550/// A [`AccountMeta`] value is normally produced by querying the wallet database via passing a
1551/// [`NoteFilter`] to [`InputSource::get_account_metadata`].
1552#[derive(Debug, Clone)]
1553pub struct AccountMeta {
1554    sapling: Option<PoolMeta>,
1555    orchard: Option<PoolMeta>,
1556    ironwood: Option<PoolMeta>,
1557}
1558
1559impl AccountMeta {
1560    /// Constructs a new [`AccountMeta`] value from its constituent parts.
1561    ///
1562    /// Ironwood metadata is tracked separately from Orchard, as Ironwood is a distinct pool.
1563    pub fn new(
1564        sapling: Option<PoolMeta>,
1565        orchard: Option<PoolMeta>,
1566        ironwood: Option<PoolMeta>,
1567    ) -> Self {
1568        Self {
1569            sapling,
1570            orchard,
1571            ironwood,
1572        }
1573    }
1574
1575    /// Returns metadata about Sapling notes belonging to the account for which this was generated.
1576    ///
1577    /// Returns [`None`] if no metadata is available or it was not possible to evaluate the query
1578    /// described by a [`NoteFilter`] given the available wallet data.
1579    pub fn sapling(&self) -> Option<&PoolMeta> {
1580        self.sapling.as_ref()
1581    }
1582
1583    /// Returns metadata about Orchard notes belonging to the account for which this was generated.
1584    ///
1585    /// Returns [`None`] if no metadata is available or it was not possible to evaluate the query
1586    /// described by a [`NoteFilter`] given the available wallet data.
1587    pub fn orchard(&self) -> Option<&PoolMeta> {
1588        self.orchard.as_ref()
1589    }
1590
1591    /// Returns metadata about Ironwood notes belonging to the account for which this was generated.
1592    ///
1593    /// Ironwood notes are Orchard-shaped but belong to a pool distinct from Orchard. Returns
1594    /// [`None`] if no metadata is available or it was not possible to evaluate the query described
1595    /// by a [`NoteFilter`] given the available wallet data.
1596    pub fn ironwood(&self) -> Option<&PoolMeta> {
1597        self.ironwood.as_ref()
1598    }
1599
1600    fn sapling_note_count(&self) -> Option<usize> {
1601        self.sapling.as_ref().map(|m| m.note_count)
1602    }
1603
1604    fn orchard_note_count(&self) -> Option<usize> {
1605        self.orchard.as_ref().map(|m| m.note_count)
1606    }
1607
1608    fn ironwood_note_count(&self) -> Option<usize> {
1609        self.ironwood.as_ref().map(|m| m.note_count)
1610    }
1611
1612    /// Returns the number of unspent notes in the wallet for the given shielded pool.
1613    pub fn note_count(&self, protocol: ShieldedPool) -> Option<usize> {
1614        match protocol {
1615            ShieldedPool::Sapling => self.sapling_note_count(),
1616            ShieldedPool::Orchard => self.orchard_note_count(),
1617            ShieldedPool::Ironwood => self.ironwood_note_count(),
1618        }
1619    }
1620
1621    /// Returns the total number of unspent shielded notes belonging to the account for which this
1622    /// was generated.
1623    ///
1624    /// Returns [`None`] if no metadata is available or it was not possible to evaluate the query
1625    /// described by a [`NoteFilter`] given the available wallet data. If metadata is available
1626    /// only for a single pool, the metadata for that pool will be returned.
1627    pub fn total_note_count(&self) -> Option<usize> {
1628        [
1629            self.sapling_note_count(),
1630            self.orchard_note_count(),
1631            self.ironwood_note_count(),
1632        ]
1633        .into_iter()
1634        .flatten()
1635        .reduce(|a, b| a + b)
1636    }
1637
1638    fn sapling_value(&self) -> Option<Zatoshis> {
1639        self.sapling.as_ref().map(|m| m.value)
1640    }
1641
1642    fn orchard_value(&self) -> Option<Zatoshis> {
1643        self.orchard.as_ref().map(|m| m.value)
1644    }
1645
1646    fn ironwood_value(&self) -> Option<Zatoshis> {
1647        self.ironwood.as_ref().map(|m| m.value)
1648    }
1649
1650    /// Returns the total value of shielded notes represented by [`Self::total_note_count`]
1651    ///
1652    /// Returns [`None`] if no metadata is available or it was not possible to evaluate the query
1653    /// described by a [`NoteFilter`] given the available wallet data. If metadata is available
1654    /// only for a single pool, the metadata for that pool will be returned.
1655    pub fn total_value(&self) -> Option<Zatoshis> {
1656        [
1657            self.sapling_value(),
1658            self.orchard_value(),
1659            self.ironwood_value(),
1660        ]
1661        .into_iter()
1662        .flatten()
1663        .reduce(|a, b| (a + b).expect("Does not overflow Zcash maximum value."))
1664    }
1665}
1666
1667/// A `u8` value in the range 0..=MAX
1668#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1669pub struct BoundedU8<const MAX: u8>(u8);
1670
1671impl<const MAX: u8> BoundedU8<MAX> {
1672    /// Creates a constant `BoundedU8` from a [`u8`] value.
1673    ///
1674    /// Panics: if the value is outside the range `0..=MAX`.
1675    pub const fn new_const(value: u8) -> Self {
1676        assert!(value <= MAX);
1677        Self(value)
1678    }
1679
1680    /// Creates a `BoundedU8` from a [`u8`] value.
1681    ///
1682    /// Returns `None` if the provided value is outside the range `0..=MAX`.
1683    pub fn new(value: u8) -> Option<Self> {
1684        if value <= MAX {
1685            Some(Self(value))
1686        } else {
1687            None
1688        }
1689    }
1690
1691    /// Returns the wrapped [`u8`] value.
1692    pub fn value(&self) -> u8 {
1693        self.0
1694    }
1695}
1696
1697impl<const MAX: u8> From<BoundedU8<MAX>> for u8 {
1698    fn from(value: BoundedU8<MAX>) -> Self {
1699        value.0
1700    }
1701}
1702
1703impl<const MAX: u8> From<BoundedU8<MAX>> for usize {
1704    fn from(value: BoundedU8<MAX>) -> Self {
1705        usize::from(value.0)
1706    }
1707}
1708
1709/// A small query language for filtering notes belonging to an account.
1710///
1711/// A filter described using this language is applied to notes individually. It is primarily
1712/// intended for retrieval of account metadata in service of making determinations for how to
1713/// allocate change notes, and is not currently intended for use in broader note selection
1714/// contexts.
1715#[derive(Clone, Debug, PartialEq, Eq)]
1716pub enum NoteFilter {
1717    /// Selects notes having value strictly greater than the provided value.
1718    ExceedsMinValue(Zatoshis),
1719    /// Selects notes having value greater than or equal to approximately the n'th percentile of
1720    /// previously sent notes in the account, irrespective of pool. The wrapped value must be in
1721    /// the range `1..=99`. The value `n` is respected in a best-effort fashion; results are likely
1722    /// to be inaccurate if the account has not yet completed scanning or if insufficient send data
1723    /// is available to establish a distribution.
1724    // TODO: it might be worthwhile to add an optional parameter here that can be used to ignore
1725    // low-valued (test/memo-only) sends when constructing the distribution to be drawn from.
1726    ExceedsPriorSendPercentile(BoundedU8<99>),
1727    /// Selects notes having value greater than or equal to the specified percentage of the account
1728    /// balance across all shielded pools. The wrapped value must be in the range `1..=99`
1729    ExceedsBalancePercentage(BoundedU8<99>),
1730    /// A note will be selected if it satisfies both of the specified conditions.
1731    ///
1732    /// If it is not possible to evaluate one of the conditions (for example,
1733    /// [`NoteFilter::ExceedsPriorSendPercentile`] cannot be evaluated if no sends have been
1734    /// performed) then that condition will be ignored. If neither condition can be evaluated,
1735    /// then the entire condition cannot be evaluated.
1736    Combine(Box<NoteFilter>, Box<NoteFilter>),
1737    /// A note will be selected if it satisfies the first condition; if it is not possible to
1738    /// evaluate that condition (for example, [`NoteFilter::ExceedsPriorSendPercentile`] cannot
1739    /// be evaluated if no sends have been performed) then the second condition will be used for
1740    /// evaluation.
1741    Attempt {
1742        condition: Box<NoteFilter>,
1743        fallback: Box<NoteFilter>,
1744    },
1745}
1746
1747impl NoteFilter {
1748    /// Constructs a [`NoteFilter::Combine`] query node.
1749    pub fn combine(l: NoteFilter, r: NoteFilter) -> Self {
1750        Self::Combine(Box::new(l), Box::new(r))
1751    }
1752
1753    /// Constructs a [`NoteFilter::Attempt`] query node.
1754    pub fn attempt(condition: NoteFilter, fallback: NoteFilter) -> Self {
1755        Self::Attempt {
1756            condition: Box::new(condition),
1757            fallback: Box::new(fallback),
1758        }
1759    }
1760}
1761
1762/// Controls which transparent outputs are eligible for selection. This is an
1763/// input-selection control only; it does not encode any consensus rule.
1764#[cfg(feature = "transparent-inputs")]
1765#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1766pub enum CoinbaseFilter {
1767    /// Select all spendable transparent outputs.
1768    #[default]
1769    AllTransparentOutputs,
1770    /// Select only coinbase transparent outputs.
1771    ///
1772    /// Coinbase transactions are identified by having `tx_index == 0` within
1773    /// their containing block. Outputs for which the transaction index is
1774    /// unknown are conservatively treated as non-coinbase and will be excluded
1775    /// when this filter is active.
1776    CoinbaseOnly,
1777    /// Select only non-coinbase transparent outputs.
1778    ///
1779    /// Used for general (non-shielding) transfers, which may produce transparent
1780    /// change; coinbase funds must instead be shielded via
1781    /// [`propose_shielding_coinbase`](crate::data_api::wallet::propose_shielding_coinbase).
1782    /// Outputs whose transaction index is unknown are treated as non-coinbase
1783    /// and are included.
1784    NonCoinbaseOnly,
1785}
1786
1787/// A trait representing the capability to query a data store for unspent transaction outputs
1788/// belonging to a account.
1789#[cfg_attr(feature = "test-dependencies", delegatable_trait)]
1790pub trait InputSource {
1791    /// The type of errors produced by a wallet backend.
1792    type Error: Debug;
1793
1794    /// Backend-specific account identifier.
1795    ///
1796    /// An account identifier corresponds to at most a single unified spending key's worth of spend
1797    /// authority, such that both received notes and change spendable by that spending authority
1798    /// will be interpreted as belonging to that account. This might be a database identifier type
1799    /// or a UUID.
1800    type AccountId: Copy + Debug + Eq + Hash;
1801
1802    /// Backend-specific note identifier.
1803    ///
1804    /// For example, this might be a database identifier type or a UUID.
1805    type NoteRef: Copy + Debug + Eq + Ord;
1806
1807    /// Fetches a spendable note by indexing into a transaction's shielded outputs for the
1808    /// specified shielded protocol.
1809    ///
1810    /// Returns `Ok(None)` if the note is not known to belong to the wallet or if the note
1811    /// is not spendable as of the given height. Locked outputs are selected according to
1812    /// `lock_filter` (see [`LockFilter`]; a [`LockFilter::Policy`] carrying the default
1813    /// [`LockedInputPolicy::Exclude`] selects none).
1814    ///
1815    /// [`LockedInputPolicy::Exclude`]: crate::data_api::wallet::input_selection::LockedInputPolicy::Exclude
1816    fn get_spendable_note(
1817        &self,
1818        txid: &TxId,
1819        protocol: ShieldedPool,
1820        index: u32,
1821        target_height: TargetHeight,
1822        lock_filter: LockFilter<'_>,
1823    ) -> Result<Option<ReceivedNote<Self::NoteRef, Note>>, Self::Error>;
1824
1825    /// Returns whether an anchor is COMPUTABLE at `height` for spends from the given pool: whether
1826    /// this data source can produce the note commitment tree root, and witnesses to it, as of the
1827    /// end of that block.
1828    ///
1829    /// A height inside the wallet's scanned range need not qualify: tree states are only
1830    /// materialized at the heights the wallet chose to retain, and a wallet that scanned past
1831    /// NU6.3 activation before boundary checkpointing was repaired is permanently missing the
1832    /// anchor-retention boundaries whose blocks carried no shielded outputs. Such a hole cannot be
1833    /// backfilled from local state, so a caller deciding whether to anchor at a retained boundary
1834    /// should consult this before committing to it, and fall back rather than propose a
1835    /// transaction that cannot be built.
1836    fn anchor_computable(
1837        &self,
1838        protocol: ShieldedPool,
1839        height: BlockHeight,
1840    ) -> Result<bool, Self::Error>;
1841
1842    /// Returns a list of spendable notes sufficient to cover the specified target value, if
1843    /// possible. Only spendable notes corresponding to the specified shielded protocol will
1844    /// be included. Locked outputs are selected according to `lock_filter` (see [`LockFilter`];
1845    /// a [`LockFilter::Policy`] carrying the default `Exclude` selects none).
1846    #[allow(clippy::too_many_arguments)]
1847    fn select_spendable_notes(
1848        &self,
1849        account: Self::AccountId,
1850        target_value: TargetValue,
1851        sources: &[ShieldedPool],
1852        target_height: TargetHeight,
1853        confirmations_policy: ConfirmationsPolicy,
1854        exclude: &[Self::NoteRef],
1855        lock_filter: LockFilter<'_>,
1856    ) -> Result<ReceivedNotes<Self::NoteRef>, Self::Error>;
1857
1858    /// Returns notes for consolidation-aware selection from `source`.
1859    ///
1860    /// The `funding` part of the result contains notes selected toward `value`. The `additional`
1861    /// part contains at most `max_additional_notes` disjoint notes that may be added
1862    /// opportunistically. The input selector validates that the funding covers the target and
1863    /// that adding any optional notes stays within its bounded consolidation envelope while
1864    /// preserving the transaction's fee and observable shape.
1865    ///
1866    /// Implementations backed by a queryable store should minimize the funding-note count within
1867    /// lock-tier preference, and return the smallest eligible notes from the funding tier as
1868    /// additional candidates. The default implementation preserves the data source's ordinary
1869    /// funding behavior and returns no additional candidates.
1870    #[allow(clippy::too_many_arguments)]
1871    fn select_spendable_notes_for_consolidation(
1872        &self,
1873        account: Self::AccountId,
1874        value: Zatoshis,
1875        source: ShieldedPool,
1876        target_height: TargetHeight,
1877        confirmations_policy: ConfirmationsPolicy,
1878        exclude: &[Self::NoteRef],
1879        lock_filter: LockFilter<'_>,
1880        _max_additional_notes: usize,
1881    ) -> Result<ConsolidationNotes<Self::NoteRef>, Self::Error> {
1882        self.select_spendable_notes(
1883            account,
1884            TargetValue::AtLeast(value),
1885            &[source],
1886            target_height,
1887            confirmations_policy,
1888            exclude,
1889            lock_filter,
1890        )
1891        .map(|funding| ConsolidationNotes::from_parts(funding, ReceivedNotes::empty()))
1892    }
1893
1894    /// Returns the OLDEST single spendable note whose value alone is at least `value`, drawn
1895    /// from the first pool in `sources` (in the given preference order) that holds one. The
1896    /// returned collection contains at most one note; it is empty when no single eligible note
1897    /// covers the value.
1898    ///
1899    /// This is the selection primitive behind
1900    /// [`NoteSelection::PreferSingle`](crate::data_api::wallet::input_selection::NoteSelection):
1901    /// a ZIP 318 migration transfer spends exactly one note, so a canonical pool crossing must
1902    /// be funded from one.
1903    ///
1904    /// The default implementation is BEST-EFFORT: it reports a note only when
1905    /// [`Self::select_spendable_notes`] happens to surface one that covers the value on its
1906    /// own. An implementation backed by a queryable store should override it with a direct
1907    /// query, so that a covering note is found whenever one exists.
1908    #[allow(clippy::too_many_arguments)]
1909    fn select_single_spendable_note(
1910        &self,
1911        account: Self::AccountId,
1912        value: Zatoshis,
1913        sources: &[ShieldedPool],
1914        target_height: TargetHeight,
1915        confirmations_policy: ConfirmationsPolicy,
1916        exclude: &[Self::NoteRef],
1917        lock_filter: LockFilter<'_>,
1918    ) -> Result<ReceivedNotes<Self::NoteRef>, Self::Error> {
1919        self.select_spendable_notes(
1920            account,
1921            TargetValue::AtLeast(value),
1922            sources,
1923            target_height,
1924            confirmations_policy,
1925            exclude,
1926            lock_filter,
1927        )
1928        .map(|notes| notes.into_single_covering(value, sources))
1929    }
1930
1931    /// Returns the list of notes belonging to the wallet that are unspent as of the specified
1932    /// target height. Locked outputs are selected according to `lock_filter` (see [`LockFilter`];
1933    /// a [`LockFilter::Policy`] carrying the default `Exclude` selects none).
1934    fn select_unspent_notes(
1935        &self,
1936        account: Self::AccountId,
1937        sources: &[ShieldedPool],
1938        target_height: TargetHeight,
1939        exclude: &[Self::NoteRef],
1940        lock_filter: LockFilter<'_>,
1941    ) -> Result<ReceivedNotes<Self::NoteRef>, Self::Error>;
1942
1943    /// Returns metadata describing the structure of the wallet for the specified account.
1944    ///
1945    /// The returned metadata value must exclude:
1946    /// - notes that are not considered spendable as of the given `target_height`
1947    /// - unspent notes excluded by the provided selector;
1948    /// - unspent notes identified in the given `exclude` list.
1949    /// - locked notes not admitted by `lock_filter` (see [`LockFilter`]; a [`LockFilter::Policy`]
1950    ///   carrying the default `Exclude` admits none).
1951    ///
1952    /// Implementations of this method may limit the complexity of supported queries. Such
1953    /// limitations should be clearly documented for the implementing type.
1954    fn get_account_metadata(
1955        &self,
1956        account: Self::AccountId,
1957        selector: &NoteFilter,
1958        target_height: TargetHeight,
1959        exclude: &[Self::NoteRef],
1960        lock_filter: LockFilter<'_>,
1961    ) -> Result<AccountMeta, Self::Error>;
1962
1963    /// Fetches the transparent output corresponding to the provided `outpoint` if it is considered
1964    /// spendable as of the provided `target_height`.
1965    ///
1966    /// Returns `Ok(None)` if the UTXO is not known to belong to the wallet or would not be
1967    /// spendable in a transaction mined in the block at the target height.
1968    #[cfg(feature = "transparent-inputs")]
1969    fn get_unspent_transparent_output(
1970        &self,
1971        _outpoint: &OutPoint,
1972        _target_height: TargetHeight,
1973    ) -> Result<Option<WalletTransparentOutput<Self::AccountId>>, Self::Error> {
1974        unimplemented!(
1975            "InputSource::get_spendable_transparent_output must be overridden for wallets to use the `transparent-inputs` feature"
1976        )
1977    }
1978
1979    /// Returns the list of unspent transparent outputs received by this wallet at `address`
1980    /// such that, at height `target_height`:
1981    /// * the transaction that produced the output had or will have at least the required number of
1982    ///   confirmations according to the provided [`ConfirmationsPolicy`]; and
1983    /// * the output can potentially be spent in a transaction mined in a block at the given
1984    ///   `target_height` (also taking into consideration the coinbase maturity rule).
1985    ///
1986    /// The `output_filter` parameter controls which transparent outputs are eligible. When set
1987    /// to [`CoinbaseFilter::CoinbaseOnly`], only outputs from coinbase transactions
1988    /// should be returned.
1989    ///
1990    /// Any output that is potentially spent by an unmined transaction in the mempool should be
1991    /// excluded unless the spending transaction will be expired at `target_height`.
1992    /// Locked outputs are selected according to `lock_filter` (see [`LockFilter`]; a
1993    /// [`LockFilter::Policy`] carrying the default `Exclude` selects none).
1994    #[cfg(feature = "transparent-inputs")]
1995    fn get_spendable_transparent_outputs(
1996        &self,
1997        _address: &TransparentAddress,
1998        _target_height: TargetHeight,
1999        _confirmations_policy: ConfirmationsPolicy,
2000        _output_filter: CoinbaseFilter,
2001        _lock_filter: LockFilter<'_>,
2002    ) -> Result<Vec<WalletTransparentOutput<Self::AccountId>>, Self::Error> {
2003        unimplemented!(
2004            "InputSource::get_spendable_transparent_outputs must be overridden for wallets to use the `transparent-inputs` feature"
2005        )
2006    }
2007
2008    /// Returns the list of spendable transparent outputs received by this wallet at any of the
2009    /// given `addresses`, subject to the same spendability conditions as
2010    /// [`InputSource::get_spendable_transparent_outputs`].
2011    ///
2012    /// This is the batched equivalent of calling
2013    /// [`InputSource::get_spendable_transparent_outputs`] once per address. It exists so that data
2014    /// stores can satisfy a multi-address request with a single query rather than one query per
2015    /// address, which is prohibitively expensive for wallets that hold large numbers of transparent
2016    /// addresses (as occurs when shielding). The default implementation simply iterates over
2017    /// `addresses`; data stores should override it with a batched query where possible.
2018    ///
2019    /// Each returned output identifies its receiving address via
2020    /// [`WalletTransparentOutput::recipient_address`].
2021    #[cfg(feature = "transparent-inputs")]
2022    fn get_spendable_transparent_outputs_for_addresses(
2023        &self,
2024        addresses: &[TransparentAddress],
2025        target_height: TargetHeight,
2026        confirmations_policy: ConfirmationsPolicy,
2027        output_filter: CoinbaseFilter,
2028        lock_filter: LockFilter<'_>,
2029    ) -> Result<Vec<WalletTransparentOutput<Self::AccountId>>, Self::Error> {
2030        let mut outputs = Vec::new();
2031        for address in addresses {
2032            outputs.extend(self.get_spendable_transparent_outputs(
2033                address,
2034                target_height,
2035                confirmations_policy,
2036                output_filter,
2037                lock_filter,
2038            )?);
2039        }
2040        Ok(outputs)
2041    }
2042
2043    /// Returns the spendable transparent outputs received by `account` whose total post-fee
2044    /// value (sum of values minus the cumulative marginal fee cost of the gathered inputs
2045    /// themselves, per `fee_rule`) is at least `target_value`, or `max_inputs` outputs
2046    /// (whichever is reached first).
2047    ///
2048    /// The gather is intended to scale to wallets with large numbers of transparent addresses and
2049    /// UTXOs: it returns a value-bounded subset rather than every spendable output, so the
2050    /// selector does not need to materialize the wallet's full UTXO set. Data stores should
2051    /// implement this with a single query that orders eligible UTXOs by descending value and
2052    /// accumulates them, recomputing the cumulative fee via `fee_rule` at each step, stopping
2053    /// once the post-fee cumulative value meets the bound. This produces a tighter result than a
2054    /// static value bound, without requiring a separate round trip to correct an under-estimated
2055    /// headroom.
2056    ///
2057    /// `max_inputs` bounds the number of transparent inputs a single transaction may consume,
2058    /// independent of `target_value`: even a small requested value could otherwise require an
2059    /// unbounded number of inputs for a wallet holding a very large number of small (e.g. dust)
2060    /// UTXOs. When the cap is reached before the value target, the returned set's post-fee value
2061    /// may be less than `target_value`; the caller's input-selection loop is expected to surface
2062    /// this as an `InsufficientFunds` error, the same as for any other value shortfall.
2063    ///
2064    /// `fee_rule` is fixed to [`StandardFeeRule`] (rather than being generic over the caller's
2065    /// actual [`ChangeStrategy`]) so that implementations of this method do not need to be
2066    /// generic over an arbitrary fee rule type. This is a heuristic bound only: the transaction's
2067    /// real fee is still computed by the caller's actual change strategy, and if this gather's
2068    /// estimate turns out to be insufficient, the caller's input-selection loop will surface an
2069    /// `InsufficientFunds` error and can re-invoke this method with a corrected `target_value`.
2070    ///
2071    /// For `TargetValue::AllFunds`, no value bound is applied and the gather returns every
2072    /// eligible output up to `max_inputs`.
2073    ///
2074    /// When `address_allow_list` is `Some`, only outputs received at one of the listed
2075    /// transparent addresses are eligible; when `None`, outputs received at any of the
2076    /// account's transparent addresses are eligible. The restriction must be applied
2077    /// *within* the gather (not to its results), so that outputs excluded by the allow list
2078    /// do not consume the value bound.
2079    ///
2080    /// This is the value-bounded counterpart to [`InputSource::get_spendable_transparent_outputs`]
2081    /// and [`InputSource::get_spendable_transparent_outputs_for_addresses`], intended for use by
2082    /// general (non-shielding) input selection in `propose_transaction`.
2083    ///
2084    /// [`ChangeStrategy`]: crate::fees::ChangeStrategy
2085    #[cfg(feature = "transparent-inputs")]
2086    #[allow(clippy::too_many_arguments)]
2087    fn select_spendable_transparent_outputs(
2088        &self,
2089        account: Self::AccountId,
2090        target_height: TargetHeight,
2091        confirmations_policy: ConfirmationsPolicy,
2092        output_filter: CoinbaseFilter,
2093        address_allow_list: Option<&[TransparentAddress]>,
2094        target_value: TargetValue,
2095        max_inputs: usize,
2096        fee_rule: &StandardFeeRule,
2097        lock_filter: LockFilter<'_>,
2098    ) -> Result<Vec<WalletTransparentOutput<Self::AccountId>>, Self::Error> {
2099        let _ = (
2100            account,
2101            target_height,
2102            confirmations_policy,
2103            output_filter,
2104            address_allow_list,
2105            target_value,
2106            max_inputs,
2107            fee_rule,
2108            lock_filter,
2109        );
2110        unimplemented!(
2111            "InputSource::select_spendable_transparent_outputs must be overridden for \
2112             wallets to use the value-bounded transparent input gather in propose_transaction"
2113        )
2114    }
2115}
2116
2117/// Read-only operations required for light wallet functions.
2118///
2119/// This trait defines the read-only portion of the storage interface atop which
2120/// higher-level wallet operations are implemented. It serves to allow wallet functions to
2121/// be abstracted away from any particular data storage substrate.
2122#[cfg_attr(feature = "test-dependencies", delegatable_trait)]
2123pub trait WalletRead {
2124    /// The type of errors that may be generated when querying a wallet data store.
2125    type Error: Debug;
2126
2127    /// The type of the account identifier.
2128    ///
2129    /// An account identifier corresponds to at most a single unified spending key's worth of spend
2130    /// authority, such that both received notes and change spendable by that spending authority
2131    /// will be interpreted as belonging to that account.
2132    type AccountId: Copy + Debug + Eq + Hash;
2133
2134    /// The concrete account type used by this wallet backend.
2135    type Account: Account<AccountId = Self::AccountId>;
2136
2137    /// Returns a vector with the IDs of all accounts known to this wallet.
2138    fn get_account_ids(&self) -> Result<Vec<Self::AccountId>, Self::Error>;
2139
2140    /// Returns the account corresponding to the given ID, if any.
2141    fn get_account(
2142        &self,
2143        account_id: Self::AccountId,
2144    ) -> Result<Option<Self::Account>, Self::Error>;
2145
2146    /// Returns the account corresponding to a given [`SeedFingerprint`] and
2147    /// [`zip32::AccountId`], if any.
2148    fn get_derived_account(
2149        &self,
2150        derivation: &Zip32Derivation,
2151    ) -> Result<Option<Self::Account>, Self::Error>;
2152
2153    /// Verifies that the given seed corresponds to the viewing key for the specified account.
2154    ///
2155    /// Returns:
2156    /// - `Ok(true)` if the viewing key for the specified account can be derived from the
2157    ///   provided seed.
2158    /// - `Ok(false)` if the derived viewing key does not match, or the specified account is not
2159    ///   present in the database.
2160    /// - `Err(_)` if a Unified Spending Key cannot be derived from the seed for the
2161    ///   specified account or the account has no known ZIP-32 derivation.
2162    fn validate_seed(
2163        &self,
2164        account_id: Self::AccountId,
2165        seed: &SecretVec<u8>,
2166    ) -> Result<bool, Self::Error>;
2167
2168    /// Checks whether the given seed is relevant to any of the derived accounts (where
2169    /// [`Account::source`] is [`AccountSource::Derived`]) in the wallet.
2170    ///
2171    /// This API does not check whether the seed is relevant to any imported account,
2172    /// because that would require brute-forcing the ZIP 32 account index space.
2173    fn seed_relevance_to_derived_accounts(
2174        &self,
2175        seed: &SecretVec<u8>,
2176    ) -> Result<SeedRelevance<Self::AccountId>, Self::Error>;
2177
2178    /// Returns the account corresponding to a given [`UnifiedFullViewingKey`], if any.
2179    fn get_account_for_ufvk(
2180        &self,
2181        ufvk: &UnifiedFullViewingKey,
2182    ) -> Result<Option<Self::Account>, Self::Error>;
2183
2184    /// Returns information about every address tracked for this account.
2185    fn list_addresses(&self, account: Self::AccountId) -> Result<Vec<AddressInfo>, Self::Error>;
2186
2187    /// Returns the wallet account that controls the given address, if any.
2188    ///
2189    /// Backends that can answer this query from an indexed lookup should implement it
2190    /// directly. Backends without such an index can delegate to
2191    /// [`defaults::find_account_for_address`], which implements the semantics described below
2192    /// using [`UnifiedIncomingViewingKey::decrypt_diversifiers`] and a linear scan over
2193    /// [`Self::get_account_ids`] / [`Self::list_addresses`].
2194    ///
2195    /// # Unified Addresses
2196    ///
2197    /// For a Unified Address each account's [`UnifiedIncomingViewingKey`] is asked, via
2198    /// [`UnifiedIncomingViewingKey::decrypt_diversifiers`], whether it could have derived any
2199    /// shielded receiver of the UA. An account matches if at least one shielded receiver is
2200    /// attributable to it — including receivers that have never been previously exposed by
2201    /// the wallet. If the shielded receivers of the UA are attributable to more than one
2202    /// account, this is treated as an inconsistent ("frankenstein") address and
2203    /// [`FindAccountForAddressError::UnifiedAddressConflict`] is returned rather than
2204    /// arbitrarily selecting one account.
2205    ///
2206    /// Backends are permitted to additionally resolve UAs by exact match against their
2207    /// tracked-address index before (or instead of) running the UIVK-algebra step, provided
2208    /// the exact-match result is consistent with at least one account identified by the
2209    /// algebraic step.
2210    ///
2211    /// # Non-Unified Addresses
2212    ///
2213    /// Backends should resolve bare shielded addresses (Sapling) via the same UIVK-algebraic
2214    /// path where feasible, so that an address derivable from an account's UIVK is resolved
2215    /// whether or not it has been previously exposed. Backends that do not can treat a bare
2216    /// shielded address as an exact-match lookup over their tracked-address index.
2217    ///
2218    /// Transparent and TEX addresses are resolved by exact match against tracked addresses
2219    /// only, since a diversifier index cannot be recovered from a transparent receiver alone.
2220    ///
2221    /// # Returns
2222    ///
2223    /// - `Ok(Some(account_id))` if the address is controlled by a single account known to
2224    ///   this wallet.
2225    /// - `Ok(None)` if no receiver of the address is recognized as belonging to any account.
2226    /// - `Err(FindAccountForAddressError::Backend(_))` if the lookup fails due to a
2227    ///   backend error.
2228    /// - `Err(FindAccountForAddressError::UnifiedAddressConflict)` if the provided address
2229    ///   is a Unified Address whose receiver components map to different accounts.
2230    ///
2231    /// [`UnifiedIncomingViewingKey`]: zcash_keys::keys::UnifiedIncomingViewingKey
2232    /// [`UnifiedIncomingViewingKey::decrypt_diversifiers`]: zcash_keys::keys::UnifiedIncomingViewingKey::decrypt_diversifiers
2233    /// [`FindAccountForAddressError::UnifiedAddressConflict`]: error::FindAccountForAddressError::UnifiedAddressConflict
2234    fn find_account_for_address<P: consensus::Parameters>(
2235        &self,
2236        params: &P,
2237        address: &zcash_keys::address::Address,
2238    ) -> Result<Option<Self::AccountId>, error::FindAccountForAddressError<Self::Error>>;
2239
2240    /// Returns the most recently generated unified address for the specified account that conforms
2241    /// to the specified address filter, if the account identifier specified refers to a valid
2242    /// account for this wallet.
2243    ///
2244    /// This will return `Ok(None)` if no previously generated address conforms to the specified
2245    /// request.
2246    fn get_last_generated_address_matching(
2247        &self,
2248        account: Self::AccountId,
2249        address_filter: UnifiedAddressRequest,
2250    ) -> Result<Option<UnifiedAddress>, Self::Error>;
2251
2252    /// Returns the birthday height for the given account, or an error if the account is not known
2253    /// to the wallet.
2254    fn get_account_birthday(&self, account: Self::AccountId) -> Result<BlockHeight, Self::Error>;
2255
2256    /// Returns the birthday height for the wallet.
2257    ///
2258    /// This returns the earliest birthday height among accounts maintained by this wallet,
2259    /// or `Ok(None)` if the wallet has no initialized accounts.
2260    fn get_wallet_birthday(&self) -> Result<Option<BlockHeight>, Self::Error>;
2261
2262    /// Returns the height at which the wallet as a whole will have exited recovery mode.
2263    ///
2264    /// This returns the latest `recover_until` height among accounts maintained by this
2265    /// wallet (see [`AccountBirthday::recover_until`]), or `Ok(None)` if no account has a
2266    /// recovery horizon set (for example, in a wallet whose accounts were all created at
2267    /// the chain tip rather than restored from backup). Heights below the returned value,
2268    /// exclusive, are in scope for wallet recovery for at least one account.
2269    fn get_wallet_recover_until(&self) -> Result<Option<BlockHeight>, Self::Error>;
2270
2271    /// Returns a [`WalletSummary`] that represents the sync status and the wallet balances as of
2272    /// the chain tip given the specified confirmation policy for all accounts known to the wallet,
2273    /// or `Ok(None)` if the wallet has no summary data available.
2274    fn get_wallet_summary(
2275        &self,
2276        confirmations_policy: ConfirmationsPolicy,
2277    ) -> Result<Option<WalletSummary<Self::AccountId>>, Self::Error>;
2278
2279    /// Returns the height of the chain as known to the wallet as of the most recent call to
2280    /// [`WalletWrite::update_chain_tip`].
2281    ///
2282    /// This will return `Ok(None)` if the height of the current consensus chain tip is unknown.
2283    fn chain_height(&self) -> Result<Option<BlockHeight>, Self::Error>;
2284
2285    /// Returns the interval on which this wallet retains note commitment tree checkpoints as
2286    /// durable anchors.
2287    ///
2288    /// A ZIP 318 pool migration anchors each of its pool-crossing transfers to a boundary of this
2289    /// interval, and proves the transfer long after that boundary has passed; the proof can only be
2290    /// constructed if the wallet kept the boundary's checkpoint. Reading the grid back off the
2291    /// wallet that maintains it — rather than configuring the migration separately — is what
2292    /// guarantees the two agree.
2293    ///
2294    /// The default implementation returns [`AnchorRetentionInterval::ZIP_318`], which matches the
2295    /// retention a backend performs if it does not configure the interval. A backend that DOES make
2296    /// retention configurable must override this to report the interval it actually retains, or
2297    /// migrations over it will draw anchors it has pruned.
2298    ///
2299    /// [`AnchorRetentionInterval::ZIP_318`]: anchor_retention::AnchorRetentionInterval::ZIP_318
2300    fn anchor_retention_interval(&self) -> anchor_retention::AnchorRetentionInterval {
2301        anchor_retention::AnchorRetentionInterval::ZIP_318
2302    }
2303
2304    /// Returns the ZIP 318 pool-migration parameters in force for this wallet: the specified
2305    /// values, with the anchor bucket grid taken from [`Self::anchor_retention_interval`].
2306    ///
2307    /// Every decision that depends on the grid must consult this rather than the network defaults,
2308    /// so that a wallet retaining a non-standard interval is treated consistently: bucketing an
2309    /// anchor and judging the resulting transaction a canonical crossing are the same question
2310    /// asked twice, and they must be asked of the same grid. Overriding
2311    /// [`Self::anchor_retention_interval`] is sufficient; this composes it.
2312    fn pool_migration_params(&self) -> anchor_retention::PoolMigrationParams {
2313        anchor_retention::PoolMigrationParams::new(self.anchor_retention_interval())
2314    }
2315
2316    /// Returns the block hash for the block at the given height, if the
2317    /// associated block data is available. Returns `Ok(None)` if the hash
2318    /// is not found in the database.
2319    fn get_block_hash(&self, block_height: BlockHeight) -> Result<Option<BlockHash>, Self::Error>;
2320
2321    /// Returns the available block metadata for the block at the specified height, if any.
2322    fn block_metadata(&self, height: BlockHeight) -> Result<Option<BlockMetadata>, Self::Error>;
2323
2324    /// Returns the metadata for the block at the height to which the wallet has been fully
2325    /// scanned.
2326    ///
2327    /// This is the height for which the wallet has fully trial-decrypted this and all preceding
2328    /// blocks above the wallet's birthday height. Along with this height, this method returns
2329    /// metadata describing the state of the wallet's note commitment trees as of the end of that
2330    /// block.
2331    fn block_fully_scanned(&self) -> Result<Option<BlockMetadata>, Self::Error>;
2332
2333    /// Returns the block height and hash for the block at the maximum scanned block height.
2334    ///
2335    /// This will return `Ok(None)` if no blocks have been scanned.
2336    fn get_max_height_hash(&self) -> Result<Option<(BlockHeight, BlockHash)>, Self::Error>;
2337
2338    /// Returns block metadata for the maximum height that the wallet has scanned.
2339    ///
2340    /// If the wallet is fully synced, this will be equivalent to `block_fully_scanned`;
2341    /// otherwise the maximal scanned height is likely to be greater than the fully scanned height
2342    /// due to the fact that out-of-order scanning can leave gaps.
2343    fn block_max_scanned(&self) -> Result<Option<BlockMetadata>, Self::Error>;
2344
2345    /// Returns a vector of suggested scan ranges based upon the current wallet state.
2346    ///
2347    /// This method should only be used in cases where the [`CompactBlock`] data that will be made
2348    /// available to `scan_cached_blocks` for the requested block ranges includes note commitment
2349    /// tree size information for each block; or else the scan is likely to fail if notes belonging
2350    /// to the wallet are detected.
2351    ///
2352    /// The returned range(s) may include block heights beyond the current chain tip. Ranges are
2353    /// returned in order of descending priority, and higher-priority ranges should always be
2354    /// scanned before lower-priority ranges; in particular, ranges with [`ScanPriority::Verify`]
2355    /// priority must always be scanned first in order to avoid blockchain continuity errors in the
2356    /// case of a reorg.
2357    ///
2358    /// Ranges above the activation height of the most recently activated shielded pool are
2359    /// returned ahead of older history (see [`ScanPriority::LatestPoolActivation`]), so a wallet
2360    /// recovering from a birthday below that height surfaces its balance before the
2361    /// pre-activation backfill completes.
2362    ///
2363    /// [`CompactBlock`]: crate::proto::compact_formats::CompactBlock
2364    /// [`ScanPriority::Verify`]: crate::data_api::scanning::ScanPriority
2365    fn suggest_scan_ranges(&self) -> Result<Vec<ScanRange>, Self::Error>;
2366
2367    /// Returns the default target height (for the block in which a new
2368    /// transaction would be mined) and anchor height (to use for a new
2369    /// transaction), given the range of block heights that the backend
2370    /// knows about.
2371    ///
2372    /// This will return `Ok(None)` if no block data is present in the database.
2373    fn get_target_and_anchor_heights(
2374        &self,
2375        min_confirmations: NonZeroU32,
2376    ) -> Result<Option<(TargetHeight, BlockHeight)>, Self::Error>;
2377
2378    /// Returns the block height in which the specified transaction was mined, or `Ok(None)` if the
2379    /// transaction is not known to the wallet or not in the main chain.
2380    fn get_tx_height(&self, txid: TxId) -> Result<Option<BlockHeight>, Self::Error>;
2381
2382    /// Returns all unified full viewing keys known to this wallet.
2383    fn get_unified_full_viewing_keys(
2384        &self,
2385    ) -> Result<HashMap<Self::AccountId, UnifiedFullViewingKey>, Self::Error>;
2386
2387    /// Returns the memo for a note.
2388    ///
2389    /// Returns `Ok(None)` if the note is known to the wallet but memo data has not yet been
2390    /// populated for that note, or if the note identifier does not correspond to a note
2391    /// that is known to the wallet.
2392    fn get_memo(&self, note_id: NoteId) -> Result<Option<Memo>, Self::Error>;
2393
2394    /// Returns the transaction with the given txid, if known to the wallet.
2395    ///
2396    /// Returns `None` if the txid is not known to the wallet or if the raw transaction data is not
2397    /// available.
2398    fn get_transaction(&self, txid: TxId) -> Result<Option<Transaction>, Self::Error>;
2399
2400    /// Returns the nullifiers for Sapling notes that the wallet is tracking, along with their
2401    /// associated account IDs, that are either unspent or have not yet been confirmed as spent (in
2402    /// that a spending transaction known to the wallet has not yet been included in a block).
2403    fn get_sapling_nullifiers(
2404        &self,
2405        query: NullifierQuery,
2406    ) -> Result<Vec<(Self::AccountId, sapling::Nullifier)>, Self::Error>;
2407
2408    /// Returns the nullifiers for Orchard notes that the wallet is tracking, along with their
2409    /// associated account IDs, that are either unspent or have not yet been confirmed as spent (in
2410    /// that a spending transaction known to the wallet has not yet been included in a block).
2411    #[cfg(feature = "orchard")]
2412    fn get_orchard_nullifiers(
2413        &self,
2414        _query: NullifierQuery,
2415    ) -> Result<Vec<(Self::AccountId, orchard::note::Nullifier)>, Self::Error> {
2416        unimplemented!(
2417            "WalletRead::get_orchard_nullifiers must be overridden for wallets to use the `orchard` feature"
2418        )
2419    }
2420
2421    /// Returns the nullifiers for Ironwood notes that the wallet is tracking, along with their
2422    /// associated account IDs, that are either unspent or have not yet been confirmed as spent.
2423    /// Ironwood nullifiers are Orchard-shaped but are tracked as a separate pool.
2424    ///
2425    /// This is a required method (like [`WalletRead::get_sapling_nullifiers`]) rather than
2426    /// defaulting to a panic: it is called on the scan path, so a backend that does not override it
2427    /// would abort the process on the first scan. Requiring it surfaces the omission at compile
2428    /// time instead.
2429    #[cfg(feature = "orchard")]
2430    fn get_ironwood_nullifiers(
2431        &self,
2432        query: NullifierQuery,
2433    ) -> Result<Vec<(Self::AccountId, orchard::note::Nullifier)>, Self::Error>;
2434
2435    /// Returns the set of non-ephemeral transparent receivers associated with the given
2436    /// account controlled by this wallet.
2437    ///
2438    /// The set contains all non-ephemeral transparent receivers that are known to have
2439    /// been derived under this account. Wallets should scan the chain for UTXOs sent to
2440    /// these receivers.
2441    ///
2442    /// # Parameters
2443    /// - `account`: The identifier for the account from which transparent receivers should be
2444    ///   returned.
2445    /// - `include_change`: A flag indicating whether transparent change addresses should be
2446    ///   returned.
2447    /// - `include_standalone`: A flag indicating whether imported standalone addresses associated
2448    ///   with the account should be returned. The value of this flag is ignored unless the
2449    ///   `transparent-key-import` feature is enabled.
2450    ///
2451    /// Use [`Self::get_ephemeral_transparent_receivers`] to obtain the ephemeral transparent
2452    /// receivers.
2453    #[cfg(feature = "transparent-inputs")]
2454    fn get_transparent_receivers(
2455        &self,
2456        _account: Self::AccountId,
2457        _include_change: bool,
2458        _include_standalone: bool,
2459    ) -> Result<HashMap<TransparentAddress, TransparentAddressMetadata>, Self::Error> {
2460        unimplemented!(
2461            "WalletRead::get_transparent_receivers must be overridden for wallets to use the `transparent-inputs` feature"
2462        )
2463    }
2464
2465    /// Returns the set of previously-exposed ephemeral transparent receivers generated by the given
2466    /// account controlled by this wallet.
2467    ///
2468    /// The set contains all ephemeral transparent receivers that are known to have been derived
2469    /// under this account within `exposure_depth` blocks of the chain tip. Wallets may scan the
2470    /// chain for UTXOs sent to these receivers, but should do so in a fashion that does not reveal
2471    /// that they are controlled by the same wallet. If the [`next_check_time`] field is set for a
2472    /// returned [`TransparentAddressMetadata`], the wallet application should defer any query to
2473    /// any public light wallet server for this address until [`next_check_time`] has passed; when
2474    /// using a light wallet server that is trusted for privacy, this delay may be omitted.
2475    ///
2476    /// # Parameters
2477    /// - `account`: The identifier for the account from which transparent receivers should be
2478    ///   returned.
2479    /// - `exposure_depth`: Implementations of this method should return only addresses exposed at
2480    ///   heights greater than `chain_tip_height - exposure_depth`.
2481    /// - `exclude_used`: When set to `true`, do not return addresses that are known to have
2482    ///   already received funds in a transaction.
2483    ///
2484    /// [`next_check_time`]: TransparentAddressMetadata::next_check_time
2485    #[cfg(feature = "transparent-inputs")]
2486    fn get_ephemeral_transparent_receivers(
2487        &self,
2488        _account: Self::AccountId,
2489        _exposure_depth: u32,
2490        _exclude_used: bool,
2491    ) -> Result<HashMap<TransparentAddress, TransparentAddressMetadata>, Self::Error> {
2492        unimplemented!(
2493            "WalletRead::get_ephemeral_transparent_receivers must be overridden for wallets to use the `transparent-inputs` feature"
2494        )
2495    }
2496
2497    /// Returns a mapping from each transparent receiver associated with the specified account
2498    /// to the key scope for that address and the balance of funds given the specified target
2499    /// height and confirmations policy.
2500    #[cfg(feature = "transparent-inputs")]
2501    fn get_transparent_balances(
2502        &self,
2503        _account: Self::AccountId,
2504        _target_height: TargetHeight,
2505        _confirmations_policy: ConfirmationsPolicy,
2506    ) -> Result<TransparentBalances, Self::Error> {
2507        unimplemented!(
2508            "WalletRead::get_transparent_balances must be overridden for wallets to use the `transparent-inputs` feature"
2509        )
2510    }
2511
2512    /// Returns the metadata associated with a given transparent receiver in an account
2513    /// controlled by this wallet, if available.
2514    ///
2515    /// This is equivalent to (but may be implemented more efficiently than):
2516    /// ```compile_fail
2517    /// Ok(
2518    ///     if let Some(result) = self.get_transparent_receivers(account, true)?.get(address) {
2519    ///         Some(result.clone())
2520    ///     } else {
2521    ///         self.get_ephemeral_transparent_receivers(account, u32::MAX, false)?
2522    ///             .get(address)
2523    ///             .cloned()
2524    ///     },
2525    /// )
2526    /// ```
2527    ///
2528    /// Returns `Ok(None)` if the address is not recognized, or we do not have metadata for it.
2529    /// Returns `Ok(Some(metadata))` if we have the metadata.
2530    #[cfg(feature = "transparent-inputs")]
2531    fn get_transparent_address_metadata(
2532        &self,
2533        _account: Self::AccountId,
2534        _address: &TransparentAddress,
2535    ) -> Result<Option<TransparentAddressMetadata>, Self::Error> {
2536        unimplemented!(
2537            "WalletRead::get_transparent_address_metadata must be overridden for wallets to use the `transparent-inputs` feature"
2538        )
2539    }
2540
2541    /// Returns the maximum block height at which a transparent output belonging to the wallet has
2542    /// been observed.
2543    ///
2544    /// We must start looking for UTXOs for addresses within the current gap limit as of the block
2545    /// height at which they might have first been revealed. This would have occurred when the gap
2546    /// advanced as a consequence of a transaction being mined. The address at the start of the current
2547    /// gap was potentially first revealed after the address at index `gap_start - (gap_limit + 1)`
2548    /// received an output in a mined transaction; therefore, we take that height to be where we
2549    /// should start searching for UTXOs.
2550    #[cfg(feature = "transparent-inputs")]
2551    fn utxo_query_height(&self, _account: Self::AccountId) -> Result<BlockHeight, Self::Error> {
2552        unimplemented!(
2553            "WalletRead::utxo_query_height must be overridden for wallets to use the `transparent-inputs` feature"
2554        )
2555    }
2556
2557    /// Returns a vector of [`TransactionDataRequest`] values that describe information needed by
2558    /// the wallet to complete its view of transaction history.
2559    ///
2560    /// Requests for the same transaction data may be returned repeatedly by successive data
2561    /// requests. The caller of this method should consider the latest set of requests returned
2562    /// by this method to be authoritative and to subsume that returned by previous calls.
2563    ///
2564    /// Callers should poll this method on a regular interval, not as part of ordinary chain
2565    /// scanning, which already produces requests for transaction data enhancement. Note that
2566    /// responding to a set of transaction data requests may result in the creation of new
2567    /// transaction data requests, such as when it is necessary to fill in purely-transparent
2568    /// transaction history by walking the chain backwards via transparent inputs.
2569    fn transaction_data_requests(&self) -> Result<Vec<TransactionDataRequest>, Self::Error>;
2570
2571    /// Returns a vector of [`ReceivedTransactionOutput`] values describing the outputs of the
2572    /// specified transaction that were received by the wallet. The number of confirmations until
2573    /// each received output will be considered spendable is determined based upon the specified
2574    /// target height and confirmations policy.
2575    fn get_received_outputs(
2576        &self,
2577        txid: TxId,
2578        target_height: TargetHeight,
2579        confirmations_policy: ConfirmationsPolicy,
2580    ) -> Result<Vec<ReceivedTransactionOutput>, Self::Error>;
2581}
2582
2583/// Read-only operations required for testing light wallet functions.
2584///
2585/// These methods expose internal details or unstable interfaces, primarily to enable use
2586/// of the [`testing`] framework. They should not be used in production software.
2587#[cfg(any(test, feature = "test-dependencies"))]
2588#[cfg_attr(feature = "test-dependencies", delegatable_trait)]
2589pub trait WalletTest: InputSource + WalletRead {
2590    /// Returns a vector of transaction summaries.
2591    ///
2592    /// Currently test-only, as production use could return a very large number of results; either
2593    /// pagination or a streaming design will be necessary to stabilize this feature for production
2594    /// use.
2595    fn get_tx_history(
2596        &self,
2597    ) -> Result<
2598        Vec<testing::TransactionSummary<<Self as WalletRead>::AccountId>>,
2599        <Self as WalletRead>::Error,
2600    >;
2601
2602    /// Returns the note IDs for shielded notes sent by the wallet in a particular
2603    /// transaction.
2604    fn get_sent_note_ids(
2605        &self,
2606        _txid: &TxId,
2607        _protocol: ShieldedPool,
2608    ) -> Result<Vec<NoteId>, <Self as WalletRead>::Error>;
2609
2610    /// Returns the outputs for a transaction sent by the wallet.
2611    #[allow(clippy::type_complexity)]
2612    fn get_sent_outputs(
2613        &self,
2614        txid: &TxId,
2615    ) -> Result<Vec<OutputOfSentTx>, <Self as WalletRead>::Error>;
2616
2617    #[allow(clippy::type_complexity)]
2618    fn get_checkpoint_history(
2619        &self,
2620        protocol: &ShieldedPool,
2621    ) -> Result<
2622        Vec<(BlockHeight, Option<incrementalmerkletree::Position>)>,
2623        <Self as WalletRead>::Error,
2624    >;
2625
2626    /// Fetches the transparent output corresponding to the provided `outpoint`.
2627    /// Allows selecting unspendable outputs for testing purposes.
2628    ///
2629    /// # Parameters
2630    /// - `outpoint`: The identifier for the output to be retrieved.
2631    /// - `spendable_as_of`: The target height of a transaction under construction that will spend the
2632    ///   returned output. If this is `None`, no spendability checks are performed.
2633    ///
2634    /// Returns `Ok(None)` if the UTXO is not known to belong to the wallet or if `spendable_as_of`
2635    /// is set and the output is available to be spent by the wallet in a transaction that is
2636    /// intended to be mined at the target height.
2637    #[cfg(feature = "transparent-inputs")]
2638    fn get_transparent_output(
2639        &self,
2640        _outpoint: &OutPoint,
2641        _spendable_as_of: Option<TargetHeight>,
2642    ) -> Result<
2643        Option<WalletTransparentOutput<<Self as WalletRead>::AccountId>>,
2644        <Self as InputSource>::Error,
2645    > {
2646        unimplemented!(
2647            "WalletTest::get_transparent_output must be overridden for wallets to use the `transparent-inputs` feature"
2648        )
2649    }
2650
2651    /// Returns all the notes that have been received by the wallet.
2652    fn get_notes(
2653        &self,
2654        protocol: ShieldedPool,
2655    ) -> Result<Vec<ReceivedNote<Self::NoteRef, Note>>, <Self as InputSource>::Error>;
2656
2657    /// Returns a vector of ephemeral transparent addresses associated with the given
2658    /// account controlled by this wallet, along with their metadata. The result includes
2659    /// reserved addresses, and addresses for the backend's configured gap limit worth
2660    /// of additional indices (capped to the maximum index).
2661    ///
2662    /// If `index_range` is some `Range`, it limits the result to addresses with indices
2663    /// in that range. An `index_range` of `None` is defined to be equivalent to
2664    /// `0..(1u32 << 31)`.
2665    ///
2666    /// Wallets should scan the chain for UTXOs sent to these ephemeral transparent
2667    /// receivers, but do not need to do so regularly. Under expected usage, outputs
2668    /// would only be detected with these receivers in the following situations:
2669    ///
2670    /// - This wallet created a payment to a ZIP 320 (TEX) address, but the second
2671    ///   transaction (that spent the output sent to the ephemeral address) did not get
2672    ///   mined before it expired.
2673    ///   - In this case the output will already be known to the wallet (because it
2674    ///     stores the transactions that it creates).
2675    ///
2676    /// - Another wallet app using the same seed phrase created a payment to a ZIP 320
2677    ///   address, and this wallet queried for the ephemeral UTXOs after the first
2678    ///   transaction was mined but before the second transaction was mined.
2679    ///   - In this case, the output should not be considered unspent until the expiry
2680    ///     height of the transaction it was received in has passed. Wallets creating
2681    ///     payments to TEX addresses generally set the same expiry height for the first
2682    ///     and second transactions, meaning that this wallet does not need to observe
2683    ///     the second transaction to determine when it would have expired.
2684    ///
2685    /// - A TEX address recipient decided to return funds that the wallet had sent to
2686    ///   them.
2687    ///
2688    /// In all cases, the wallet should re-shield the unspent outputs, in a separate
2689    /// transaction per ephemeral address, before re-spending the funds.
2690    #[cfg(feature = "transparent-inputs")]
2691    fn get_known_ephemeral_addresses(
2692        &self,
2693        _account: <Self as WalletRead>::AccountId,
2694        _index_range: Option<Range<NonHardenedChildIndex>>,
2695    ) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, <Self as WalletRead>::Error>
2696    {
2697        unimplemented!(
2698            "WalletRead::get_known_ephemeral_addresses must be overridden for wallets to use the `transparent-inputs` feature"
2699        )
2700    }
2701
2702    /// If a given ephemeral address might have been reserved, i.e. would be included in
2703    /// the result of `get_known_ephemeral_addresses(account_id, None)` for any of the
2704    /// wallet's accounts, then return `Ok(Some(account_id))`. Otherwise return `Ok(None)`.
2705    ///
2706    /// This is equivalent to (but may be implemented more efficiently than):
2707    /// ```compile_fail
2708    /// for account_id in self.get_account_ids()? {
2709    ///     if self
2710    ///         .get_known_ephemeral_addresses(account_id, None)?
2711    ///         .into_iter()
2712    ///         .any(|(known_addr, _)| &known_addr == address)
2713    ///     {
2714    ///         return Ok(Some(account_id));
2715    ///     }
2716    /// }
2717    /// Ok(None)
2718    /// ```
2719    #[cfg(feature = "transparent-inputs")]
2720    fn find_account_for_ephemeral_address(
2721        &self,
2722        address: &TransparentAddress,
2723    ) -> Result<Option<<Self as WalletRead>::AccountId>, <Self as WalletRead>::Error> {
2724        for account_id in self.get_account_ids()? {
2725            if self
2726                .get_known_ephemeral_addresses(account_id, None)?
2727                .into_iter()
2728                .any(|(known_addr, _)| &known_addr == address)
2729            {
2730                return Ok(Some(account_id));
2731            }
2732        }
2733        Ok(None)
2734    }
2735
2736    /// Performs final checks at the conclusion of each test.
2737    ///
2738    /// This method allows wallet backend developers to perform any necessary consistency
2739    /// checks or cleanup. By default it does nothing.
2740    fn finally(&self) {}
2741}
2742
2743/// The output of a transaction sent by the wallet.
2744///
2745/// This type is opaque, and exists for use by tests defined in this crate.
2746#[cfg(any(test, feature = "test-dependencies"))]
2747#[allow(dead_code)]
2748#[derive(Clone, Debug)]
2749pub struct OutputOfSentTx {
2750    value: Zatoshis,
2751    external_recipient: Option<Address>,
2752    #[cfg(feature = "transparent-inputs")]
2753    ephemeral_address: Option<(Address, NonHardenedChildIndex)>,
2754}
2755
2756#[cfg(any(test, feature = "test-dependencies"))]
2757impl OutputOfSentTx {
2758    /// Constructs an output from its test-relevant parts.
2759    ///
2760    /// If the output is to an ephemeral address, `ephemeral_address` should contain the
2761    /// address along with the `address_index` it was derived from under the BIP 32 path
2762    /// `m/44'/<coin_type>'/<account>'/2/<address_index>`.
2763    pub fn from_parts(
2764        value: Zatoshis,
2765        external_recipient: Option<Address>,
2766        #[cfg(feature = "transparent-inputs")] ephemeral_address: Option<(
2767            Address,
2768            NonHardenedChildIndex,
2769        )>,
2770    ) -> Self {
2771        Self {
2772            value,
2773            external_recipient,
2774            #[cfg(feature = "transparent-inputs")]
2775            ephemeral_address,
2776        }
2777    }
2778
2779    /// Returns the value of the output.
2780    pub fn value(&self) -> Zatoshis {
2781        self.value
2782    }
2783
2784    /// Returns the recipient of the sent output.
2785    pub fn external_recipient(&self) -> Option<&Address> {
2786        self.external_recipient.as_ref()
2787    }
2788
2789    /// Returns the ephemeral address to which the output was sent, along with the non-hardened
2790    /// transparent child index at which that address was derived.
2791    #[cfg(feature = "transparent-inputs")]
2792    pub fn ephemeral_address(&self) -> Option<&(Address, NonHardenedChildIndex)> {
2793        self.ephemeral_address.as_ref()
2794    }
2795}
2796
2797/// The relevance of a seed to a given wallet.
2798///
2799/// This is the return type for [`WalletRead::seed_relevance_to_derived_accounts`].
2800#[derive(Clone, Debug, PartialEq, Eq)]
2801pub enum SeedRelevance<A: Copy> {
2802    /// The seed is relevant to at least one derived account within the wallet.
2803    Relevant { account_ids: NonEmpty<A> },
2804    /// The seed is not relevant to any of the derived accounts within the wallet.
2805    NotRelevant,
2806    /// The wallet contains no derived accounts.
2807    NoDerivedAccounts,
2808    /// The wallet contains no accounts.
2809    NoAccounts,
2810}
2811
2812/// Metadata describing the sizes of the zcash note commitment trees as of a particular block.
2813#[derive(Debug, Clone, Copy)]
2814pub struct BlockMetadata {
2815    block_height: BlockHeight,
2816    block_hash: BlockHash,
2817    sapling_tree_size: Option<u32>,
2818    #[cfg(feature = "orchard")]
2819    orchard_tree_size: Option<u32>,
2820    #[cfg(feature = "orchard")]
2821    ironwood_tree_size: Option<u32>,
2822}
2823
2824impl BlockMetadata {
2825    /// Constructs a new [`BlockMetadata`] value from its constituent parts.
2826    pub fn from_parts(
2827        block_height: BlockHeight,
2828        block_hash: BlockHash,
2829        sapling_tree_size: Option<u32>,
2830        #[cfg(feature = "orchard")] orchard_tree_size: Option<u32>,
2831        #[cfg(feature = "orchard")] ironwood_tree_size: Option<u32>,
2832    ) -> Self {
2833        Self {
2834            block_height,
2835            block_hash,
2836            sapling_tree_size,
2837            #[cfg(feature = "orchard")]
2838            orchard_tree_size,
2839            #[cfg(feature = "orchard")]
2840            ironwood_tree_size,
2841        }
2842    }
2843
2844    /// Returns the block height.
2845    pub fn block_height(&self) -> BlockHeight {
2846        self.block_height
2847    }
2848
2849    /// Returns the hash of the block
2850    pub fn block_hash(&self) -> BlockHash {
2851        self.block_hash
2852    }
2853
2854    /// Returns the size of the Sapling note commitment tree for the final treestate of the block
2855    /// that this [`BlockMetadata`] describes, if available.
2856    pub fn sapling_tree_size(&self) -> Option<u32> {
2857        self.sapling_tree_size
2858    }
2859
2860    /// Returns the size of the Orchard note commitment tree for the final treestate of the block
2861    /// that this [`BlockMetadata`] describes, if available.
2862    #[cfg(feature = "orchard")]
2863    pub fn orchard_tree_size(&self) -> Option<u32> {
2864        self.orchard_tree_size
2865    }
2866
2867    /// Returns the size of the Ironwood note commitment tree for the final treestate of the block
2868    /// that this [`BlockMetadata`] describes, if available.
2869    #[cfg(feature = "orchard")]
2870    pub fn ironwood_tree_size(&self) -> Option<u32> {
2871        self.ironwood_tree_size
2872    }
2873}
2874
2875/// The protocol-specific note commitment and nullifier data extracted from the per-transaction
2876/// shielded bundles in [`CompactBlock`], used by the wallet for note commitment tree maintenance
2877/// and spend detection.
2878///
2879/// [`CompactBlock`]: crate::proto::compact_formats::CompactBlock
2880pub struct ScannedBundles<NoteCommitment, NF> {
2881    final_tree_size: u32,
2882    commitments: Vec<(NoteCommitment, Retention<BlockHeight>)>,
2883    nullifier_map: Vec<(TxIndex, TxId, Vec<NF>)>,
2884}
2885
2886impl<NoteCommitment, NF> ScannedBundles<NoteCommitment, NF> {
2887    pub(crate) fn new(
2888        final_tree_size: u32,
2889        commitments: Vec<(NoteCommitment, Retention<BlockHeight>)>,
2890        nullifier_map: Vec<(TxIndex, TxId, Vec<NF>)>,
2891    ) -> Self {
2892        Self {
2893            final_tree_size,
2894            nullifier_map,
2895            commitments,
2896        }
2897    }
2898
2899    /// Returns the size of the note commitment tree as of the end of the scanned block.
2900    pub fn final_tree_size(&self) -> u32 {
2901        self.final_tree_size
2902    }
2903
2904    /// Returns the vector of nullifiers for each transaction in the block.
2905    ///
2906    /// The returned tuple is keyed by both transaction ID and the index of the transaction within
2907    /// the block, so that either the txid or the combination of the block hash available from
2908    /// [`ScannedBlock::block_hash`] and returned transaction index may be used to uniquely
2909    /// identify the transaction, depending upon the needs of the caller.
2910    pub fn nullifier_map(&self) -> &[(TxIndex, TxId, Vec<NF>)] {
2911        &self.nullifier_map
2912    }
2913
2914    /// Returns the ordered list of note commitments to be added to the note commitment
2915    /// tree.
2916    pub fn commitments(&self) -> &[(NoteCommitment, Retention<BlockHeight>)] {
2917        &self.commitments
2918    }
2919}
2920
2921/// A struct used to return the vectors of note commitments for a [`ScannedBlock`]
2922/// as owned values.
2923pub struct ScannedBlockCommitments {
2924    /// The ordered vector of note commitments for Sapling outputs of the block.
2925    pub sapling: Vec<(sapling::Node, Retention<BlockHeight>)>,
2926    /// The ordered vector of note commitments for Orchard outputs of the block.
2927    /// Present only when the `orchard` feature is enabled.
2928    #[cfg(feature = "orchard")]
2929    pub orchard: Vec<(orchard::tree::MerkleHashOrchard, Retention<BlockHeight>)>,
2930    /// The ordered vector of note commitments for Ironwood outputs of the block.
2931    /// Present only when the `orchard` feature is enabled.
2932    #[cfg(feature = "orchard")]
2933    pub ironwood: Vec<(orchard::tree::MerkleHashOrchard, Retention<BlockHeight>)>,
2934}
2935
2936/// The subset of information that is relevant to this wallet that has been
2937/// decrypted and extracted from a [`CompactBlock`].
2938///
2939/// [`CompactBlock`]: crate::proto::compact_formats::CompactBlock
2940pub struct ScannedBlock<AccountId> {
2941    block_height: BlockHeight,
2942    block_hash: BlockHash,
2943    block_time: u32,
2944    transactions: Vec<WalletTx<AccountId>>,
2945    sapling: ScannedBundles<sapling::Node, sapling::Nullifier>,
2946    #[cfg(feature = "orchard")]
2947    orchard: ScannedBundles<orchard::tree::MerkleHashOrchard, orchard::note::Nullifier>,
2948    #[cfg(feature = "orchard")]
2949    ironwood: ScannedBundles<orchard::tree::MerkleHashOrchard, orchard::note::Nullifier>,
2950}
2951
2952impl<AccountId> ScannedBlock<AccountId> {
2953    /// Constructs a new `ScannedBlock`
2954    pub(crate) fn from_parts(
2955        block_height: BlockHeight,
2956        block_hash: BlockHash,
2957        block_time: u32,
2958        transactions: Vec<WalletTx<AccountId>>,
2959        sapling: ScannedBundles<sapling::Node, sapling::Nullifier>,
2960        #[cfg(feature = "orchard")] orchard: ScannedBundles<
2961            orchard::tree::MerkleHashOrchard,
2962            orchard::note::Nullifier,
2963        >,
2964        #[cfg(feature = "orchard")] ironwood: ScannedBundles<
2965            orchard::tree::MerkleHashOrchard,
2966            orchard::note::Nullifier,
2967        >,
2968    ) -> Self {
2969        Self {
2970            block_height,
2971            block_hash,
2972            block_time,
2973            transactions,
2974            sapling,
2975            #[cfg(feature = "orchard")]
2976            orchard,
2977            #[cfg(feature = "orchard")]
2978            ironwood,
2979        }
2980    }
2981
2982    /// Returns the height of the block that was scanned.
2983    pub fn height(&self) -> BlockHeight {
2984        self.block_height
2985    }
2986
2987    /// Returns the block hash of the block that was scanned.
2988    pub fn block_hash(&self) -> BlockHash {
2989        self.block_hash
2990    }
2991
2992    /// Returns the block time of the block that was scanned, as a Unix timestamp in seconds.
2993    pub fn block_time(&self) -> u32 {
2994        self.block_time
2995    }
2996
2997    /// Returns the list of transactions from this block that are relevant to the wallet.
2998    pub fn transactions(&self) -> &[WalletTx<AccountId>] {
2999        &self.transactions
3000    }
3001
3002    /// Returns the Sapling note commitment tree and nullifier data for the block.
3003    pub fn sapling(&self) -> &ScannedBundles<sapling::Node, sapling::Nullifier> {
3004        &self.sapling
3005    }
3006
3007    /// Returns the Orchard note commitment tree and nullifier data for the block.
3008    #[cfg(feature = "orchard")]
3009    pub fn orchard(
3010        &self,
3011    ) -> &ScannedBundles<orchard::tree::MerkleHashOrchard, orchard::note::Nullifier> {
3012        &self.orchard
3013    }
3014
3015    /// Returns the Ironwood note commitment tree and nullifier data for the block.
3016    #[cfg(feature = "orchard")]
3017    pub fn ironwood(
3018        &self,
3019    ) -> &ScannedBundles<orchard::tree::MerkleHashOrchard, orchard::note::Nullifier> {
3020        &self.ironwood
3021    }
3022
3023    /// Consumes `self` and returns the lists of Sapling, Orchard, and Ironwood note commitments
3024    /// associated with the scanned block as an owned value.
3025    pub fn into_commitments(self) -> ScannedBlockCommitments {
3026        ScannedBlockCommitments {
3027            sapling: self.sapling.commitments,
3028            #[cfg(feature = "orchard")]
3029            orchard: self.orchard.commitments,
3030            #[cfg(feature = "orchard")]
3031            ironwood: self.ironwood.commitments,
3032        }
3033    }
3034
3035    /// Returns the [`BlockMetadata`] corresponding to the scanned block.
3036    pub fn to_block_metadata(&self) -> BlockMetadata {
3037        BlockMetadata {
3038            block_height: self.block_height,
3039            block_hash: self.block_hash,
3040            sapling_tree_size: Some(self.sapling.final_tree_size),
3041            #[cfg(feature = "orchard")]
3042            orchard_tree_size: Some(self.orchard.final_tree_size),
3043            #[cfg(feature = "orchard")]
3044            ironwood_tree_size: Some(self.ironwood.final_tree_size),
3045        }
3046    }
3047}
3048
3049/// A trait representing a decryptable transaction.
3050pub trait DecryptableTransaction<AccountId> {
3051    type DecryptedSaplingOutput;
3052    #[cfg(feature = "orchard")]
3053    type DecryptedOrchardOutput;
3054}
3055
3056impl<AccountId> DecryptableTransaction<AccountId> for Transaction {
3057    type DecryptedSaplingOutput = DecryptedOutput<sapling::Note, AccountId>;
3058    #[cfg(feature = "orchard")]
3059    type DecryptedOrchardOutput = DecryptedOutput<(orchard::Note, orchard::ValuePool), AccountId>;
3060}
3061
3062/// A transaction that was detected during scanning of the blockchain,
3063/// including its decrypted Sapling and/or Orchard outputs.
3064///
3065/// The purpose of this struct is to permit atomic updates of the
3066/// wallet database when transactions are successfully decrypted.
3067pub struct DecryptedTransaction<'a, Tx: DecryptableTransaction<AccountId>, AccountId> {
3068    mined_height: Option<BlockHeight>,
3069    tx: &'a Tx,
3070    sapling_outputs: Vec<Tx::DecryptedSaplingOutput>,
3071    #[cfg(feature = "orchard")]
3072    orchard_outputs: Vec<Tx::DecryptedOrchardOutput>,
3073    #[cfg(feature = "orchard")]
3074    ironwood_outputs: Vec<Tx::DecryptedOrchardOutput>,
3075}
3076
3077impl<'a, Tx: DecryptableTransaction<AccountId>, AccountId> DecryptedTransaction<'a, Tx, AccountId> {
3078    /// Constructs a new [`DecryptedTransaction`] from its constituent parts.
3079    ///
3080    /// Ironwood outputs are Orchard-shaped but belong to a distinct pool, and are passed and
3081    /// tracked separately from Orchard outputs.
3082    pub fn new(
3083        mined_height: Option<BlockHeight>,
3084        tx: &'a Tx,
3085        sapling_outputs: Vec<Tx::DecryptedSaplingOutput>,
3086        #[cfg(feature = "orchard")] orchard_outputs: Vec<Tx::DecryptedOrchardOutput>,
3087        #[cfg(feature = "orchard")] ironwood_outputs: Vec<Tx::DecryptedOrchardOutput>,
3088    ) -> Self {
3089        Self {
3090            mined_height,
3091            tx,
3092            sapling_outputs,
3093            #[cfg(feature = "orchard")]
3094            orchard_outputs,
3095            #[cfg(feature = "orchard")]
3096            ironwood_outputs,
3097        }
3098    }
3099
3100    /// Returns the height at which the transaction was mined, if known.
3101    pub fn mined_height(&self) -> Option<BlockHeight> {
3102        self.mined_height
3103    }
3104    /// Returns the raw transaction data.
3105    pub fn tx(&self) -> &Tx {
3106        self.tx
3107    }
3108    /// Returns the Sapling outputs that were decrypted from the transaction.
3109    pub fn sapling_outputs(&self) -> &[Tx::DecryptedSaplingOutput] {
3110        &self.sapling_outputs
3111    }
3112    /// Returns the Orchard outputs that were decrypted from the transaction.
3113    #[cfg(feature = "orchard")]
3114    pub fn orchard_outputs(&self) -> &[Tx::DecryptedOrchardOutput] {
3115        &self.orchard_outputs
3116    }
3117
3118    /// Returns the Ironwood outputs that were decrypted from the transaction.
3119    ///
3120    /// Ironwood outputs are Orchard-shaped but belong to a pool distinct from Orchard.
3121    #[cfg(feature = "orchard")]
3122    pub fn ironwood_outputs(&self) -> &[Tx::DecryptedOrchardOutput] {
3123        &self.ironwood_outputs
3124    }
3125
3126    /// Returns whether the transaction has decrypted outputs
3127    pub fn has_decrypted_outputs(&self) -> bool {
3128        let has_sapling = !self.sapling_outputs.is_empty();
3129        #[cfg(feature = "orchard")]
3130        let has_orchard = !self.orchard_outputs.is_empty() || !self.ironwood_outputs.is_empty();
3131        #[cfg(not(feature = "orchard"))]
3132        let has_orchard = false;
3133
3134        has_sapling || has_orchard
3135    }
3136}
3137
3138/// A transaction that was constructed and sent by the wallet.
3139///
3140/// The purpose of this struct is to permit atomic updates of the
3141/// wallet database when transactions are created and submitted
3142/// to the network.
3143pub struct SentTransaction<'a, AccountId> {
3144    tx: &'a Transaction,
3145    created: time::OffsetDateTime,
3146    target_height: TargetHeight,
3147    funding_account: AccountId,
3148    outputs: &'a [SentTransactionOutput<AccountId>],
3149    fee_amount: Zatoshis,
3150    #[cfg(feature = "transparent-inputs")]
3151    utxos_spent: &'a [OutPoint],
3152}
3153
3154impl<'a, AccountId> SentTransaction<'a, AccountId> {
3155    /// Constructs a new [`SentTransaction`] from its constituent parts.
3156    ///
3157    /// ### Parameters
3158    /// - `tx`: the raw transaction data
3159    /// - `created`: the system time at which the transaction was created
3160    /// - `target_height`: the target height that was used in the construction of the transaction
3161    /// - `funding_account`: the account that spent funds in creation of the transaction
3162    /// - `outputs`: the outputs created by the transaction, including those sent to external
3163    ///   recipients which may not otherwise be recoverable
3164    /// - `fee_amount`: the fee value paid by the transaction
3165    /// - `utxos_spent`: the UTXOs controlled by the wallet that were spent in this transaction
3166    pub fn new(
3167        tx: &'a Transaction,
3168        created: time::OffsetDateTime,
3169        target_height: TargetHeight,
3170        funding_account: AccountId,
3171        outputs: &'a [SentTransactionOutput<AccountId>],
3172        fee_amount: Zatoshis,
3173        #[cfg(feature = "transparent-inputs")] utxos_spent: &'a [OutPoint],
3174    ) -> Self {
3175        Self {
3176            tx,
3177            created,
3178            target_height,
3179            funding_account,
3180            outputs,
3181            fee_amount,
3182            #[cfg(feature = "transparent-inputs")]
3183            utxos_spent,
3184        }
3185    }
3186
3187    /// Returns the transaction that was sent.
3188    pub fn tx(&self) -> &Transaction {
3189        self.tx
3190    }
3191    /// Returns the timestamp of the transaction's creation.
3192    pub fn created(&self) -> time::OffsetDateTime {
3193        self.created
3194    }
3195    /// Returns the id for the account that created the outputs.
3196    pub fn funding_account(&self) -> &AccountId {
3197        &self.funding_account
3198    }
3199    /// Returns the outputs of the transaction.
3200    pub fn outputs(&self) -> &[SentTransactionOutput<AccountId>] {
3201        self.outputs
3202    }
3203    /// Returns the fee paid by the transaction.
3204    pub fn fee_amount(&self) -> Zatoshis {
3205        self.fee_amount
3206    }
3207    /// Returns the list of UTXOs spent in the created transaction.
3208    #[cfg(feature = "transparent-inputs")]
3209    pub fn utxos_spent(&self) -> &[OutPoint] {
3210        self.utxos_spent
3211    }
3212
3213    /// Returns the block height that this transaction was created to target.
3214    pub fn target_height(&self) -> TargetHeight {
3215        self.target_height
3216    }
3217}
3218
3219/// High-level information about the output of a transaction received by the wallet.
3220///
3221/// This type is capable of representing both shielded and transparent outputs. It does not
3222/// internally store the transaction ID, so it must be interpreted in the context of a caller
3223/// having requested output information for a specific transaction.
3224pub struct ReceivedTransactionOutput {
3225    pool_type: PoolType,
3226    output_index: usize,
3227    value: Zatoshis,
3228    confirmations_until_spendable: u32,
3229}
3230
3231impl ReceivedTransactionOutput {
3232    /// Constructs a [`ReceivedTransactionOutput`] from its constituent parts.
3233    pub fn from_parts(
3234        pool_type: PoolType,
3235        output_index: usize,
3236        value: Zatoshis,
3237        confirmations_until_spendable: u32,
3238    ) -> Self {
3239        Self {
3240            pool_type,
3241            output_index,
3242            value,
3243            confirmations_until_spendable,
3244        }
3245    }
3246
3247    /// Returns the pool in which the output value was received.
3248    pub fn pool_type(&self) -> PoolType {
3249        self.pool_type
3250    }
3251
3252    /// Returns the index of the output among the transaction's outputs to the associated pool.
3253    pub fn output_index(&self) -> usize {
3254        self.output_index
3255    }
3256
3257    /// Returns the value of the output.
3258    pub fn value(&self) -> Zatoshis {
3259        self.value
3260    }
3261
3262    /// Returns the number of confirmations required for the output to be treated as spendable,
3263    /// given a [`ConfirmationsPolicy`] that was specified at the time of the request for this
3264    /// data.
3265    pub fn confirmations_until_spendable(&self) -> u32 {
3266        self.confirmations_until_spendable
3267    }
3268}
3269
3270/// Identifies one of the wallet-maintained note commitment trees.
3271#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3272pub enum NoteCommitmentTree {
3273    /// The Sapling note commitment tree.
3274    Sapling,
3275    /// The Orchard note commitment tree.
3276    #[cfg(feature = "orchard")]
3277    Orchard,
3278    /// The Ironwood note commitment tree.
3279    #[cfg(feature = "orchard")]
3280    Ironwood,
3281}
3282
3283/// An output of a transaction generated by the wallet.
3284///
3285/// This type is capable of representing both shielded and transparent outputs.
3286pub struct SentTransactionOutput<AccountId> {
3287    output_index: usize,
3288    note_commitment_tree: Option<NoteCommitmentTree>,
3289    recipient: Recipient<AccountId>,
3290    value: Zatoshis,
3291    memo: Option<MemoBytes>,
3292}
3293
3294impl<AccountId> SentTransactionOutput<AccountId> {
3295    /// Constructs a new [`SentTransactionOutput`] from its constituent parts.
3296    ///
3297    /// ### Fields:
3298    /// * `output_index` - the index of the output or action in the sent transaction
3299    /// * `recipient` - the recipient of the output, either a Zcash address or a
3300    ///   wallet-internal account and the note belonging to the wallet created by
3301    ///   the output
3302    /// * `value` - the value of the output, in zatoshis
3303    /// * `memo` - the memo that was sent with this output
3304    pub fn from_parts(
3305        output_index: usize,
3306        recipient: Recipient<AccountId>,
3307        value: Zatoshis,
3308        memo: Option<MemoBytes>,
3309    ) -> Self {
3310        Self {
3311            output_index,
3312            note_commitment_tree: None,
3313            recipient,
3314            value,
3315            memo,
3316        }
3317    }
3318
3319    /// Constructs a new [`SentTransactionOutput`] with explicit note commitment tree metadata.
3320    pub(crate) fn from_parts_in_tree(
3321        note_commitment_tree: Option<NoteCommitmentTree>,
3322        output_index: usize,
3323        recipient: Recipient<AccountId>,
3324        value: Zatoshis,
3325        memo: Option<MemoBytes>,
3326    ) -> Self {
3327        Self {
3328            output_index,
3329            note_commitment_tree,
3330            recipient,
3331            value,
3332            memo,
3333        }
3334    }
3335
3336    /// Returns the index within the transaction that contains the recipient output.
3337    ///
3338    /// - If `recipient_address` is a Sapling address, this is an index into the Sapling
3339    ///   outputs of the transaction.
3340    /// - If `recipient_address` is a transparent address, this is an index into the
3341    ///   transparent outputs of the transaction.
3342    pub fn output_index(&self) -> usize {
3343        self.output_index
3344    }
3345    /// Returns the note commitment tree for this output, if known.
3346    pub fn note_commitment_tree(&self) -> Option<NoteCommitmentTree> {
3347        self.note_commitment_tree
3348    }
3349    /// Returns the recipient address of the transaction, or the account id and
3350    /// resulting note/outpoint for wallet-internal outputs.
3351    pub fn recipient(&self) -> &Recipient<AccountId> {
3352        &self.recipient
3353    }
3354    /// Returns the value of the newly created output.
3355    pub fn value(&self) -> Zatoshis {
3356        self.value
3357    }
3358    /// Returns the memo that was attached to the output, if any. This will only be `None`
3359    /// for transparent outputs.
3360    pub fn memo(&self) -> Option<&MemoBytes> {
3361        self.memo.as_ref()
3362    }
3363}
3364
3365/// A data structure used to set the birthday height for an account, and ensure that the initial
3366/// note commitment tree state is recorded at that height.
3367#[derive(Clone, Debug, PartialEq, Eq)]
3368pub struct AccountBirthday {
3369    prior_chain_state: ChainState,
3370    recover_until: Option<BlockHeight>,
3371}
3372
3373/// Errors that can occur in the construction of an [`AccountBirthday`] from a [`TreeState`].
3374#[derive(Debug)]
3375#[non_exhaustive]
3376pub enum BirthdayError {
3377    /// The block height of the [`TreeState`] was out of range for a [`BlockHeight`].
3378    HeightInvalid(TryFromIntError),
3379    /// The note commitment tree frontiers of the [`TreeState`] could not be decoded.
3380    Decode(io::Error),
3381}
3382
3383impl fmt::Display for BirthdayError {
3384    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3385        match self {
3386            BirthdayError::HeightInvalid(e) => {
3387                write!(f, "Invalid block height for account birthday: {e}")
3388            }
3389            BirthdayError::Decode(e) => write!(
3390                f,
3391                "Failed to decode the note commitment tree state for the account birthday: {e}"
3392            ),
3393        }
3394    }
3395}
3396
3397impl std::error::Error for BirthdayError {
3398    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
3399        match self {
3400            BirthdayError::HeightInvalid(e) => Some(e),
3401            BirthdayError::Decode(e) => Some(e),
3402        }
3403    }
3404}
3405
3406impl From<TryFromIntError> for BirthdayError {
3407    fn from(value: TryFromIntError) -> Self {
3408        Self::HeightInvalid(value)
3409    }
3410}
3411
3412impl From<io::Error> for BirthdayError {
3413    fn from(value: io::Error) -> Self {
3414        Self::Decode(value)
3415    }
3416}
3417
3418impl AccountBirthday {
3419    /// Constructs a new [`AccountBirthday`] from its constituent parts.
3420    ///
3421    /// * `prior_chain_state`: The chain state prior to the birthday height of the account. The
3422    ///   birthday height is defined as the height of the first block to be scanned in wallet
3423    ///   recovery.
3424    /// * `recover_until`: An optional height at which the wallet should exit "recovery mode". In
3425    ///   order to avoid confusing shifts in wallet balance and spendability that may temporarily be
3426    ///   visible to a user during the process of recovering from seed, wallets may optionally set a
3427    ///   "recover until" height. The wallet is considered to be in "recovery mode" until there
3428    ///   exist no unscanned ranges between the wallet's birthday height and the provided
3429    ///   `recover_until` height, exclusive.
3430    pub fn from_parts(prior_chain_state: ChainState, recover_until: Option<BlockHeight>) -> Self {
3431        Self {
3432            prior_chain_state,
3433            recover_until,
3434        }
3435    }
3436
3437    /// Constructs a new [`AccountBirthday`] from a [`TreeState`] returned from `lightwalletd`.
3438    ///
3439    /// * `treestate`: The tree state corresponding to the last block prior to the wallet's
3440    ///   birthday height.
3441    /// * `recover_until`: An optional height at which the wallet should exit "recovery mode". In
3442    ///   order to avoid confusing shifts in wallet balance and spendability that may temporarily be
3443    ///   visible to a user during the process of recovering from seed, wallets may optionally set a
3444    ///   "recover until" height. The wallet is considered to be in "recovery mode" until there
3445    ///   exist no unscanned ranges between the wallet's birthday height and the provided
3446    ///   `recover_until` height, exclusive.
3447    pub fn from_treestate(
3448        treestate: TreeState,
3449        recover_until: Option<BlockHeight>,
3450    ) -> Result<Self, BirthdayError> {
3451        Ok(Self {
3452            prior_chain_state: treestate.to_chain_state()?,
3453            recover_until,
3454        })
3455    }
3456
3457    /// Returns the Sapling note commitment tree frontier as of the end of the block at
3458    /// [`Self::height`].
3459    pub fn sapling_frontier(
3460        &self,
3461    ) -> &Frontier<sapling::Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }> {
3462        self.prior_chain_state.final_sapling_tree()
3463    }
3464
3465    /// Returns the Orchard note commitment tree frontier as of the end of the block at
3466    /// [`Self::height`].
3467    #[cfg(feature = "orchard")]
3468    pub fn orchard_frontier(
3469        &self,
3470    ) -> &Frontier<orchard::tree::MerkleHashOrchard, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }>
3471    {
3472        self.prior_chain_state.final_orchard_tree()
3473    }
3474
3475    /// Returns the birthday height of the account.
3476    pub fn height(&self) -> BlockHeight {
3477        self.prior_chain_state.block_height() + 1
3478    }
3479
3480    /// Returns the height at which the wallet should exit "recovery mode".
3481    pub fn recover_until(&self) -> Option<BlockHeight> {
3482        self.recover_until
3483    }
3484
3485    /// Returns the [`ChainState`] corresponding to the last block prior to the wallet's birthday
3486    pub fn prior_chain_state(&self) -> &ChainState {
3487        &self.prior_chain_state
3488    }
3489
3490    #[cfg(any(test, feature = "test-dependencies"))]
3491    /// Constructs a new [`AccountBirthday`] at the given network upgrade's activation,
3492    /// with no "recover until" height.
3493    ///
3494    /// # Panics
3495    ///
3496    /// Panics if the activation height for the given network upgrade is not set.
3497    pub fn from_activation<P: zcash_protocol::consensus::Parameters>(
3498        params: &P,
3499        network_upgrade: NetworkUpgrade,
3500        prior_block_hash: BlockHash,
3501    ) -> AccountBirthday {
3502        AccountBirthday::from_parts(
3503            ChainState::empty(
3504                params.activation_height(network_upgrade).unwrap() - 1,
3505                prior_block_hash,
3506            ),
3507            None,
3508        )
3509    }
3510
3511    #[cfg(any(test, feature = "test-dependencies"))]
3512    /// Constructs a new [`AccountBirthday`] at Sapling activation, with no
3513    /// "recover until" height.
3514    ///
3515    /// # Panics
3516    ///
3517    /// Panics if the Sapling activation height is not set.
3518    pub fn from_sapling_activation<P: zcash_protocol::consensus::Parameters>(
3519        params: &P,
3520        prior_block_hash: BlockHash,
3521    ) -> AccountBirthday {
3522        Self::from_activation(params, NetworkUpgrade::Sapling, prior_block_hash)
3523    }
3524}
3525
3526/// This trait encapsulates the write capabilities required to update stored wallet data.
3527///
3528/// # Adding accounts
3529///
3530/// This trait provides several methods for adding accounts to the wallet data:
3531/// - [`WalletWrite::create_account`]
3532/// - [`WalletWrite::import_account_hd`]
3533/// - [`WalletWrite::import_account_ufvk`]
3534///
3535/// All of these methods take an [`AccountBirthday`]. The birthday height is defined as
3536/// the minimum block height that will be scanned for funds belonging to the wallet. If
3537/// `birthday.height()` is below the current chain tip, the account addition operation
3538/// will trigger a re-scan of the blocks at and above the provided height.
3539///
3540/// The order in which you call these methods will affect the resulting wallet structure:
3541/// - If only [`WalletWrite::create_account`] is used, the resulting accounts will have
3542///   sequential [ZIP 32] account indices within each given seed.
3543/// - If [`WalletWrite::import_account_hd`] is used to import accounts with non-sequential
3544///   ZIP 32 account indices from the same seed, a call to [`WalletWrite::create_account`]
3545///   will use the ZIP 32 account index just after the highest-numbered existing account.
3546/// - If an account is added to the wallet, and then a later call to one of the methods
3547///   would produce a UFVK that collides with that account on any FVK component (i.e.
3548///   Sapling, Orchard, or transparent), an error will be returned. This can occur in the
3549///   following cases:
3550///   - An account is created via [`WalletWrite::create_account`] with an auto-selected
3551///     ZIP 32 account index, and that index is later imported explicitly via either
3552///     [`WalletWrite::import_account_ufvk`] or [`WalletWrite::import_account_hd`].
3553///   - An account is imported via [`WalletWrite::import_account_ufvk`] or
3554///     [`WalletWrite::import_account_hd`], and then the ZIP 32 account index
3555///     corresponding to that account's UFVK is later imported either implicitly
3556///     via [`WalletWrite::create_account`], or explicitly via a call to
3557///     [`WalletWrite::import_account_ufvk`] or [`WalletWrite::import_account_hd`].
3558///
3559/// Note that an error will be returned on an FVK collision even if the UFVKs do not
3560/// match exactly, e.g. if they have different subsets of components.
3561///
3562/// An account is treated as having a single root of spending authority that spans the shielded and
3563/// transparent rules for the purpose of balance, transaction listing, and so forth. However,
3564/// transparent keys imported via `WalletWrite::import_standalone_transparent_pubkey` or
3565/// `WalletWrite::import_standalone_transparent_script` (available with the
3566/// `transparent-key-import` feature) break this abstraction slightly, so wallets using this API
3567/// need to be cautious to enforce the invariant that the wallet either maintains access to the
3568/// keys required to spend **ALL** outputs received by the account, or that it **DOES NOT** offer
3569/// any spending capability for the account, i.e. the account is treated as view-only for all
3570/// user-facing operations.
3571///
3572/// A future change to this trait might introduce a method to "upgrade" an imported
3573/// account with derivation information. See [zcash/librustzcash#1284] for details.
3574///
3575/// Users of the `WalletWrite` trait should generally distinguish in their APIs and wallet UIs
3576/// between creating a new account, and importing an account that previously existed. By
3577/// convention, wallets should only allow a new account to be generated for a seed after confirmed
3578/// funds have been received by the newest existing account for that seed; this allows automated
3579/// account recovery to discover and recover all funds within a particular seed.
3580///
3581/// # Creating a new wallet
3582///
3583/// To create a new wallet:
3584/// - Generate a new [BIP 39] mnemonic phrase, using a crate like [`bip0039`].
3585/// - Derive the corresponding seed from the mnemonic phrase.
3586/// - Use [`WalletWrite::create_account`] with the resulting seed.
3587///
3588/// Callers should construct the [`AccountBirthday`] using [`AccountBirthday::from_treestate`] for
3589/// the block at height `chain_tip_height - 100`. Setting the birthday height to a tree state below
3590/// the pruning depth ensures that reorgs cannot cause funds intended for the wallet to be missed;
3591/// otherwise, if the chain tip height were used for the wallet birthday, a transaction targeted at
3592/// a height greater than the chain tip could be mined at a height below that tip as part of a
3593/// reorg.
3594///
3595/// # Restoring a wallet from backup
3596///
3597/// To restore a backed-up wallet:
3598/// - Derive the seed from its BIP 39 mnemonic phrase.
3599/// - Use [`WalletWrite::import_account_hd`] once for each ZIP 32 account index that the
3600///   user wants to restore.
3601/// - If the highest previously-used ZIP 32 account index was _not_ restored by the user,
3602///   remember this index separately as `index_max`. The first time the user wants to
3603///   generate a new account, use [`WalletWrite::import_account_hd`] to create the account
3604///   `index_max + 1`.
3605/// - [`WalletWrite::create_account`] can be used to generate subsequent new accounts in
3606///   the restored wallet.
3607///
3608/// Automated account recovery has not yet been implemented by this crate. A wallet app
3609/// that supports multiple accounts can implement it manually by tracking account balances
3610/// relative to [`WalletSummary::fully_scanned_height`], and creating new accounts as
3611/// funds appear in existing accounts.
3612///
3613/// If the number of accounts is known in advance, the wallet should create all accounts before
3614/// scanning the chain so that the scan can be done in a single pass for all accounts.
3615///
3616/// [ZIP 32]: https://zips.z.cash/zip-0032
3617/// [zcash/librustzcash#1284]: https://github.com/zcash/librustzcash/issues/1284
3618/// [BIP 39]: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
3619/// [`bip0039`]: https://crates.io/crates/bip0039
3620#[cfg_attr(feature = "test-dependencies", delegatable_trait)]
3621pub trait WalletWrite:
3622    WalletRead
3623    + OutputLockStore<
3624        AccountId = <Self as WalletRead>::AccountId,
3625        Error = <Self as WalletRead>::Error,
3626    >
3627{
3628    /// The type of identifiers used to look up transparent UTXOs.
3629    type UtxoRef;
3630
3631    /// Tells the wallet to track the next available account-level spend authority for the provided
3632    /// seed value, given the current set of [ZIP 316] account identifiers known to the wallet database.
3633    ///
3634    /// The "next available account" is defined as the ZIP-32 account index immediately following
3635    /// the highest existing account index among all accounts in the wallet that share the given
3636    /// seed. Users of the [`WalletWrite`] trait that only call this method are guaranteed to have
3637    /// accounts with sequential indices.
3638    ///
3639    /// Returns the account identifier for the newly-created wallet database entry, along with the
3640    /// associated [`UnifiedSpendingKey`]. Note that the unique account identifier should *not* be
3641    /// assumed equivalent to the ZIP 32 account index. It is an opaque identifier for a pool of
3642    /// funds or set of outputs controlled by a single spending authority.
3643    ///
3644    /// The ZIP-32 account index may be obtained by calling [`WalletRead::get_account`]
3645    /// with the returned account identifier.
3646    ///
3647    /// The [`WalletWrite`] trait documentation has more details about account creation and import.
3648    ///
3649    /// # Arguments
3650    /// - `account_name`: A human-readable name for the account.
3651    /// - `seed`: The 256-byte (at least) HD seed from which to derive the account UFVK.
3652    /// - `birthday`: Metadata about where to start scanning blocks to find transactions intended
3653    ///   for the account.
3654    /// - `key_source`: A string identifier or other metadata describing the source of the seed.
3655    ///   This is treated as opaque metadata by the wallet backend; it is provided for use by
3656    ///   applications which need to track additional identifying information for an account.
3657    ///
3658    /// # Implementation notes
3659    ///
3660    /// Implementations of this method **MUST NOT** "fill in gaps" by selecting an account index
3661    /// that is lower than any existing account index among all accounts in the wallet that share
3662    /// the given seed.
3663    ///
3664    /// # Panics
3665    ///
3666    /// Panics if the length of the seed is not between 32 and 252 bytes inclusive.
3667    ///
3668    /// [ZIP 316]: https://zips.z.cash/zip-0316
3669    fn create_account(
3670        &mut self,
3671        account_name: &str,
3672        seed: &SecretVec<u8>,
3673        birthday: &AccountBirthday,
3674        key_source: Option<&str>,
3675    ) -> Result<(<Self as WalletRead>::AccountId, UnifiedSpendingKey), <Self as WalletRead>::Error>;
3676
3677    /// Tells the wallet to track a specific account index for a given seed.
3678    ///
3679    /// Returns details about the imported account, including the unique account identifier for
3680    /// the newly-created wallet database entry, along with the associated [`UnifiedSpendingKey`].
3681    /// Note that the unique account identifier should *not* be assumed equivalent to the ZIP 32
3682    /// account index. It is an opaque identifier for a pool of funds or set of outputs controlled
3683    /// by a single spending authority.
3684    ///
3685    /// Import accounts with indices that are exactly one greater than the highest existing account
3686    /// index to ensure account indices are contiguous, thereby facilitating automated account
3687    /// recovery.
3688    ///
3689    /// The [`WalletWrite`] trait documentation has more details about account creation and import.
3690    ///
3691    /// # Arguments
3692    /// - `account_name`: A human-readable name for the account.
3693    /// - `seed`: The 256-byte (at least) HD seed from which to derive the account UFVK.
3694    /// - `account_index`: The ZIP 32 account-level component of the HD derivation path at
3695    ///   which to derive the account's UFVK.
3696    /// - `birthday`: Metadata about where to start scanning blocks to find transactions intended
3697    ///   for the account.
3698    /// - `key_source`: A string identifier or other metadata describing the source of the seed.
3699    ///   This is treated as opaque metadata by the wallet backend; it is provided for use by
3700    ///   applications which need to track additional identifying information for an account.
3701    ///
3702    /// # Panics
3703    ///
3704    /// Panics if the length of the seed is not between 32 and 252 bytes inclusive.
3705    ///
3706    /// [ZIP 316]: https://zips.z.cash/zip-0316
3707    fn import_account_hd(
3708        &mut self,
3709        account_name: &str,
3710        seed: &SecretVec<u8>,
3711        account_index: zip32::AccountId,
3712        birthday: &AccountBirthday,
3713        key_source: Option<&str>,
3714    ) -> Result<(Self::Account, UnifiedSpendingKey), <Self as WalletRead>::Error>;
3715
3716    /// Tells the wallet to track an account using a unified full viewing key.
3717    ///
3718    /// Returns details about the imported account, including the unique account identifier for
3719    /// the newly-created wallet database entry. Unlike the other account creation APIs
3720    /// ([`Self::create_account`] and [`Self::import_account_hd`]), no spending key is returned
3721    /// because the wallet has no information about how the UFVK was derived.
3722    ///
3723    /// Certain optimizations are possible for accounts which will never be used to spend funds. If
3724    /// `spending_key_available` is `false`, the wallet may choose to optimize for this case, in
3725    /// which case any attempt to spend funds from the account will result in an error.
3726    ///
3727    /// The [`WalletWrite`] trait documentation has more details about account creation and import.
3728    ///
3729    /// # Arguments
3730    /// - `account_name`: A human-readable name for the account.
3731    /// - `unified_key`: The UFVK used to detect transactions involving the account.
3732    /// - `birthday`: Metadata about where to start scanning blocks to find transactions intended
3733    ///   for the account.
3734    /// - `purpose`: Metadata describing whether or not data required for spending should be
3735    ///   tracked by the wallet.
3736    /// - `key_source`: A string identifier or other metadata describing the source of the seed.
3737    ///   This is treated as opaque metadata by the wallet backend; it is provided for use by
3738    ///   applications which need to track additional identifying information for an account.
3739    fn import_account_ufvk(
3740        &mut self,
3741        account_name: &str,
3742        unified_key: &UnifiedFullViewingKey,
3743        birthday: &AccountBirthday,
3744        purpose: AccountPurpose,
3745        key_source: Option<&str>,
3746    ) -> Result<Self::Account, <Self as WalletRead>::Error>;
3747
3748    /// Deletes the specified account, and all transactions that exclusively involve it, from the
3749    /// wallet database.
3750    ///
3751    /// WARNING: This is a destructive operation and may result in the permanent loss of
3752    /// potentially important information that is not recoverable from chain data, including:
3753    /// * Data about transactions sent by the account for which [`OvkPolicy::Discard`] (or
3754    ///   [`OvkPolicy::Custom`] with random OVKs) was used;
3755    /// * Data related to transactions that the account attempted to send that expired or were
3756    ///   otherwise invalidated without having been mined in the main chain;
3757    /// * Data related to transactions that were observed in the mempool as having inputs or
3758    ///   outputs that involved the account, but that were never mined in the main chain;
3759    /// * Data related to transactions that were received by the wallet in a mined block, where
3760    ///   that block was later un-mined in a chain reorg and the transaction was either invalidated
3761    ///   or was never re-mined.
3762    ///
3763    /// [`OvkPolicy::Discard`]: crate::wallet::OvkPolicy::Discard
3764    /// [`OvkPolicy::Custom`]: crate::wallet::OvkPolicy::Custom
3765    fn delete_account(
3766        &mut self,
3767        account: <Self as WalletRead>::AccountId,
3768    ) -> Result<(), <Self as WalletRead>::Error>;
3769
3770    /// Imports the given pubkey into the account without key derivation information, and adds the
3771    /// associated transparent p2pkh address.
3772    ///
3773    /// The imported address will contribute to the balance of the account (for UFVK-based
3774    /// accounts), but spending funds held by this address requires the associated spending keys to
3775    /// be provided explicitly when calling [`create_proposed_transactions`]. By extension, calls
3776    /// to [`propose_shielding`] must only include addresses for which the spending application
3777    /// holds or can obtain the spending keys.
3778    ///
3779    /// [`create_proposed_transactions`]: crate::data_api::wallet::create_proposed_transactions
3780    /// [`propose_shielding`]: crate::data_api::wallet::propose_shielding
3781    #[cfg(feature = "transparent-key-import")]
3782    fn import_standalone_transparent_pubkey(
3783        &mut self,
3784        _account: <Self as WalletRead>::AccountId,
3785        _pubkey: secp256k1::PublicKey,
3786    ) -> Result<(), <Self as WalletRead>::Error> {
3787        unimplemented!(
3788            "WalletWrite::import_standalone_transparent_pubkey must be overridden for wallets to use the `transparent-key-import` feature"
3789        )
3790    }
3791
3792    /// Imports a batch of standalone transparent pubkeys into the account, adding the associated
3793    /// transparent p2pkh addresses. See [`import_standalone_transparent_pubkey`] for the semantics
3794    /// and spending limitations that apply to each imported pubkey.
3795    ///
3796    /// This is equivalent to calling [`import_standalone_transparent_pubkey`] once per pubkey, but
3797    /// implementations may validate the target account a single time for the whole batch. The
3798    /// default implementation calls [`import_standalone_transparent_pubkey`] for each pubkey; a
3799    /// pubkey whose receiver address is already known to the wallet is skipped.
3800    ///
3801    /// [`import_standalone_transparent_pubkey`]: Self::import_standalone_transparent_pubkey
3802    #[cfg(feature = "transparent-key-import")]
3803    fn import_standalone_transparent_pubkeys(
3804        &mut self,
3805        account: <Self as WalletRead>::AccountId,
3806        pubkeys: &[secp256k1::PublicKey],
3807    ) -> Result<(), <Self as WalletRead>::Error> {
3808        for pubkey in pubkeys {
3809            self.import_standalone_transparent_pubkey(account, *pubkey)?;
3810        }
3811        Ok(())
3812    }
3813
3814    /// Imports the given redeem script into the account without key derivation information, and
3815    /// adds the associated transparent p2sh address.
3816    ///
3817    /// The imported address will contribute to the balance of the account (for UFVK-based
3818    /// accounts), but spending funds held by this address requires the associated spending keys to
3819    /// be provided explicitly when calling [`create_proposed_transactions`]. By extension, calls
3820    /// to [`propose_shielding`] must only include addresses for which the spending application
3821    /// holds or can obtain the spending keys.
3822    ///
3823    /// [`create_proposed_transactions`]: crate::data_api::wallet::create_proposed_transactions
3824    /// [`propose_shielding`]: crate::data_api::wallet::propose_shielding
3825    ///
3826    /// # Spending limitations
3827    ///
3828    /// P2PKH-in-P2SH scripts are unsupported by PCZT at this time, so the only way to spend
3829    /// from such an address is to use the [`create_proposed_transactions`] signing path.
3830    #[cfg(feature = "transparent-key-import")]
3831    fn import_standalone_transparent_script(
3832        &mut self,
3833        _account: <Self as WalletRead>::AccountId,
3834        _script: zcash_script::script::Redeem,
3835    ) -> Result<(), <Self as WalletRead>::Error> {
3836        unimplemented!(
3837            "WalletWrite::import_standalone_transparent_script must be overridden for wallets to use the `transparent-key-import` feature"
3838        )
3839    }
3840
3841    /// Generates, persists, and marks as exposed the next available diversified address for the
3842    /// specified account, given the current addresses known to the wallet.
3843    ///
3844    /// Returns `Ok(None)` if the account identifier does not correspond to a known
3845    /// account.
3846    fn get_next_available_address(
3847        &mut self,
3848        account: <Self as WalletRead>::AccountId,
3849        request: UnifiedAddressRequest,
3850    ) -> Result<Option<(UnifiedAddress, DiversifierIndex)>, <Self as WalletRead>::Error>;
3851
3852    /// Generates, persists, and marks as exposed a diversified address for the specified account
3853    /// at the provided diversifier index.
3854    ///
3855    /// Returns `Ok(None)` in the case that it is not possible to generate an address conforming
3856    /// to the provided request at the specified diversifier index. Such a result might arise from
3857    /// the diversifier index not being valid for a [`ReceiverRequirement::Require`]'ed receiver.
3858    /// Some implementations of this trait may return `Err(_)` in some cases to expose more
3859    /// information, which is only accessible in a backend-specific context.
3860    ///
3861    /// Address generation should fail if an address has already been exposed for the given
3862    /// diversifier index and the given request produced an address having different receivers than
3863    /// what was originally exposed.
3864    ///
3865    /// # WARNINGS
3866    /// If an address generated using this method has a transparent receiver and the
3867    /// chosen diversifier index would be outside the wallet's internally-configured gap limit,
3868    /// funds sent to these address are **likely to not be discovered on recovery from seed**. It
3869    /// up to the caller of this method to either ensure that they only request transparent
3870    /// receivers with indices within the range of a reasonable gap limit, or that they ensure that
3871    /// their wallet provides backup facilities that can be used to ensure that funds sent to such
3872    /// addresses are recoverable after a loss of wallet data.
3873    ///
3874    /// [`ReceiverRequirement::Require`]: zcash_keys::keys::ReceiverRequirement::Require
3875    fn get_address_for_index(
3876        &mut self,
3877        account: <Self as WalletRead>::AccountId,
3878        diversifier_index: DiversifierIndex,
3879        request: UnifiedAddressRequest,
3880    ) -> Result<Option<UnifiedAddress>, <Self as WalletRead>::Error>;
3881
3882    /// Updates the wallet's view of the blockchain.
3883    ///
3884    /// This method is used to provide the wallet with information about the state of the
3885    /// blockchain, and detect any previously scanned data that needs to be re-validated
3886    /// before proceeding with scanning. It should be called at wallet startup prior to calling
3887    /// [`WalletRead::suggest_scan_ranges`] in order to provide the wallet with the information it
3888    /// needs to correctly prioritize scanning operations.
3889    fn update_chain_tip(
3890        &mut self,
3891        tip_height: BlockHeight,
3892    ) -> Result<(), <Self as WalletRead>::Error>;
3893
3894    /// Drops the scan work queued below `height`, except where retained by
3895    /// `retain_with_priority`. Returns the number of queue entries removed or altered.
3896    ///
3897    /// If `retain_with_priority` is `None`, no entries below `height` are retained,
3898    /// irrespective of their priority. If it is `Some(priority)`, entries with that
3899    /// priority and greater are retained (left untouched, even where they straddle
3900    /// `height`), as are entries with the bookkeeping priorities
3901    /// [`ScanPriority::Scanned`] and [`ScanPriority::Ignored`] — those record which
3902    /// regions of the chain the backend has already covered or deliberately skips, and
3903    /// removing them would cause the backend to forget coverage state it maintains
3904    /// itself (use the `None` form when that full reset is the intent). Entries with
3905    /// priorities between the bookkeeping ones and the retained threshold are pruned.
3906    ///
3907    /// Pruning must not leave a gap in the queue's coverage: implementations are required
3908    /// to preserve contiguity across whatever remains below `height`, which in general
3909    /// means demoting pruned ranges to [`ScanPriority::Ignored`] rather than deleting them
3910    /// outright. Only coverage below the lowest retained entry may be deleted, since that
3911    /// merely raises the floor of the queue. A caller may therefore observe that the total
3912    /// span of the queue is unchanged and that the pruned region is now `Ignored`.
3913    ///
3914    /// This is a queue-hygiene operation. The primary use case is discarding historic scan
3915    /// ranges that no remaining account justifies: [`WalletWrite::delete_account`] does not
3916    /// modify the scan queue, so the deep ranges queued for a since-deleted account's
3917    /// birthday would otherwise still be scanned even though no remaining account can have
3918    /// notes below its own birthday. In that case, pass the wallet birthday
3919    /// ([`WalletRead::get_wallet_birthday`]) as `height` and retain
3920    /// [`ScanPriority::OpenAdjacent`] and greater — the priorities that may legitimately
3921    /// reach below the wallet birthday in service of note witnesses.
3922    fn prune_scan_queue_below(
3923        &mut self,
3924        height: BlockHeight,
3925        retain_with_priority: Option<ScanPriority>,
3926    ) -> Result<u64, <Self as WalletRead>::Error>;
3927
3928    /// Updates the state of the wallet database by persisting the provided block information,
3929    /// along with the note commitments that were detected when scanning the block for transactions
3930    /// pertaining to this wallet.
3931    ///
3932    /// ### Arguments
3933    /// - `from_state` must be the chain state for the block height prior to the first
3934    ///   block in `blocks`.
3935    /// - `blocks` must be sequential, in order of increasing block height.
3936    fn put_blocks(
3937        &mut self,
3938        from_state: &ChainState,
3939        blocks: Vec<ScannedBlock<<Self as WalletRead>::AccountId>>,
3940    ) -> Result<(), <Self as WalletRead>::Error>;
3941
3942    /// Adds a transparent UTXO received by the wallet to the data store.
3943    fn put_received_transparent_utxo(
3944        &mut self,
3945        output: &WalletTransparentOutput<<Self as WalletRead>::AccountId>,
3946    ) -> Result<Self::UtxoRef, <Self as WalletRead>::Error>;
3947
3948    /// Caches a decrypted transaction in the persistent wallet store.
3949    fn store_decrypted_tx(
3950        &mut self,
3951        received_tx: DecryptedTransaction<Transaction, <Self as WalletRead>::AccountId>,
3952    ) -> Result<(), <Self as WalletRead>::Error>;
3953
3954    /// Sets the trust status of the given transaction to either trusted or untrusted.
3955    ///
3956    /// The outputs of a trusted transaction will be available for spending with
3957    /// [`ConfirmationsPolicy::trusted`] confirmations even if the output is not wallet-internal.
3958    fn set_tx_trust(
3959        &mut self,
3960        txid: TxId,
3961        trusted: bool,
3962    ) -> Result<(), <Self as WalletRead>::Error>;
3963
3964    /// Saves information about transactions constructed by the wallet to the persistent
3965    /// wallet store.
3966    ///
3967    /// This must be called before the transactions are sent to the network.
3968    ///
3969    /// Transactions that have been stored by this method should be retransmitted while it
3970    /// is still possible that they could be mined.
3971    ///
3972    /// Implementations must unlock any locked outputs that are recorded as spent by the
3973    /// stored transactions. Once spend records exist, the outputs are protected from
3974    /// double-selection by the spend tracking mechanism, so the explicit locks are no
3975    /// longer needed.
3976    fn store_transactions_to_be_sent(
3977        &mut self,
3978        transactions: &[SentTransaction<<Self as WalletRead>::AccountId>],
3979    ) -> Result<(), <Self as WalletRead>::Error>;
3980
3981    /// Truncates the wallet database to at most the specified height.
3982    ///
3983    /// Implementations of this method may choose a lower block height to which the data store will
3984    /// be truncated if it is not possible to truncate exactly to the specified height. Upon
3985    /// successful truncation, this method returns the height to which the data store was actually
3986    /// truncated.
3987    ///
3988    /// This method assumes that the state of the underlying data store is consistent up to a
3989    /// particular block height. Since it is possible that a chain reorg might invalidate some
3990    /// stored state, this method must be implemented in order to allow users of this API to
3991    /// "reset" the data store to correctly represent chainstate as of at most the requested block
3992    /// height.
3993    ///
3994    /// After calling this method, the block at the returned height will be the most recent block
3995    /// and all other operations will treat this block as the chain tip for balance determination
3996    /// purposes.
3997    ///
3998    /// There may be restrictions on heights to which it is possible to truncate. Specifically, it
3999    /// will only be possible to truncate to heights at which is is possible to create a witness
4000    /// given the current state of the wallet's note commitment tree.
4001    fn truncate_to_height(
4002        &mut self,
4003        max_height: BlockHeight,
4004    ) -> Result<BlockHeight, <Self as WalletRead>::Error>;
4005
4006    /// Truncates the wallet database to the specified chain state.
4007    ///
4008    /// In contrast to [`truncate_to_height`], this method allows the caller to truncate the wallet
4009    /// database to a precise height by providing additional chain state information needed for
4010    /// note commitment tree maintenance after the truncation.
4011    ///
4012    /// [`truncate_to_height`]: WalletWrite::truncate_to_height
4013    fn truncate_to_chain_state(
4014        &mut self,
4015        chain_state: ChainState,
4016    ) -> Result<(), <Self as WalletRead>::Error>;
4017
4018    /// Rewinds the wallet to the specified chain state, preserving wallet data which has been
4019    /// confirmed beyond the pruning depth, and lowering the birthday height of selected accounts
4020    /// to the block following the chain state.
4021    ///
4022    /// In contrast to [`truncate_to_chain_state`], which unconditionally removes wallet state
4023    /// above `chain_state.block_height()`, this rewinds the scan queue to the target height but
4024    /// only rewinds blocks, note commitment trees, transactions, transparent UTXO observations,
4025    /// and nullifier-map entries as far back as the implementation's pruning floor; data at or
4026    /// below that floor is preserved.
4027    ///
4028    /// `reset_account_birthdays` selects which accounts (if any) may have their birthday
4029    /// metadata lowered as a result of this rewind. The semantics are:
4030    ///
4031    /// - Every account in `reset_account_birthdays` has its birthday metadata updated to
4032    ///   `chain_state.block_height() + 1` (with corresponding tree sizes taken from
4033    ///   `chain_state`) if and only if the new birthday is less than the account's existing
4034    ///   birthday. Existing birthdays are never raised by this method.
4035    /// - Accounts that are *not* in `reset_account_birthdays` are never modified, regardless of
4036    ///   the rewind target. Note that this only governs per-account birthday metadata:
4037    ///   rescanning of blocks that re-enter the scan queue applies to *all* accounts in the
4038    ///   wallet, since scanning is performed against all viewing keys.
4039    /// - If `reset_account_birthdays` is empty and *every* account in the wallet has a birthday
4040    ///   greater than `chain_state.block_height() + 1` (the value to which a reset birthday
4041    ///   would be lowered), this method returns [`RewindError::RewindBeyondBirthdays`] and no
4042    ///   other state is modified. So long as at least one account in the wallet already has a
4043    ///   birthday at or below `chain_state.block_height() + 1`, this error is not returned —
4044    ///   such an account already provides the wallet with an anchor at or below the new
4045    ///   birthday floor, so no reset is required. The reported map contains every account in
4046    ///   the wallet along with its existing birthday height; the caller may re-invoke the
4047    ///   method with any subset of those accounts included in `reset_account_birthdays`.
4048    ///
4049    /// Implementations may also return an [`Err`] (typically via [`RewindError::DataSource`])
4050    /// if `reset_account_birthdays` contains identifiers that do not correspond to accounts in
4051    /// the wallet.
4052    ///
4053    /// [`truncate_to_chain_state`]: WalletWrite::truncate_to_chain_state
4054    fn rewind_to_chain_state(
4055        &mut self,
4056        chain_state: ChainState,
4057        reset_account_birthdays: HashSet<<Self as WalletRead>::AccountId>,
4058    ) -> Result<(), RewindError<<Self as WalletRead>::AccountId, <Self as WalletRead>::Error>>;
4059
4060    /// Reserves the next `n` available ephemeral addresses for the given account.
4061    /// This cannot be undone, so as far as possible, errors associated with transaction
4062    /// construction should have been reported before calling this method.
4063    ///
4064    /// To ensure that sufficient information is stored on-chain to allow recovering
4065    /// funds sent back to any of the used addresses, a "gap limit" of 20 addresses
4066    /// should be observed as described in [BIP 44].
4067    ///
4068    /// Returns an error if there is insufficient space within the gap limit to allocate
4069    /// the given number of addresses, or if the account identifier does not correspond
4070    /// to a known account.
4071    ///
4072    /// [BIP 44]: https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#user-content-Address_gap_limit
4073    #[cfg(feature = "transparent-inputs")]
4074    fn reserve_next_n_ephemeral_addresses(
4075        &mut self,
4076        _account_id: <Self as WalletRead>::AccountId,
4077        _n: usize,
4078    ) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, <Self as WalletRead>::Error>
4079    {
4080        unimplemented!(
4081            "WalletWrite::reserve_next_n_ephemeral_addresses must be overridden for wallets to use the `transparent-inputs` feature"
4082        )
4083    }
4084
4085    /// Reserves the next `n` available internal-scope (change) transparent addresses for
4086    /// the given account, as described in [BIP 44] under the `change` path level. This
4087    /// cannot be undone, so as far as possible, errors associated with transaction
4088    /// construction should have been reported before calling this method.
4089    ///
4090    /// Internal-scope transparent addresses are used to receive change for transactions
4091    /// having fully-transparent value flows, when the change strategy in use is configured
4092    /// with [`TransparentChangePolicy::TransparentChangeAllowed`].
4093    ///
4094    /// To ensure that funds sent to internal-scope addresses are recoverable, implementations
4095    /// of this method should observe a gap limit as described in [BIP 44]; change addresses
4096    /// receive funds immediately upon reservation, so a smaller gap limit than the one used
4097    /// for external addresses may be observed.
4098    ///
4099    /// Returns an error if there is insufficient space within the gap limit to allocate
4100    /// the given number of addresses, or if the account identifier does not correspond
4101    /// to a known account.
4102    ///
4103    /// [BIP 44]: https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
4104    /// [`TransparentChangePolicy::TransparentChangeAllowed`]: crate::fees::TransparentChangePolicy::TransparentChangeAllowed
4105    #[cfg(feature = "transparent-inputs")]
4106    fn reserve_next_n_internal_addresses(
4107        &mut self,
4108        _account_id: <Self as WalletRead>::AccountId,
4109        _n: usize,
4110    ) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, <Self as WalletRead>::Error>
4111    {
4112        unimplemented!(
4113            "WalletWrite::reserve_next_n_internal_addresses must be overridden for wallets to \
4114             create transactions that produce transparent change"
4115        )
4116    }
4117
4118    /// Updates the wallet backend with respect to the status of a specific transaction, from the
4119    /// perspective of the main chain.
4120    ///
4121    /// Fully transparent transactions, and transactions that do not contain either shielded inputs
4122    /// or shielded outputs belonging to the wallet, may not be discovered by the process of chain
4123    /// scanning; as a consequence, the wallet must actively query to determine whether such
4124    /// transactions have been mined.
4125    fn set_transaction_status(
4126        &mut self,
4127        _txid: TxId,
4128        _status: TransactionStatus,
4129    ) -> Result<(), <Self as WalletRead>::Error>;
4130
4131    /// Schedules a UTXO check for the given address at a random time that has an expected value of
4132    /// `offset_seconds` from the current system time.
4133    ///
4134    /// Returns the time at which the check has been scheduled, or `None` if the address is not
4135    /// being tracked by the wallet.
4136    #[cfg(feature = "transparent-inputs")]
4137    fn schedule_next_check(
4138        &mut self,
4139        _address: &TransparentAddress,
4140        _offset_seconds: u32,
4141    ) -> Result<Option<SystemTime>, <Self as WalletRead>::Error> {
4142        unimplemented!(
4143            "WalletWrite::schedule_next_check must be overridden for wallets to use the `transparent-inputs` feature"
4144        )
4145    }
4146
4147    /// Informs the wallet backend that the given transparent addresses are known to have been
4148    /// exposed externally at or before the block height paired with each address.
4149    ///
4150    /// This method is intended for use when a wallet has learned, through means outside the
4151    /// observation of the chain by this backend, that addresses under the wallet's control have
4152    /// been disclosed to an external party. Calling this method ensures that the wallet's exposure
4153    /// metadata accounts for the earlier disclosures.
4154    ///
4155    /// If the wallet already tracks an earlier exposure for an address, the earlier height is
4156    /// retained.
4157    ///
4158    /// The operation is atomic: if any address in `exposures` is not known to the wallet,
4159    /// implementations must roll back all updates performed during the call and return an
4160    /// implementation-defined error identifying the first unrecognized address.
4161    /// Passing an empty slice is a no-op.
4162    #[cfg(feature = "transparent-inputs")]
4163    fn mark_transparent_addresses_exposed(
4164        &mut self,
4165        _exposures: &[(TransparentAddress, BlockHeight)],
4166    ) -> Result<(), <Self as WalletRead>::Error> {
4167        unimplemented!(
4168            "WalletWrite::mark_transparent_addresses_exposed must be overridden for wallets to use the `transparent-inputs` feature"
4169        )
4170    }
4171
4172    /// Notifies the wallet backend that the given query for transactions involving a particular
4173    /// address has completed evaluation.
4174    ///
4175    /// # Arguments
4176    /// - `request`: the [`TransactionsInvolvingAddress`] request that was executed.
4177    /// - `as_of_height`: The maximum height among blocks that were inspected in the process of
4178    ///   performing the requested check.
4179    #[cfg(feature = "transparent-inputs")]
4180    fn notify_address_checked(
4181        &mut self,
4182        _request: TransactionsInvolvingAddress,
4183        _as_of_height: BlockHeight,
4184    ) -> Result<(), <Self as WalletRead>::Error> {
4185        unimplemented!(
4186            "WalletWrite::notify_address_checked must be overridden for wallets to use the `transparent-inputs` feature"
4187        )
4188    }
4189
4190    /// Notifies the wallet backend that a specific transparent output was confirmed unspent as of
4191    /// the given height, in response to a [`TransactionDataRequest::GetSpendingTx`]
4192    /// request.
4193    ///
4194    /// # Arguments
4195    /// - `outpoint`: the transparent outpoint whose spend status was checked.
4196    /// - `as_of_height`: the maximum height among blocks that were inspected, and through which
4197    ///   the outpoint is confirmed to remain unspent.
4198    #[cfg(feature = "spend-index")]
4199    fn notify_output_verified_unspent(
4200        &mut self,
4201        _outpoint: OutPoint,
4202        _as_of_height: BlockHeight,
4203    ) -> Result<(), <Self as WalletRead>::Error> {
4204        unimplemented!(
4205            "WalletWrite::notify_output_verified_unspent must be overridden for wallets to use the `spend-index` feature"
4206        )
4207    }
4208}
4209
4210/// Applies a batch of note commitment tree changes — shards, an optional replacement tree
4211/// cap, and a checkpoint delta — directly to the given tree's backing [`ShardStore`].
4212///
4213/// `shards` must be in ascending shard-index order; stores may reject sequences that would
4214/// leave gaps in the tree. Checkpoint removals are applied before additions, so that a
4215/// checkpoint whose data has changed may appear in both lists.
4216///
4217/// This is the shared implementation of the [`WalletCommitmentTrees`] `put_*_shards`
4218/// provided methods.
4219///
4220/// NOTE: This procedure must be called only within a the context of a transaction, such as
4221/// in the scope of a `with_*_tree_mut` call; otherwise, failure of an intermediate step could
4222/// lead to data corruption.
4223fn apply_tree_changes<H, S, const DEPTH: u8, const SHARD_HEIGHT: u8>(
4224    tree: &mut ShardTree<S, DEPTH, SHARD_HEIGHT>,
4225    shards: &[shardtree::LocatedPrunableTree<H>],
4226    cap: Option<&shardtree::PrunableTree<H>>,
4227    checkpoints_remove: &[BlockHeight],
4228    checkpoints_add: &[(BlockHeight, shardtree::store::Checkpoint)],
4229) -> Result<(), ShardTreeError<S::Error>>
4230where
4231    H: incrementalmerkletree::Hashable + Clone + PartialEq,
4232    S: ShardStore<H = H, CheckpointId = BlockHeight>,
4233{
4234    for shard in shards {
4235        tree.store_mut()
4236            .put_shard(shard.clone())
4237            .map_err(ShardTreeError::Storage)?;
4238    }
4239    if let Some(cap) = cap {
4240        tree.store_mut()
4241            .put_cap(cap.clone())
4242            .map_err(ShardTreeError::Storage)?;
4243    }
4244    for height in checkpoints_remove {
4245        tree.store_mut()
4246            .remove_checkpoint(height)
4247            .map_err(ShardTreeError::Storage)?;
4248    }
4249    for (height, checkpoint) in checkpoints_add {
4250        tree.store_mut()
4251            .add_checkpoint(*height, checkpoint.clone())
4252            .map_err(ShardTreeError::Storage)?;
4253    }
4254    Ok(())
4255}
4256
4257/// This trait describes a capability for manipulating wallet note commitment trees.
4258#[cfg_attr(feature = "test-dependencies", delegatable_trait)]
4259pub trait WalletCommitmentTrees {
4260    type Error: Debug;
4261
4262    /// The type of the backing [`ShardStore`] for the Sapling note commitment tree.
4263    type SaplingShardStore<'a>: ShardStore<H = sapling::Node, CheckpointId = BlockHeight, Error = Self::Error>;
4264
4265    /// Evaluates the given callback function with a reference to the Sapling
4266    /// note commitment tree maintained by the wallet.
4267    fn with_sapling_tree_mut<F, A, E>(&mut self, callback: F) -> Result<A, E>
4268    where
4269        for<'a> F: FnMut(
4270            &'a mut ShardTree<
4271                Self::SaplingShardStore<'a>,
4272                { sapling::NOTE_COMMITMENT_TREE_DEPTH },
4273                SAPLING_SHARD_HEIGHT,
4274            >,
4275        ) -> Result<A, E>,
4276        E: From<ShardTreeError<Self::Error>>;
4277
4278    /// Adds a sequence of Sapling note commitment tree subtree roots to the data store.
4279    ///
4280    /// Each such value should be the Merkle root of a subtree of the Sapling note commitment tree
4281    /// containing 2^[`SAPLING_SHARD_HEIGHT`] note commitments.
4282    fn put_sapling_subtree_roots(
4283        &mut self,
4284        start_index: u64,
4285        roots: &[CommitmentTreeRoot<sapling::Node>],
4286    ) -> Result<(), ShardTreeError<Self::Error>>;
4287
4288    /// Returns the stored root hash of the completed Sapling subtree with the given index,
4289    /// or `Ok(None)` if no root is recorded for that subtree.
4290    ///
4291    /// This is the store's record of the subtree root as most recently provided via
4292    /// [`WalletCommitmentTrees::put_sapling_subtree_roots`] (i.e. the chain-authoritative
4293    /// root obtained from a chain data provider), or as recorded when a locally-completed
4294    /// subtree was persisted.
4295    fn get_sapling_subtree_root(
4296        &mut self,
4297        index: u64,
4298    ) -> Result<Option<sapling::Node>, ShardTreeError<Self::Error>>;
4299
4300    /// The type of the backing [`ShardStore`] for the Orchard note commitment tree.
4301    #[cfg(feature = "orchard")]
4302    type OrchardShardStore<'a>: ShardStore<
4303            H = orchard::tree::MerkleHashOrchard,
4304            CheckpointId = BlockHeight,
4305            Error = Self::Error,
4306        >;
4307
4308    /// Evaluates the given callback function with a reference to the Orchard
4309    /// note commitment tree maintained by the wallet.
4310    #[cfg(feature = "orchard")]
4311    fn with_orchard_tree_mut<F, A, E>(&mut self, callback: F) -> Result<A, E>
4312    where
4313        for<'a> F: FnMut(
4314            &'a mut ShardTree<
4315                Self::OrchardShardStore<'a>,
4316                { ORCHARD_SHARD_HEIGHT * 2 },
4317                ORCHARD_SHARD_HEIGHT,
4318            >,
4319        ) -> Result<A, E>,
4320        E: From<ShardTreeError<Self::Error>>;
4321
4322    /// Adds a sequence of Orchard note commitment tree subtree roots to the data store.
4323    ///
4324    /// Each such value should be the Merkle root of a subtree of the Orchard note commitment tree
4325    /// containing 2^[`ORCHARD_SHARD_HEIGHT`] note commitments.
4326    #[cfg(feature = "orchard")]
4327    fn put_orchard_subtree_roots(
4328        &mut self,
4329        start_index: u64,
4330        roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
4331    ) -> Result<(), ShardTreeError<Self::Error>>;
4332
4333    /// Returns the stored root hash of the completed Orchard subtree with the given index,
4334    /// or `Ok(None)` if no root is recorded for that subtree.
4335    ///
4336    /// This is the store's record of the subtree root as most recently provided via
4337    /// [`WalletCommitmentTrees::put_orchard_subtree_roots`] (i.e. the chain-authoritative
4338    /// root obtained from a chain data provider), or as recorded when a locally-completed
4339    /// subtree was persisted.
4340    #[cfg(feature = "orchard")]
4341    fn get_orchard_subtree_root(
4342        &mut self,
4343        index: u64,
4344    ) -> Result<Option<orchard::tree::MerkleHashOrchard>, ShardTreeError<Self::Error>>;
4345
4346    /// Evaluates the given callback with the Ironwood note commitment tree
4347    /// maintained by the wallet, if this backend has one.
4348    ///
4349    /// The default implementation reports that no Ironwood tree is available.
4350    /// Backends that track Ironwood note commitments should override this and
4351    /// provide their separate Ironwood tree.
4352    #[cfg(feature = "orchard")]
4353    fn with_ironwood_tree_mut<F, A, E>(&mut self, _callback: F) -> Result<Option<A>, E>
4354    where
4355        for<'a> F: FnMut(
4356            &'a mut ShardTree<
4357                Self::OrchardShardStore<'a>,
4358                { ORCHARD_SHARD_HEIGHT * 2 },
4359                ORCHARD_SHARD_HEIGHT,
4360            >,
4361        ) -> Result<A, E>,
4362        E: From<ShardTreeError<Self::Error>>,
4363    {
4364        Ok(None)
4365    }
4366
4367    /// Adds a sequence of Ironwood note commitment tree subtree roots to the data store, if this
4368    /// backend tracks an Ironwood tree.
4369    ///
4370    /// Each such value should be the Merkle root of a subtree of the Ironwood note commitment tree
4371    /// containing 2^[`ORCHARD_SHARD_HEIGHT`] note commitments; Ironwood shares the Orchard note
4372    /// commitment tree's shape, so the same shard height applies.
4373    ///
4374    /// The default implementation is a no-op, for backends that do not track an Ironwood tree
4375    /// (mirroring [`WalletCommitmentTrees::with_ironwood_tree_mut`]). Backends that track Ironwood
4376    /// note commitments should override this.
4377    #[cfg(feature = "orchard")]
4378    fn put_ironwood_subtree_roots(
4379        &mut self,
4380        _start_index: u64,
4381        _roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
4382    ) -> Result<(), ShardTreeError<Self::Error>> {
4383        Ok(())
4384    }
4385
4386    /// Returns the stored root hash of the completed Ironwood subtree with the given
4387    /// index, or `Ok(None)` if no root is recorded for that subtree (in particular, if
4388    /// this backend does not track an Ironwood tree — the default implementation).
4389    #[cfg(feature = "orchard")]
4390    fn get_ironwood_subtree_root(
4391        &mut self,
4392        _index: u64,
4393    ) -> Result<Option<orchard::tree::MerkleHashOrchard>, ShardTreeError<Self::Error>> {
4394        Ok(None)
4395    }
4396
4397    /// Applies a batch of changes — shards, an optional replacement tree cap, and a
4398    /// checkpoint delta — to the wallet's Sapling note commitment tree.
4399    ///
4400    /// `shards` must be in ascending shard-index order; stores may reject sequences that
4401    /// would leave gaps in the tree. Checkpoint removals are applied before additions, so
4402    /// that a checkpoint whose data has changed may appear in both lists.
4403    ///
4404    /// This is intended for wallet stores that accumulate note commitment tree updates
4405    /// outside the backing store (for example, in an in-memory tree) and flush them in
4406    /// batches. The default implementation applies the changes through
4407    /// [`WalletCommitmentTrees::with_sapling_tree_mut`].
4408    fn put_sapling_shards(
4409        &mut self,
4410        shards: &[shardtree::LocatedPrunableTree<sapling::Node>],
4411        cap: Option<&shardtree::PrunableTree<sapling::Node>>,
4412        checkpoints_remove: &[BlockHeight],
4413        checkpoints_add: &[(BlockHeight, shardtree::store::Checkpoint)],
4414    ) -> Result<(), ShardTreeError<Self::Error>> {
4415        self.with_sapling_tree_mut(|tree| {
4416            apply_tree_changes(tree, shards, cap, checkpoints_remove, checkpoints_add)
4417        })
4418    }
4419
4420    /// Applies a batch of changes — shards, an optional replacement tree cap, and a
4421    /// checkpoint delta — to the wallet's Orchard note commitment tree.
4422    ///
4423    /// `shards` must be in ascending shard-index order; stores may reject sequences that
4424    /// would leave gaps in the tree. Checkpoint removals are applied before additions, so
4425    /// that a checkpoint whose data has changed may appear in both lists.
4426    ///
4427    /// This is intended for wallet stores that accumulate note commitment tree updates
4428    /// outside the backing store (for example, in an in-memory tree) and flush them in
4429    /// batches. The default implementation applies the changes through
4430    /// [`WalletCommitmentTrees::with_orchard_tree_mut`].
4431    #[cfg(feature = "orchard")]
4432    fn put_orchard_shards(
4433        &mut self,
4434        shards: &[shardtree::LocatedPrunableTree<orchard::tree::MerkleHashOrchard>],
4435        cap: Option<&shardtree::PrunableTree<orchard::tree::MerkleHashOrchard>>,
4436        checkpoints_remove: &[BlockHeight],
4437        checkpoints_add: &[(BlockHeight, shardtree::store::Checkpoint)],
4438    ) -> Result<(), ShardTreeError<Self::Error>> {
4439        self.with_orchard_tree_mut(|tree| {
4440            apply_tree_changes(tree, shards, cap, checkpoints_remove, checkpoints_add)
4441        })
4442    }
4443
4444    /// Applies a batch of changes — shards, an optional replacement tree cap, and a
4445    /// checkpoint delta — to the wallet's Ironwood note commitment tree, if this backend
4446    /// tracks one.
4447    ///
4448    /// `shards` must be in ascending shard-index order; stores may reject sequences that
4449    /// would leave gaps in the tree. Checkpoint removals are applied before additions, so
4450    /// that a checkpoint whose data has changed may appear in both lists.
4451    ///
4452    /// The default implementation applies the changes through
4453    /// [`WalletCommitmentTrees::with_ironwood_tree_mut`]; for backends that do not track an
4454    /// Ironwood tree (see that method's documentation), the changes are ignored.
4455    #[cfg(feature = "orchard")]
4456    fn put_ironwood_shards(
4457        &mut self,
4458        shards: &[shardtree::LocatedPrunableTree<orchard::tree::MerkleHashOrchard>],
4459        cap: Option<&shardtree::PrunableTree<orchard::tree::MerkleHashOrchard>>,
4460        checkpoints_remove: &[BlockHeight],
4461        checkpoints_add: &[(BlockHeight, shardtree::store::Checkpoint)],
4462    ) -> Result<(), ShardTreeError<Self::Error>> {
4463        self.with_ironwood_tree_mut(|tree| {
4464            apply_tree_changes(tree, shards, cap, checkpoints_remove, checkpoints_add)
4465        })?;
4466        Ok(())
4467    }
4468
4469    /// Releases all retained ("anchor") checkpoints with height strictly less than `max_height`
4470    /// from the wallet's note commitment trees, allowing them to be pruned normally.
4471    ///
4472    /// Anchor checkpoints are established during scanning (and may be created directly via
4473    /// [`ShardTree::ensure_retained`]); they are otherwise exempt from automatic pruning of excess
4474    /// checkpoints. This releases the retention of those that have aged below `max_height` in the
4475    /// Sapling and (when the `orchard` feature is enabled) the Orchard and Ironwood trees.
4476    fn remove_retained_checkpoints_below(
4477        &mut self,
4478        max_height: BlockHeight,
4479    ) -> Result<(), ShardTreeError<Self::Error>> {
4480        self.with_sapling_tree_mut(|tree| {
4481            for height in tree
4482                .store()
4483                .retained_checkpoints()
4484                .map_err(ShardTreeError::Storage)?
4485            {
4486                if height < max_height {
4487                    tree.remove_retained_checkpoint(&height)?;
4488                }
4489            }
4490            Ok::<_, ShardTreeError<Self::Error>>(())
4491        })?;
4492
4493        #[cfg(feature = "orchard")]
4494        self.with_orchard_tree_mut(|tree| {
4495            for height in tree
4496                .store()
4497                .retained_checkpoints()
4498                .map_err(ShardTreeError::Storage)?
4499            {
4500                if height < max_height {
4501                    tree.remove_retained_checkpoint(&height)?;
4502                }
4503            }
4504            Ok::<_, ShardTreeError<Self::Error>>(())
4505        })?;
4506
4507        // A backend that does not track an Ironwood tree returns `None` here and is left unchanged.
4508        #[cfg(feature = "orchard")]
4509        self.with_ironwood_tree_mut(|tree| {
4510            for height in tree
4511                .store()
4512                .retained_checkpoints()
4513                .map_err(ShardTreeError::Storage)?
4514            {
4515                if height < max_height {
4516                    tree.remove_retained_checkpoint(&height)?;
4517                }
4518            }
4519            Ok::<_, ShardTreeError<Self::Error>>(())
4520        })?;
4521
4522        Ok(())
4523    }
4524}
4525
4526/// Property tests for the [`Balance`] bucket arithmetic.
4527///
4528/// These pin the accounting semantics the locked-value bucket joined: every bucket except
4529/// `uneconomic_value` participates in [`Balance::total`] and in the shared overflow guard,
4530/// while `uneconomic_value` is guarded only against its own overflow and never contributes
4531/// to the total.
4532#[cfg(test)]
4533mod balance_tests {
4534    use proptest::prelude::*;
4535    use zcash_protocol::value::{BalanceError, MAX_MONEY, Zatoshis};
4536
4537    use super::Balance;
4538
4539    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
4540    enum Bucket {
4541        Spendable = 0,
4542        Locked = 1,
4543        PendingChange = 2,
4544        PendingSpendable = 3,
4545        Uneconomic = 4,
4546    }
4547    use Bucket::*;
4548
4549    const ALL_BUCKETS: [Bucket; 5] = [
4550        Spendable,
4551        Locked,
4552        PendingChange,
4553        PendingSpendable,
4554        Uneconomic,
4555    ];
4556    /// The buckets that participate in `Balance::total` and its overflow guard.
4557    const TOTAL_BUCKETS: [Bucket; 4] = [Spendable, Locked, PendingChange, PendingSpendable];
4558
4559    fn apply(balance: &mut Balance, bucket: Bucket, value: Zatoshis) -> Result<(), BalanceError> {
4560        match bucket {
4561            Spendable => balance.add_spendable_value(value),
4562            Locked => balance.add_locked_value(value),
4563            PendingChange => balance.add_pending_change_value(value),
4564            PendingSpendable => balance.add_pending_spendable_value(value),
4565            Uneconomic => balance.add_uneconomic_value(value),
4566        }
4567    }
4568
4569    fn get(balance: &Balance, bucket: Bucket) -> Zatoshis {
4570        match bucket {
4571            Spendable => balance.spendable_value(),
4572            Locked => balance.locked_value(),
4573            PendingChange => balance.change_pending_confirmation(),
4574            PendingSpendable => balance.value_pending_spendability(),
4575            Uneconomic => balance.uneconomic_value(),
4576        }
4577    }
4578
4579    fn arb_bucket() -> impl Strategy<Value = Bucket> {
4580        prop_oneof![
4581            Just(Spendable),
4582            Just(Locked),
4583            Just(PendingChange),
4584            Just(PendingSpendable),
4585            Just(Uneconomic),
4586        ]
4587    }
4588
4589    /// A bucket and a value to add to it. Values are mostly small (so most sequences stay
4590    /// within `MAX_MONEY`) with occasional near-cap draws to exercise the overflow guards.
4591    fn arb_add() -> impl Strategy<Value = (Bucket, u64)> {
4592        (
4593            arb_bucket(),
4594            prop_oneof![
4595                3 => 0u64..=1_000_000,
4596                1 => 0u64..=MAX_MONEY,
4597            ],
4598        )
4599    }
4600
4601    proptest! {
4602        /// Bucket adds succeed exactly while their overflow guard permits, mutate only the
4603        /// requested bucket, and leave the balance untouched on failure. `total()` is always
4604        /// the sum of the four participating buckets. (In particular this establishes that
4605        /// the `unwrap` inside each guarded add is unreachable.)
4606        #[test]
4607        fn add_total_consistency(adds in proptest::collection::vec(arb_add(), 0..12)) {
4608            let mut balance = Balance::ZERO;
4609            // The model: per-bucket totals, indexed by bucket discriminant.
4610            let mut model = [0u64; 5];
4611
4612            for (bucket, v) in adds {
4613                let value = Zatoshis::from_u64(v).unwrap();
4614                let before = balance;
4615                let result = apply(&mut balance, bucket, value);
4616
4617                let total: u64 = TOTAL_BUCKETS.iter().map(|b| model[*b as usize]).sum();
4618                let expect_ok = match bucket {
4619                    Uneconomic => model[Uneconomic as usize] + v <= MAX_MONEY,
4620                    _ => total + v <= MAX_MONEY,
4621                };
4622                if expect_ok {
4623                    prop_assert!(result.is_ok());
4624                    model[bucket as usize] += v;
4625                } else {
4626                    prop_assert!(result.is_err());
4627                    prop_assert_eq!(
4628                        balance, before,
4629                        "a failed add must leave the balance unchanged"
4630                    );
4631                }
4632
4633                let total: u64 = TOTAL_BUCKETS.iter().map(|b| model[*b as usize]).sum();
4634                prop_assert_eq!(balance.total(), Zatoshis::from_u64(total).unwrap());
4635                for b in ALL_BUCKETS {
4636                    prop_assert_eq!(
4637                        get(&balance, b),
4638                        Zatoshis::from_u64(model[b as usize]).unwrap()
4639                    );
4640                }
4641            }
4642        }
4643
4644        /// `Balance + Balance` is componentwise addition: it succeeds exactly when the
4645        /// combined total and the combined uneconomic value each remain within `MAX_MONEY`,
4646        /// and on success every bucket of the sum is the sum of the corresponding buckets.
4647        #[test]
4648        fn balance_addition_is_componentwise(
4649            a in proptest::collection::vec(arb_add(), 0..6),
4650            b in proptest::collection::vec(arb_add(), 0..6),
4651        ) {
4652            let build = |adds: &[(Bucket, u64)]| {
4653                let mut balance = Balance::ZERO;
4654                for (bucket, v) in adds {
4655                    let _ = apply(&mut balance, *bucket, Zatoshis::from_u64(*v).unwrap());
4656                }
4657                balance
4658            };
4659            let ba = build(&a);
4660            let bb = build(&b);
4661
4662            let combined_total = u64::from(ba.total()) + u64::from(bb.total());
4663            let combined_uneconomic =
4664                u64::from(ba.uneconomic_value()) + u64::from(bb.uneconomic_value());
4665            match ba + bb {
4666                Ok(sum) => {
4667                    prop_assert!(combined_total <= MAX_MONEY);
4668                    prop_assert!(combined_uneconomic <= MAX_MONEY);
4669                    for bucket in ALL_BUCKETS {
4670                        prop_assert_eq!(
4671                            u64::from(get(&sum, bucket)),
4672                            u64::from(get(&ba, bucket)) + u64::from(get(&bb, bucket))
4673                        );
4674                    }
4675                    prop_assert_eq!(u64::from(sum.total()), combined_total);
4676                }
4677                Err(_) => {
4678                    prop_assert!(
4679                        combined_total > MAX_MONEY || combined_uneconomic > MAX_MONEY,
4680                        "balance addition failed although no component overflows"
4681                    );
4682                }
4683            }
4684        }
4685    }
4686}
4687
4688#[cfg(test)]
4689mod tests {
4690    use incrementalmerkletree::{
4691        Address as TreeAddress, Hashable, Level, Marking, Position, Retention,
4692    };
4693    use shardtree::store::{Checkpoint, memory::MemoryShardStore};
4694    use zcash_keys::{
4695        address::{Address, UnifiedAddress},
4696        keys::UnifiedAddressRequest,
4697    };
4698
4699    use super::*;
4700
4701    #[cfg(feature = "orchard")]
4702    use crate::data_api::error::FindAccountForAddressError;
4703    use crate::data_api::testing::{
4704        MockWalletDb, pool::ShieldedPoolTester, sapling::SaplingPoolTester,
4705    };
4706
4707    use transparent::address::TransparentAddress;
4708    use zip32::DiversifierIndex;
4709
4710    #[test]
4711    fn put_sapling_shards_flushes_through_the_interface() {
4712        let mut db = MockWalletDb::new(zcash_protocol::consensus::Network::TestNetwork);
4713
4714        // Build a shard-rooted subtree the same way `put_blocks` does, with a checkpoint on
4715        // the final leaf.
4716        let leaf = <sapling::Node as Hashable>::empty_leaf();
4717        let checkpoint_height = BlockHeight::from(3);
4718        let commitments = (0u64..4).map(|i| {
4719            (
4720                leaf,
4721                if i == 3 {
4722                    Retention::Checkpoint {
4723                        id: checkpoint_height,
4724                        marking: Marking::None,
4725                    }
4726                } else {
4727                    Retention::Ephemeral
4728                },
4729            )
4730        });
4731        let built = shardtree::LocatedTree::from_iter(
4732            Position::from(0)..Position::from(4),
4733            Level::from(SAPLING_SHARD_HEIGHT),
4734            commitments,
4735        )
4736        .expect("commitments produce a subtree");
4737        let checkpoints_add = built
4738            .checkpoints
4739            .iter()
4740            .map(|(height, position)| (*height, Checkpoint::at_position(*position)))
4741            .collect::<Vec<_>>();
4742
4743        db.put_sapling_shards(&[built.subtree], None, &[], &checkpoints_add)
4744            .expect("bulk flush succeeds");
4745
4746        // The shard and checkpoint are visible through the standard tree access path.
4747        db.with_sapling_tree_mut(|tree| {
4748            assert!(
4749                tree.store()
4750                    .get_shard(TreeAddress::from_parts(
4751                        Level::from(SAPLING_SHARD_HEIGHT),
4752                        0
4753                    ))
4754                    .map_err(ShardTreeError::Storage)?
4755                    .is_some()
4756            );
4757            assert_eq!(
4758                tree.store()
4759                    .max_checkpoint_id()
4760                    .map_err(ShardTreeError::Storage)?,
4761                Some(checkpoint_height)
4762            );
4763            Ok::<_, ShardTreeError<_>>(())
4764        })
4765        .expect("tree reads succeed");
4766
4767        // Removals are applied before additions, so a checkpoint set can be replaced in a
4768        // single call.
4769        let new_height = BlockHeight::from(7);
4770        db.put_sapling_shards(
4771            &[],
4772            None,
4773            &[checkpoint_height],
4774            &[(new_height, Checkpoint::tree_empty())],
4775        )
4776        .expect("checkpoint replacement succeeds");
4777
4778        db.with_sapling_tree_mut(|tree| {
4779            assert_eq!(
4780                tree.store()
4781                    .max_checkpoint_id()
4782                    .map_err(ShardTreeError::Storage)?,
4783                Some(new_height)
4784            );
4785            assert_eq!(
4786                tree.store()
4787                    .checkpoint_count()
4788                    .map_err(ShardTreeError::Storage)?,
4789                1
4790            );
4791            Ok::<_, ShardTreeError<_>>(())
4792        })
4793        .expect("tree reads succeed");
4794    }
4795
4796    /// Exercises [`apply_tree_changes`] — the shared implementation of the `put_*_shards`
4797    /// provided methods — directly over a [`MemoryShardStore`] of the given node type.
4798    ///
4799    /// [`MemoryShardStore`]: shardtree::store::memory::MemoryShardStore
4800    fn check_apply_tree_changes<H>()
4801    where
4802        H: incrementalmerkletree::Hashable + Clone + PartialEq + core::fmt::Debug,
4803    {
4804        let mut tree: ShardTree<
4805            MemoryShardStore<H, BlockHeight>,
4806            { SAPLING_SHARD_HEIGHT * 2 },
4807            SAPLING_SHARD_HEIGHT,
4808        > = ShardTree::new(MemoryShardStore::empty(), 100);
4809
4810        // Build a shard-rooted subtree the same way `put_blocks` does, with a checkpoint on
4811        // the final leaf.
4812        let leaf = H::empty_leaf();
4813        let checkpoint_height = BlockHeight::from(3);
4814        let commitments = (0u64..4).map(|i| {
4815            (
4816                leaf.clone(),
4817                if i == 3 {
4818                    Retention::Checkpoint {
4819                        id: checkpoint_height,
4820                        marking: Marking::None,
4821                    }
4822                } else {
4823                    Retention::Ephemeral
4824                },
4825            )
4826        });
4827        let built = shardtree::LocatedTree::from_iter(
4828            Position::from(0)..Position::from(4),
4829            Level::from(SAPLING_SHARD_HEIGHT),
4830            commitments,
4831        )
4832        .expect("commitments produce a subtree");
4833        let checkpoints_add = built
4834            .checkpoints
4835            .iter()
4836            .map(|(height, position)| (*height, Checkpoint::at_position(*position)))
4837            .collect::<Vec<_>>();
4838
4839        apply_tree_changes(&mut tree, &[built.subtree], None, &[], &checkpoints_add)
4840            .expect("bulk flush succeeds");
4841
4842        assert!(
4843            tree.store()
4844                .get_shard(TreeAddress::from_parts(
4845                    Level::from(SAPLING_SHARD_HEIGHT),
4846                    0
4847                ))
4848                .expect("shard read succeeds")
4849                .is_some()
4850        );
4851        assert_eq!(
4852            tree.store()
4853                .max_checkpoint_id()
4854                .expect("checkpoint read succeeds"),
4855            Some(checkpoint_height)
4856        );
4857
4858        // Removals are applied before additions, so a checkpoint set can be replaced in a
4859        // single call.
4860        let new_height = BlockHeight::from(7);
4861        apply_tree_changes(
4862            &mut tree,
4863            &[],
4864            None,
4865            &[checkpoint_height],
4866            &[(new_height, Checkpoint::tree_empty())],
4867        )
4868        .expect("checkpoint replacement succeeds");
4869
4870        assert_eq!(
4871            tree.store()
4872                .max_checkpoint_id()
4873                .expect("checkpoint read succeeds"),
4874            Some(new_height)
4875        );
4876        assert_eq!(
4877            tree.store()
4878                .checkpoint_count()
4879                .expect("checkpoint read succeeds"),
4880            1
4881        );
4882    }
4883
4884    #[test]
4885    fn apply_tree_changes_supports_every_pool_node_type() {
4886        check_apply_tree_changes::<sapling::Node>();
4887        // Orchard and Ironwood both use `MerkleHashOrchard` trees of the same shape.
4888        #[cfg(feature = "orchard")]
4889        check_apply_tree_changes::<orchard::tree::MerkleHashOrchard>();
4890    }
4891
4892    #[cfg(feature = "orchard")]
4893    #[test]
4894    fn put_ironwood_shards_is_ignored_without_an_ironwood_tree() {
4895        // `MockWalletDb` does not track an Ironwood tree, so the default
4896        // `with_ironwood_tree_mut` reports no tree and the changes are ignored rather than
4897        // returning an error.
4898        let mut db = MockWalletDb::new(zcash_protocol::consensus::Network::TestNetwork);
4899        db.put_ironwood_shards(
4900            &[],
4901            None,
4902            &[],
4903            &[(BlockHeight::from(1), Checkpoint::tree_empty())],
4904        )
4905        .expect("ignored on backends without an Ironwood tree");
4906    }
4907
4908    #[test]
4909    fn account_meta_totals_include_ironwood() {
4910        let meta = AccountMeta::new(
4911            Some(PoolMeta::new(2, Zatoshis::const_from_u64(200))),
4912            Some(PoolMeta::new(3, Zatoshis::const_from_u64(300))),
4913            Some(PoolMeta::new(5, Zatoshis::const_from_u64(500))),
4914        );
4915        assert_eq!(meta.note_count(ShieldedPool::Ironwood), Some(5));
4916        assert_eq!(meta.total_note_count(), Some(10));
4917        assert_eq!(meta.total_value(), Some(Zatoshis::const_from_u64(1000)));
4918
4919        // With metadata for only the Ironwood pool, the totals reflect that pool alone.
4920        let ironwood_only = AccountMeta::new(
4921            None,
4922            None,
4923            Some(PoolMeta::new(4, Zatoshis::const_from_u64(400))),
4924        );
4925        assert_eq!(ironwood_only.note_count(ShieldedPool::Ironwood), Some(4));
4926        assert_eq!(ironwood_only.total_note_count(), Some(4));
4927        assert_eq!(
4928            ironwood_only.total_value(),
4929            Some(Zatoshis::const_from_u64(400))
4930        );
4931    }
4932
4933    fn derived_source() -> AddressSource {
4934        AddressSource::Derived {
4935            diversifier_index: DiversifierIndex::default(),
4936            #[cfg(feature = "transparent-inputs")]
4937            transparent_key_scope: None,
4938        }
4939    }
4940
4941    fn address_info_of(address: Address) -> AddressInfo {
4942        AddressInfo::from_parts(address, derived_source())
4943            .expect("test address metadata must be valid")
4944    }
4945
4946    fn transparent_address_for_tag(tag: u8) -> TransparentAddress {
4947        TransparentAddress::PublicKeyHash([tag; 20])
4948    }
4949
4950    fn sapling_address_for_tag(tag: u8) -> sapling::PaymentAddress {
4951        match SaplingPoolTester::sk_default_address(&SaplingPoolTester::sk(&[tag; 32])) {
4952            Address::Sapling(pa) => pa,
4953            other => panic!("expected Sapling address, got {other:?}"),
4954        }
4955    }
4956
4957    fn unified_account_with(
4958        transparent: Option<TransparentAddress>,
4959        sapling: Option<sapling::PaymentAddress>,
4960        #[cfg(feature = "orchard")] orchard: Option<orchard::Address>,
4961    ) -> Address {
4962        UnifiedAddress::from_receivers(
4963            #[cfg(feature = "orchard")]
4964            Some(orchard).flatten(),
4965            Some(sapling).flatten(),
4966            transparent,
4967        )
4968        .expect("test UA must be valid")
4969        .into()
4970    }
4971
4972    #[test]
4973    fn find_account_for_transparent_address_returns_matching_account() {
4974        let wallet = MockWalletDb::from_account_addresses(
4975            zcash_protocol::consensus::Network::MainNetwork,
4976            [
4977                (
4978                    1,
4979                    vec![address_info_of(Address::Transparent(
4980                        transparent_address_for_tag(1),
4981                    ))],
4982                ),
4983                (
4984                    2,
4985                    vec![address_info_of(Address::Transparent(
4986                        transparent_address_for_tag(2),
4987                    ))],
4988                ),
4989            ],
4990        );
4991        let result = wallet.find_account_for_address(
4992            &zcash_protocol::consensus::Network::MainNetwork,
4993            &Address::Transparent(transparent_address_for_tag(1)),
4994        );
4995        assert_eq!(result.unwrap(), Some(1));
4996    }
4997
4998    #[test]
4999    fn find_account_for_transparent_receiver_in_unified_address_returns_matching_account() {
5000        let transparent = transparent_address_for_tag(1);
5001        let sapling_address = sapling_address_for_tag(11);
5002
5003        #[cfg(feature = "orchard")]
5004        {
5005            let wallet = MockWalletDb::from_account_addresses(
5006                zcash_protocol::consensus::Network::MainNetwork,
5007                [(
5008                    1,
5009                    vec![address_info_of(unified_account_with(
5010                        Some(transparent),
5011                        Some(sapling_address),
5012                        None,
5013                    ))],
5014                )],
5015            );
5016            let result = wallet.find_account_for_address(
5017                &zcash_protocol::consensus::Network::MainNetwork,
5018                &Address::Transparent(transparent),
5019            );
5020            assert_eq!(result.unwrap(), Some(1));
5021        }
5022        #[cfg(not(feature = "orchard"))]
5023        {
5024            let wallet = MockWalletDb::from_account_addresses(
5025                zcash_protocol::consensus::Network::MainNetwork,
5026                [(
5027                    1,
5028                    vec![address_info_of(unified_account_with(
5029                        Some(transparent),
5030                        Some(sapling_address),
5031                    ))],
5032                )],
5033            );
5034            let result = wallet.find_account_for_address(
5035                &zcash_protocol::consensus::Network::MainNetwork,
5036                &Address::Transparent(transparent),
5037            );
5038            assert_eq!(result.unwrap(), Some(1));
5039        }
5040    }
5041
5042    #[test]
5043    fn find_account_for_address_returns_none_when_simple_address_is_unknown() {
5044        let address = Address::Transparent(transparent_address_for_tag(1));
5045        let wallet = MockWalletDb::from_account_addresses(
5046            zcash_protocol::consensus::Network::MainNetwork,
5047            [(1, vec![address_info_of(address)])],
5048        );
5049
5050        let other_address = Address::Transparent(transparent_address_for_tag(9));
5051        let result = wallet.find_account_for_address(
5052            &zcash_protocol::consensus::Network::MainNetwork,
5053            &other_address,
5054        );
5055
5056        assert_eq!(result.unwrap(), None);
5057    }
5058
5059    fn test_ufvk(seed_tag: u8) -> zcash_keys::keys::UnifiedFullViewingKey {
5060        zcash_keys::keys::UnifiedSpendingKey::from_seed(
5061            &zcash_protocol::consensus::Network::MainNetwork,
5062            &[seed_tag; 32],
5063            zip32::AccountId::ZERO,
5064        )
5065        .expect("valid seed")
5066        .to_unified_full_viewing_key()
5067    }
5068
5069    #[test]
5070    fn find_account_for_unified_address_returns_account_when_receivers_map_to_same_account() {
5071        let ufvk = test_ufvk(1);
5072        let wallet = MockWalletDb::from_account_ufvks(
5073            zcash_protocol::consensus::Network::MainNetwork,
5074            [(1, ufvk.clone())],
5075        );
5076
5077        let (ua, _) = ufvk
5078            .default_address(UnifiedAddressRequest::AllAvailableKeys)
5079            .expect("default address must be derivable");
5080
5081        let result = wallet.find_account_for_address(
5082            &zcash_protocol::consensus::Network::MainNetwork,
5083            &Address::Unified(ua),
5084        );
5085
5086        assert_eq!(result.unwrap(), Some(1));
5087    }
5088
5089    #[test]
5090    fn find_account_for_unified_address_returns_none_when_no_receiver_matches() {
5091        let wallet = MockWalletDb::from_account_ufvks(
5092            zcash_protocol::consensus::Network::MainNetwork,
5093            [(1, test_ufvk(1))],
5094        );
5095
5096        // A UA derived from a different seed — no account in the wallet owns any of its
5097        // shielded receivers.
5098        let (ua_from_other_seed, _) = test_ufvk(99)
5099            .default_address(UnifiedAddressRequest::AllAvailableKeys)
5100            .expect("default address must be derivable");
5101
5102        let result = wallet.find_account_for_address(
5103            &zcash_protocol::consensus::Network::MainNetwork,
5104            &Address::Unified(ua_from_other_seed),
5105        );
5106
5107        assert_eq!(result.unwrap(), None);
5108    }
5109
5110    #[test]
5111    fn find_account_for_sapling_address_resolves_via_uivk_algebra_when_not_previously_exposed() {
5112        // A bare Sapling address derivable from an account's UIVK must resolve even when the
5113        // wallet has never stored (and therefore never "exposed") that address.
5114        let ufvk = test_ufvk(1);
5115        let wallet = MockWalletDb::from_account_ufvks(
5116            zcash_protocol::consensus::Network::MainNetwork,
5117            [(1, ufvk.clone())],
5118        );
5119
5120        let (ua, _) = ufvk
5121            .default_address(UnifiedAddressRequest::AllAvailableKeys)
5122            .expect("default address must be derivable");
5123        let sapling_pa = *ua.sapling().expect("sapling receiver");
5124
5125        // `wallet` has no stored addresses: only the account's UFVK. The list_addresses scan
5126        // would therefore miss this address; only the synthesized-UA algebraic path can
5127        // resolve it.
5128        let result = wallet.find_account_for_address(
5129            &zcash_protocol::consensus::Network::MainNetwork,
5130            &Address::Sapling(sapling_pa),
5131        );
5132
5133        assert_eq!(result.unwrap(), Some(1));
5134    }
5135
5136    #[test]
5137    fn find_account_for_address_returns_none_for_empty_wallet() {
5138        let wallet = MockWalletDb::from_account_addresses(
5139            zcash_protocol::consensus::Network::MainNetwork,
5140            std::iter::empty(),
5141        );
5142
5143        let result = wallet.find_account_for_address(
5144            &zcash_protocol::consensus::Network::MainNetwork,
5145            &Address::Transparent(transparent_address_for_tag(1)),
5146        );
5147        assert_eq!(result.unwrap(), None);
5148
5149        let result = wallet.find_account_for_address(
5150            &zcash_protocol::consensus::Network::MainNetwork,
5151            &Address::Sapling(sapling_address_for_tag(1)),
5152        );
5153        assert_eq!(result.unwrap(), None);
5154    }
5155
5156    #[cfg(feature = "orchard")]
5157    #[test]
5158    fn find_account_for_unified_address_errors_when_receivers_map_to_different_accounts() {
5159        let ufvk1 = test_ufvk(1);
5160        let ufvk2 = test_ufvk(2);
5161        let wallet = MockWalletDb::from_account_ufvks(
5162            zcash_protocol::consensus::Network::MainNetwork,
5163            [(1, ufvk1.clone()), (2, ufvk2.clone())],
5164        );
5165
5166        let (ua1, _) = ufvk1
5167            .default_address(UnifiedAddressRequest::AllAvailableKeys)
5168            .expect("default address must be derivable");
5169        let (ua2, _) = ufvk2
5170            .default_address(UnifiedAddressRequest::AllAvailableKeys)
5171            .expect("default address must be derivable");
5172
5173        // A frankenstein UA whose Sapling receiver is from account 1 and whose Orchard
5174        // receiver is from account 2.
5175        let frankenstein = UnifiedAddress::from_receivers(
5176            Some(ua2.orchard().copied().expect("orchard receiver")),
5177            Some(ua1.sapling().copied().expect("sapling receiver")),
5178            None,
5179        )
5180        .expect("sapling+orchard UA must be valid");
5181
5182        let result = wallet.find_account_for_address(
5183            &zcash_protocol::consensus::Network::MainNetwork,
5184            &Address::Unified(frankenstein),
5185        );
5186
5187        assert!(matches!(
5188            result,
5189            Err(FindAccountForAddressError::UnifiedAddressConflict)
5190        ));
5191    }
5192
5193    /// Each unshielded mutator updates only its own bucket, and transparent mutations leave the
5194    /// shielded aggregates untouched.
5195    #[test]
5196    fn account_balance_unshielded_split_mutators() {
5197        let mut balance = AccountBalance::ZERO;
5198
5199        let regular_value = Zatoshis::const_from_u64(100_000);
5200        let coinbase_value = Zatoshis::const_from_u64(50_000);
5201
5202        balance
5203            .with_unshielded_regular_balance_mut(|bal| bal.add_spendable_value(regular_value))
5204            .unwrap();
5205        balance
5206            .with_unshielded_coinbase_balance_mut(|bal| {
5207                bal.add_pending_spendable_value(coinbase_value)
5208            })
5209            .unwrap();
5210
5211        // The regular bucket contains only the regular value.
5212        assert_eq!(
5213            balance.unshielded_regular_balance().spendable_value(),
5214            regular_value
5215        );
5216        assert_eq!(balance.unshielded_regular_balance().total(), regular_value);
5217        assert_eq!(
5218            balance
5219                .unshielded_regular_balance()
5220                .value_pending_spendability(),
5221            Zatoshis::ZERO
5222        );
5223
5224        // The coinbase bucket contains only the coinbase value, as pending.
5225        assert_eq!(
5226            balance.unshielded_coinbase_balance().spendable_value(),
5227            Zatoshis::ZERO
5228        );
5229        assert_eq!(
5230            balance
5231                .unshielded_coinbase_balance()
5232                .value_pending_spendability(),
5233            coinbase_value
5234        );
5235        assert_eq!(
5236            balance.unshielded_coinbase_balance().total(),
5237            coinbase_value
5238        );
5239
5240        // The shielded-only aggregates are unaffected by transparent mutations.
5241        assert_eq!(balance.spendable_value(), Zatoshis::ZERO);
5242        assert_eq!(balance.change_pending_confirmation(), Zatoshis::ZERO);
5243        assert_eq!(balance.value_pending_spendability(), Zatoshis::ZERO);
5244        assert_eq!(balance.sapling_balance(), &Balance::ZERO);
5245        assert_eq!(balance.orchard_balance(), &Balance::ZERO);
5246        assert_eq!(balance.ironwood_balance(), &Balance::ZERO);
5247    }
5248
5249    /// `unshielded_balance` returns the sum of the regular and coinbase buckets, and the
5250    /// account-level aggregates include both buckets.
5251    #[test]
5252    fn account_balance_unshielded_balance_is_sum() {
5253        let mut balance = AccountBalance::ZERO;
5254
5255        let regular_spendable = Zatoshis::const_from_u64(100_000);
5256        let regular_dust = Zatoshis::const_from_u64(100);
5257        let coinbase_pending = Zatoshis::const_from_u64(625_000_000);
5258        let coinbase_dust = Zatoshis::const_from_u64(42);
5259
5260        balance
5261            .with_unshielded_regular_balance_mut(|bal| {
5262                bal.add_spendable_value(regular_spendable)?;
5263                bal.add_uneconomic_value(regular_dust)
5264            })
5265            .unwrap();
5266        balance
5267            .with_unshielded_coinbase_balance_mut(|bal| {
5268                bal.add_pending_spendable_value(coinbase_pending)?;
5269                bal.add_uneconomic_value(coinbase_dust)
5270            })
5271            .unwrap();
5272
5273        // The by-value combined balance is the field-wise sum of both buckets.
5274        let combined = balance.unshielded_balance();
5275        assert_eq!(
5276            combined,
5277            (*balance.unshielded_regular_balance() + *balance.unshielded_coinbase_balance())
5278                .unwrap()
5279        );
5280        assert_eq!(combined.spendable_value(), regular_spendable);
5281        assert_eq!(combined.value_pending_spendability(), coinbase_pending);
5282        assert_eq!(
5283            combined.uneconomic_value(),
5284            (regular_dust + coinbase_dust).unwrap()
5285        );
5286
5287        // The deprecated accessor reports the sum of both buckets' totals.
5288        #[allow(deprecated)]
5289        let unshielded = balance.unshielded();
5290        assert_eq!(
5291            unshielded,
5292            (balance.unshielded_regular_balance().total()
5293                + balance.unshielded_coinbase_balance().total())
5294            .unwrap()
5295        );
5296
5297        // The account total and uneconomic value include both buckets. (`Balance::total`
5298        // excludes uneconomic value, so the dust does not appear in the account total.)
5299        assert_eq!(
5300            balance.total(),
5301            (regular_spendable + coinbase_pending).unwrap()
5302        );
5303        assert_eq!(
5304            balance.uneconomic_value(),
5305            (regular_dust + coinbase_dust).unwrap()
5306        );
5307    }
5308
5309    /// The `check_total` invariant rejects mutations that would cause the sum of the regular and
5310    /// coinbase transparent buckets to exceed `MAX_MONEY`.
5311    #[test]
5312    fn account_balance_unshielded_overflow_rejected() {
5313        let max_money = Zatoshis::const_from_u64(zcash_protocol::value::MAX_MONEY);
5314        let mut balance = AccountBalance::ZERO;
5315
5316        // Fill the regular bucket up to MAX_MONEY; this is fine on its own.
5317        balance
5318            .with_unshielded_regular_balance_mut(|bal| bal.add_spendable_value(max_money))
5319            .unwrap();
5320        assert_eq!(balance.total(), max_money);
5321
5322        // Any further value in the coinbase bucket must be rejected by the account-level
5323        // invariant check, even though the coinbase bucket does not overflow on its own.
5324        let result: Result<(), BalanceError> =
5325            balance.with_unshielded_coinbase_balance_mut(|bal| {
5326                bal.add_pending_spendable_value(Zatoshis::const_from_u64(1))
5327            });
5328        assert!(matches!(result, Err(BalanceError::Overflow)));
5329    }
5330}