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