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