Skip to main content

pepper_sync/
wallet.rs

1//! Module for wallet structs and types generated by the sync engine from block chain data or to track the wallet's
2//! sync status.
3//! The structs will be (or be transposed into) the fundamental wallet components for the wallet interfacing with this
4//! sync engine.
5
6use std::{
7    collections::{BTreeMap, BTreeSet},
8    convert::Infallible,
9    fmt::Debug,
10    marker::PhantomData,
11    ops::Range,
12    sync::atomic::{self, AtomicU8},
13};
14
15use incrementalmerkletree::Position;
16use orchard::tree::MerkleHashOrchard;
17use shardtree::{ShardTree, store::memory::MemoryShardStore};
18use tokio::sync::mpsc;
19use zcash_address::unified::ParseError;
20use zcash_keys::{address::UnifiedAddress, encoding::encode_payment_address};
21use zcash_primitives::{block::BlockHash, transaction::TxId};
22use zcash_protocol::{
23    PoolType, ShieldedPool,
24    consensus::{self, BlockHeight},
25    memo::Memo,
26    value::Zatoshis,
27};
28use zcash_transparent::address::Script;
29use zcash_transparent::bundle::OutPoint;
30
31use zingo_netutils::lightwallet_protocol::CompactBlock;
32use zingo_status::confirmation_status::ConfirmationStatus;
33
34use crate::{
35    client::FetchRequest,
36    error::{ServerError, SyncModeError},
37    keys::{self, KeyId, transparent::TransparentAddressId},
38    scan::compact_blocks::calculate_block_tree_bounds,
39    shardtree_ext::{CheckpointAppendOutcome, ShardTreeExt as _},
40    sync::{SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE, ScanPriority, ScanRange},
41    utils::{block, transaction},
42    witness,
43};
44
45pub mod traits;
46
47#[cfg(feature = "wallet_essentials")]
48pub mod serialization;
49
50/// A block, or a transaction in it, that scanning must reach. A target names a block height and, when a particular
51/// transaction is wanted, its txid. Two targets naming the same block height and txid are the same target.
52///
53/// The blocks around a target are prioritised for scanning: a narrow range by default, and the whole shard of each
54/// pool the target's transaction was found to hold notes of, so those notes become spendable.
55#[derive(Debug, Clone, Copy)]
56pub struct ScanTarget {
57    block_height: BlockHeight,
58    txid: Option<TxId>,
59    pools: ScanTargetPools,
60    origin: ScanTargetOrigin,
61}
62
63// Where a scan target came from, which decides what a transaction the server does not serve means.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub(crate) enum ScanTargetOrigin {
66    // Trial decryption, spend detection or transparent address discovery. A transaction the server does not serve
67    // is an inconsistency in the served data.
68    Internal,
69    // `crate::add_scan_targets`. A transaction the server does not serve, or a block height outside the chain, is a
70    // consumer error and the target is dropped.
71    External,
72}
73
74// The shielded pools a scan target's transaction was found to hold notes of, as bits of a byte.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
76pub(crate) struct ScanTargetPools(u8);
77
78impl ScanTargetPools {
79    const ALL_POOLS: [ShieldedPool; 3] = [
80        ShieldedPool::Sapling,
81        ShieldedPool::Orchard,
82        ShieldedPool::Ironwood,
83    ];
84
85    const fn bit(pool: ShieldedPool) -> u8 {
86        match pool {
87            ShieldedPool::Sapling => 1,
88            ShieldedPool::Orchard => 1 << 1,
89            ShieldedPool::Ironwood => 1 << 2,
90        }
91    }
92
93    #[cfg(feature = "wallet_essentials")]
94    pub(crate) fn from_bits(bits: u8) -> Option<Self> {
95        let known = Self::ALL_POOLS
96            .into_iter()
97            .fold(0, |known, pool| known | Self::bit(pool));
98        (bits & !known == 0).then_some(Self(bits))
99    }
100
101    #[cfg(feature = "wallet_essentials")]
102    pub(crate) fn bits(self) -> u8 {
103        self.0
104    }
105
106    pub(crate) fn with(self, pool: ShieldedPool) -> Self {
107        Self(self.0 | Self::bit(pool))
108    }
109
110    pub(crate) fn is_empty(self) -> bool {
111        self.0 == 0
112    }
113
114    pub(crate) fn iter(self) -> impl Iterator<Item = ShieldedPool> {
115        Self::ALL_POOLS
116            .into_iter()
117            .filter(move |&pool| self.0 & Self::bit(pool) != 0)
118    }
119}
120
121impl ScanTarget {
122    /// A target to add through [`crate::add_scan_targets`]. The blocks around `block_height` are prioritised for
123    /// scanning, and the transaction `txid` is scanned there when given.
124    #[must_use]
125    pub fn new(block_height: BlockHeight, txid: Option<TxId>) -> Self {
126        Self {
127            block_height,
128            txid,
129            pools: ScanTargetPools::default(),
130            origin: ScanTargetOrigin::External,
131        }
132    }
133
134    pub(crate) fn internal(block_height: BlockHeight, txid: TxId) -> Self {
135        Self {
136            block_height,
137            txid: Some(txid),
138            pools: ScanTargetPools::default(),
139            origin: ScanTargetOrigin::Internal,
140        }
141    }
142
143    /// The block height of the target.
144    #[must_use]
145    pub fn block_height(&self) -> BlockHeight {
146        self.block_height
147    }
148
149    /// The txid of the transaction to scan at the target's block height, when a particular one is wanted.
150    #[must_use]
151    pub fn txid(&self) -> Option<TxId> {
152        self.txid
153    }
154
155    pub(crate) fn pools(&self) -> ScanTargetPools {
156        self.pools
157    }
158
159    pub(crate) fn origin(&self) -> ScanTargetOrigin {
160        self.origin
161    }
162
163    pub(crate) fn with_pool(self, pool: ShieldedPool) -> Self {
164        Self {
165            pools: self.pools.with(pool),
166            ..self
167        }
168    }
169
170    // The target that stands for both `self` and `other`, which name the same block height and txid: it holds the
171    // pools of both, and it is internal where either is.
172    fn merge(mut self, other: Self) -> Self {
173        self.pools.0 |= other.pools.0;
174        if other.origin == ScanTargetOrigin::Internal {
175            self.origin = ScanTargetOrigin::Internal;
176        }
177        self
178    }
179
180    fn key(&self) -> (BlockHeight, Option<TxId>) {
181        (self.block_height, self.txid)
182    }
183}
184
185impl PartialEq for ScanTarget {
186    fn eq(&self, other: &Self) -> bool {
187        self.key() == other.key()
188    }
189}
190
191impl Eq for ScanTarget {}
192
193impl PartialOrd for ScanTarget {
194    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
195        Some(self.cmp(other))
196    }
197}
198
199impl Ord for ScanTarget {
200    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
201        self.key().cmp(&other.key())
202    }
203}
204
205/// Initial sync state.
206///
207/// All fields will be reset when a new sync session starts.
208#[derive(Debug, Clone)]
209pub struct InitialSyncState {
210    /// One block above the fully scanned wallet height at start of sync session.
211    ///
212    /// If chain height is not larger than fully scanned height when sync is called, this value will be set to chain
213    /// height instead.
214    pub(crate) sync_start_height: BlockHeight,
215    /// The tree sizes of the fully scanned height and chain tip at start of sync session.
216    pub(crate) wallet_tree_bounds: TreeBounds,
217    /// Total number of blocks scanned in previous sync sessions.
218    pub(crate) previously_scanned_blocks: u32,
219    /// Total number of sapling outputs scanned in previous sync sessions.
220    pub(crate) previously_scanned_sapling_outputs: u32,
221    /// Total number of orchard outputs scanned in previous sync sessions.
222    pub(crate) previously_scanned_orchard_outputs: u32,
223    /// Total number of ironwood outputs scanned in previous sync sessions.
224    pub(crate) previously_scanned_ironwood_outputs: u32,
225}
226
227impl InitialSyncState {
228    /// Create new `InitialSyncState`
229    #[must_use]
230    pub fn new() -> Self {
231        InitialSyncState {
232            sync_start_height: 0.into(),
233            wallet_tree_bounds: TreeBounds {
234                sapling_initial_tree_size: 0,
235                sapling_final_tree_size: 0,
236                orchard_initial_tree_size: 0,
237                orchard_final_tree_size: 0,
238                ironwood_initial_tree_size: 0,
239                ironwood_final_tree_size: 0,
240            },
241            previously_scanned_blocks: 0,
242            previously_scanned_sapling_outputs: 0,
243            previously_scanned_orchard_outputs: 0,
244            previously_scanned_ironwood_outputs: 0,
245        }
246    }
247}
248
249impl Default for InitialSyncState {
250    fn default() -> Self {
251        Self::new()
252    }
253}
254
255/// Encapsulates the current state of sync
256#[derive(Debug, Clone)]
257pub struct SyncState {
258    /// A vec of block ranges with scan priorities from wallet birthday to chain tip.
259    /// In block height order with no overlaps or gaps.
260    pub(crate) scan_ranges: Vec<ScanRange>,
261    /// The block ranges that contain all sapling outputs of complete sapling shards.
262    ///
263    /// There is an edge case where a range may include two (or more) shards. However, this only occurs when the lower
264    /// shards are already scanned so will cause no issues when punching in the higher scan priorites.
265    pub(crate) sapling_shard_ranges: Vec<Range<BlockHeight>>,
266    /// The block ranges that contain all orchard outputs of complete orchard shards.
267    ///
268    /// There is an edge case where a range may include two (or more) shards. However, this only occurs when the lower
269    /// shards are already scanned so will cause no issues when punching in the higher scan priorites.
270    pub(crate) orchard_shard_ranges: Vec<Range<BlockHeight>>,
271    /// The block ranges that contain all ironwood outputs of complete ironwood shards.
272    ///
273    /// There is an edge case where a range may include two (or more) shards. However, this only occurs when the lower
274    /// shards are already scanned so will cause no issues when punching in the higher scan priorites.
275    pub(crate) ironwood_shard_ranges: Vec<Range<BlockHeight>>,
276    /// Scan targets for relevant transactions to the wallet.
277    pub(crate) scan_targets: BTreeSet<ScanTarget>,
278    /// Initial sync state.
279    pub(crate) initial_sync_state: InitialSyncState,
280    /// Compact block transparent data is only scanned above this height. This includes checking transparent outputs
281    /// against the wallet's in-use and gap addresses and mapping transparent inputs to the wallet's outpoint map.
282    ///
283    /// Set to the chain height when transparent address discovery completes at the start of a sync session, as it has
284    /// located all relevant transactions at or below this height. Lowered if the wallet is truncated below this
285    /// height, by a re-org or a chain height below the wallet's, as transparent address discovery does not cover the
286    /// blocks scanned in place of the truncated blocks.
287    ///
288    /// Blocks mined during a sync session are above the floor and may still be unscanned when the session ends, so
289    /// the transparent address discovery of the next sync session searches from the block above the floor.
290    pub(crate) transparent_scan_floor: Option<BlockHeight>,
291}
292
293impl SyncState {
294    /// Create new `SyncState`
295    #[must_use]
296    pub fn new() -> Self {
297        SyncState {
298            scan_ranges: Vec::new(),
299            sapling_shard_ranges: Vec::new(),
300            orchard_shard_ranges: Vec::new(),
301            ironwood_shard_ranges: Vec::new(),
302            scan_targets: BTreeSet::new(),
303            initial_sync_state: InitialSyncState::new(),
304            transparent_scan_floor: None,
305        }
306    }
307
308    /// Scan ranges
309    #[must_use]
310    pub fn scan_ranges(&self) -> &[ScanRange] {
311        &self.scan_ranges
312    }
313
314    /// Sapling shard ranges
315    #[must_use]
316    pub fn sapling_shard_ranges(&self) -> &[Range<BlockHeight>] {
317        &self.sapling_shard_ranges
318    }
319
320    /// Orchard shard ranges
321    #[must_use]
322    pub fn orchard_shard_ranges(&self) -> &[Range<BlockHeight>] {
323        &self.orchard_shard_ranges
324    }
325
326    /// Ironwood shard ranges
327    #[must_use]
328    pub fn ironwood_shard_ranges(&self) -> &[Range<BlockHeight>] {
329        &self.ironwood_shard_ranges
330    }
331
332    /// The shard ranges of `pool`
333    #[must_use]
334    pub fn shard_ranges(&self, pool: ShieldedPool) -> &[Range<BlockHeight>] {
335        match pool {
336            ShieldedPool::Sapling => &self.sapling_shard_ranges,
337            ShieldedPool::Orchard => &self.orchard_shard_ranges,
338            ShieldedPool::Ironwood => &self.ironwood_shard_ranges,
339        }
340    }
341
342    /// Returns true if all scan ranges are scanned.
343    pub(crate) fn scan_complete(&self) -> bool {
344        self.scan_ranges
345            .iter()
346            .all(|scan_range| scan_range.priority() == ScanPriority::Scanned)
347    }
348
349    /// Returns the block height at which all blocks equal to and below this height are scanned.
350    /// Returns `None` if `self.scan_ranges` is empty.
351    #[must_use]
352    pub fn fully_scanned_height(&self) -> Option<BlockHeight> {
353        if let Some(scan_range) = self
354            .scan_ranges
355            .iter()
356            .find(|scan_range| scan_range.priority() != ScanPriority::Scanned)
357        {
358            Some(scan_range.block_range().start - 1)
359        } else {
360            self.scan_ranges
361                .last()
362                .map(|range| range.block_range().end - 1)
363        }
364    }
365
366    /// Returns the highest block height that has been scanned.
367    /// If no scan ranges have been scanned, returns the block below the wallet birthday.
368    /// Returns `None` if `self.scan_ranges` is empty.
369    #[must_use]
370    pub fn highest_scanned_height(&self) -> Option<BlockHeight> {
371        if let Some(last_scanned_range) = self.scan_ranges.iter().rfind(|scan_range| {
372            scan_range.priority() == ScanPriority::Scanned
373                || scan_range.priority() == ScanPriority::ScannedWithoutMapping
374                || scan_range.priority() == ScanPriority::RefetchingNullifiers
375        }) {
376            Some(last_scanned_range.block_range().end - 1)
377        } else {
378            self.wallet_birthday().map(|start| start - 1)
379        }
380    }
381
382    /// Returns the wallet birthday or `None` if `self.scan_ranges` is empty.
383    ///
384    #[must_use]
385    pub fn wallet_birthday(&self) -> Option<BlockHeight> {
386        self.scan_ranges
387            .first()
388            .map(|range| range.block_range().start)
389    }
390
391    /// - Inserts `scan_target`, merged into the target already held for the same block height and txid if any.
392    pub(crate) fn insert_scan_target(&mut self, scan_target: ScanTarget) {
393        let merged = self
394            .scan_targets
395            .take(&scan_target)
396            .map_or(scan_target, |held| held.merge(scan_target));
397        self.scan_targets.insert(merged);
398    }
399
400    /// Returns the last known chain height to the wallet or `None` if `self.scan_ranges` is empty.
401    #[must_use]
402    pub fn last_known_chain_height(&self) -> Option<BlockHeight> {
403        self.scan_ranges
404            .last()
405            .map(|range| range.block_range().end - 1)
406    }
407}
408
409impl Default for SyncState {
410    fn default() -> Self {
411        Self::new()
412    }
413}
414
415/// Sync modes.
416#[derive(Debug, Clone, Copy, PartialEq, Eq)]
417pub enum SyncMode {
418    /// Sync is not running.
419    NotRunning,
420    /// Sync is held in a paused state and the wallet guard is dropped.
421    Paused,
422    /// Sync is running.
423    Running,
424    /// Sync is shutting down.
425    Shutdown,
426}
427
428impl SyncMode {
429    /// Constructor from u8.
430    ///
431    /// Returns `None` if `mode` is not a valid enum variant.
432    pub fn from_u8(mode: u8) -> Result<Self, SyncModeError> {
433        match mode {
434            0 => Ok(Self::NotRunning),
435            1 => Ok(Self::Paused),
436            2 => Ok(Self::Running),
437            3 => Ok(Self::Shutdown),
438            _ => Err(SyncModeError::InvalidSyncMode(mode)),
439        }
440    }
441
442    /// Reads the mode held in `atomic_sync_mode`, failing when the byte is no variant of this enum.
443    pub fn from_atomic_u8(atomic_sync_mode: &AtomicU8) -> Result<SyncMode, SyncModeError> {
444        SyncMode::from_u8(atomic_sync_mode.load(atomic::Ordering::Acquire))
445    }
446
447    /// Returns the mode a completed scan leaves behind, which is `Shutdown` for a running engine and the same mode for every other.
448    pub fn on_completion(self) -> Self {
449        match self {
450            Self::Running => Self::Shutdown,
451            other => other,
452        }
453    }
454
455    /// Replaces the mode held in `atomic_sync_mode` with `step` of it in one atomic exchange and returns the mode it replaced.
456    pub fn apply(
457        atomic_sync_mode: &AtomicU8,
458        step: impl Fn(Self) -> Self,
459    ) -> Result<Self, SyncModeError> {
460        atomic_sync_mode
461            .try_update(
462                atomic::Ordering::AcqRel,
463                atomic::Ordering::Acquire,
464                |mode| Self::from_u8(mode).ok().map(|mode| step(mode) as u8),
465            )
466            .map_err(SyncModeError::InvalidSyncMode)
467            .and_then(Self::from_u8)
468    }
469
470    /// Moves `atomic_sync_mode` from `from` to `to` when it holds `from` and returns the mode it held before.
471    pub fn transition(
472        atomic_sync_mode: &AtomicU8,
473        from: Self,
474        to: Self,
475    ) -> Result<Self, SyncModeError> {
476        Self::apply(
477            atomic_sync_mode,
478            |mode| if mode == from { to } else { mode },
479        )
480    }
481}
482
483/// Initial and final tree sizes.
484#[derive(Debug, Clone, Copy)]
485#[allow(missing_docs)]
486pub struct TreeBounds {
487    pub sapling_initial_tree_size: u32,
488    pub sapling_final_tree_size: u32,
489    pub orchard_initial_tree_size: u32,
490    pub orchard_final_tree_size: u32,
491    pub ironwood_initial_tree_size: u32,
492    pub ironwood_final_tree_size: u32,
493}
494
495/// Output ID for a given pool type.
496#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
497pub struct OutputId {
498    /// ID of associated transaction.
499    txid: TxId,
500    /// Index of output within the transactions bundle of the given pool type.
501    output_index: u32,
502}
503
504impl OutputId {
505    /// Creates new `OutputId` from parts.
506    #[must_use]
507    pub fn new(txid: TxId, output_index: u32) -> Self {
508        OutputId { txid, output_index }
509    }
510
511    /// Transaction ID of output's associated transaction.
512    #[must_use]
513    pub fn txid(&self) -> TxId {
514        self.txid
515    }
516
517    /// Index of output within the transactions bundle of the given pool type.
518    #[must_use]
519    pub fn output_index(&self) -> u32 {
520        self.output_index
521    }
522}
523
524impl std::fmt::Display for OutputId {
525    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
526        write!(
527            f,
528            "{{
529                txid: {}
530                output index: {}
531            }}",
532            self.txid, self.output_index
533        )
534    }
535}
536
537impl From<&OutPoint> for OutputId {
538    fn from(value: &OutPoint) -> Self {
539        OutputId::new(*value.txid(), value.n())
540    }
541}
542
543impl From<OutputId> for OutPoint {
544    fn from(value: OutputId) -> Self {
545        OutPoint::new(value.txid.into(), value.output_index)
546    }
547}
548
549/// Binary tree map of nullifiers from transaction spends or actions
550#[derive(Debug)]
551pub struct NullifierMap {
552    /// Sapling nullifer map
553    pub sapling: BTreeMap<sapling_crypto::Nullifier, ScanTarget>,
554    /// Orchard nullifer map
555    pub orchard: BTreeMap<orchard::note::Nullifier, ScanTarget>,
556    /// Ironwood nullifer map
557    pub ironwood: BTreeMap<orchard::note::Nullifier, ScanTarget>,
558}
559
560impl NullifierMap {
561    /// Construct new nullifier map.
562    #[must_use]
563    pub fn new() -> Self {
564        Self {
565            sapling: BTreeMap::new(),
566            orchard: BTreeMap::new(),
567            ironwood: BTreeMap::new(),
568        }
569    }
570
571    /// Clear nullifier map.
572    pub fn clear(&mut self) {
573        self.sapling.clear();
574        self.orchard.clear();
575        self.ironwood.clear();
576    }
577}
578
579impl Default for NullifierMap {
580    fn default() -> Self {
581        Self::new()
582    }
583}
584
585/// Wallet block data
586#[derive(Debug, Clone)]
587pub struct WalletBlock {
588    pub(crate) block_height: BlockHeight,
589    pub(crate) block_hash: BlockHash,
590    pub(crate) prev_hash: BlockHash,
591    pub(crate) time: u32,
592    pub(crate) txids: Vec<TxId>,
593    pub(crate) tree_bounds: TreeBounds,
594}
595
596impl WalletBlock {
597    pub(crate) async fn from_compact_block(
598        consensus_parameters: &impl consensus::Parameters,
599        fetch_request_sender: mpsc::UnboundedSender<FetchRequest>,
600        block: &CompactBlock,
601    ) -> Result<Self, ServerError> {
602        let tree_bounds =
603            calculate_block_tree_bounds(consensus_parameters, fetch_request_sender, block).await?;
604
605        Ok(Self {
606            block_height: block::get_compact_height(block),
607            block_hash: block::get_compact_hash(block),
608            prev_hash: block::get_compact_prev_hash(block),
609            time: block.time,
610            txids: block
611                .vtx
612                .iter()
613                .map(transaction::get_compact_txid)
614                .collect(),
615            tree_bounds,
616        })
617    }
618
619    /// Block height.
620    #[must_use]
621    pub fn block_height(&self) -> BlockHeight {
622        self.block_height
623    }
624
625    /// Block hash.
626    #[must_use]
627    pub fn block_hash(&self) -> BlockHash {
628        self.block_hash
629    }
630
631    /// Previous block hash.
632    #[must_use]
633    pub fn prev_hash(&self) -> BlockHash {
634        self.prev_hash
635    }
636
637    /// Time block was mined.
638    #[must_use]
639    pub fn time(&self) -> u32 {
640        self.time
641    }
642
643    /// Transaction IDs of transactions in the block.
644    #[must_use]
645    pub fn txids(&self) -> &[TxId] {
646        &self.txids
647    }
648
649    /// Tree size bounds
650    #[must_use]
651    pub fn tree_bounds(&self) -> TreeBounds {
652        self.tree_bounds
653    }
654}
655
656/// Wallet transaction
657pub struct WalletTransaction {
658    pub(crate) txid: TxId,
659    pub(crate) status: ConfirmationStatus,
660    pub(crate) transaction: zcash_primitives::transaction::Transaction,
661    pub(crate) datetime: u32,
662    pub(crate) transparent_coins: Vec<TransparentCoin>,
663    pub(crate) sapling_notes: Vec<SaplingNote>,
664    pub(crate) orchard_notes: Vec<OrchardNote>,
665    pub(crate) ironwood_notes: Vec<IronwoodNote>,
666    pub(crate) outgoing_sapling_notes: Vec<OutgoingSaplingNote>,
667    pub(crate) outgoing_orchard_notes: Vec<OutgoingOrchardNote>,
668    pub(crate) outgoing_ironwood_notes: Vec<OutgoingIronwoodNote>,
669}
670
671impl WalletTransaction {
672    /// Transaction ID
673    #[must_use]
674    pub fn txid(&self) -> TxId {
675        self.txid
676    }
677
678    /// Confirmation status
679    #[must_use]
680    pub fn status(&self) -> ConfirmationStatus {
681        self.status
682    }
683
684    /// [`zcash_primitives::transaction::Transaction`]
685    #[must_use]
686    pub fn transaction(&self) -> &zcash_primitives::transaction::Transaction {
687        &self.transaction
688    }
689
690    /// Datetime. In form of seconds since unix epoch.
691    #[must_use]
692    pub fn datetime(&self) -> u32 {
693        self.datetime
694    }
695
696    /// Transparent coins
697    #[must_use]
698    pub fn transparent_coins(&self) -> &[TransparentCoin] {
699        &self.transparent_coins
700    }
701
702    /// Transparent coins mutable
703    pub fn transparent_coins_mut(&mut self) -> Vec<&mut TransparentCoin> {
704        self.transparent_coins.iter_mut().collect()
705    }
706
707    /// Sapling notes
708    #[must_use]
709    pub fn sapling_notes(&self) -> &[SaplingNote] {
710        &self.sapling_notes
711    }
712
713    /// Sapling notes mutable
714    pub fn sapling_notes_mut(&mut self) -> Vec<&mut SaplingNote> {
715        self.sapling_notes.iter_mut().collect()
716    }
717
718    /// Orchard notes
719    #[must_use]
720    pub fn orchard_notes(&self) -> &[OrchardNote] {
721        &self.orchard_notes
722    }
723
724    /// Orchard notes mutable
725    pub fn orchard_notes_mut(&mut self) -> Vec<&mut OrchardNote> {
726        self.orchard_notes.iter_mut().collect()
727    }
728
729    /// Ironwood notes
730    #[must_use]
731    pub fn ironwood_notes(&self) -> &[IronwoodNote] {
732        &self.ironwood_notes
733    }
734
735    /// Ironwood notes mutable
736    pub fn ironwood_notes_mut(&mut self) -> Vec<&mut IronwoodNote> {
737        self.ironwood_notes.iter_mut().collect()
738    }
739
740    /// Outgoing sapling notes
741    #[must_use]
742    pub fn outgoing_sapling_notes(&self) -> &[OutgoingSaplingNote] {
743        &self.outgoing_sapling_notes
744    }
745
746    /// Outgoing orchard notes
747    #[must_use]
748    pub fn outgoing_orchard_notes(&self) -> &[OutgoingOrchardNote] {
749        &self.outgoing_orchard_notes
750    }
751
752    /// Outgoing ironwood notes
753    #[must_use]
754    pub fn outgoing_ironwood_notes(&self) -> &[OutgoingIronwoodNote] {
755        &self.outgoing_ironwood_notes
756    }
757
758    /// Returns nullifers from sapling bundle.
759    /// Returns empty vec if bundle is `None`.
760    pub fn sapling_nullifiers(&self) -> Vec<&sapling_crypto::Nullifier> {
761        self.transaction
762            .sapling_bundle()
763            .map_or_else(Vec::new, |bundle| {
764                bundle
765                    .shielded_spends()
766                    .iter()
767                    .map(|spend| spend.nullifier())
768                    .collect::<Vec<_>>()
769            })
770    }
771
772    /// Returns nullifers from orchard bundle.
773    /// Returns empty vec if bundle is `None`.
774    pub fn orchard_nullifiers(&self) -> Vec<&orchard::note::Nullifier> {
775        self.transaction
776            .orchard_bundle()
777            .map_or_else(Vec::new, |bundle| {
778                bundle
779                    .actions()
780                    .iter()
781                    .map(orchard::Action::nullifier)
782                    .collect::<Vec<_>>()
783            })
784    }
785
786    /// Returns nullifers from ironwood bundle.
787    /// Returns empty vec if bundle is `None`.
788    pub fn ironwood_nullifiers(&self) -> Vec<&orchard::note::Nullifier> {
789        self.transaction
790            .ironwood_bundle()
791            .map_or_else(Vec::new, |bundle| {
792                bundle
793                    .actions()
794                    .iter()
795                    .map(orchard::Action::nullifier)
796                    .collect::<Vec<_>>()
797            })
798    }
799
800    /// Returns outpoints from transparent bundle.
801    /// Returns empty vec if bundle is `None`.
802    pub fn outpoints(&self) -> Vec<&OutPoint> {
803        self.transaction
804            .transparent_bundle()
805            .map_or_else(Vec::new, |bundle| {
806                bundle
807                    .vin
808                    .iter()
809                    .map(zcash_transparent::bundle::TxIn::prevout)
810                    .collect::<Vec<_>>()
811            })
812    }
813
814    /// Updates transaction status if `status` is a valid update for the current transaction status.
815    /// For example, if `status` is `Mempool` but the current transaction status is `Confirmed`, the status will remain
816    /// unchanged.
817    /// A `Failed` transaction may return to `Mempool` status, as being observed in the mempool is
818    /// proof the transaction was not rejected or expired.
819    /// `datetime` refers to the time in which the status was updated, or the time the block was mined when updating
820    /// to `Confirmed` status.
821    /// If `fail_confirmed` is set, a confirmed transaction can be set to failed (i.e. during re-org truncation).
822    pub fn update_status(
823        &mut self,
824        status: ConfirmationStatus,
825        datetime: u32,
826        fail_confirmed: bool,
827    ) {
828        match status {
829            ConfirmationStatus::Transmitted(_)
830                if matches!(self.status(), ConfirmationStatus::Calculated(_)) =>
831            {
832                self.status = status;
833                self.datetime = datetime;
834            }
835            ConfirmationStatus::Mempool(_)
836                if matches!(
837                    self.status(),
838                    ConfirmationStatus::Calculated(_)
839                        | ConfirmationStatus::Transmitted(_)
840                        | ConfirmationStatus::Failed(_)
841                ) =>
842            {
843                self.status = status;
844                self.datetime = datetime;
845            }
846            ConfirmationStatus::Confirmed(_)
847                if matches!(
848                    self.status(),
849                    ConfirmationStatus::Calculated(_)
850                        | ConfirmationStatus::Transmitted(_)
851                        | ConfirmationStatus::Mempool(_)
852                ) =>
853            {
854                self.status = status;
855                self.datetime = datetime;
856            }
857            ConfirmationStatus::Failed(_)
858                if (fail_confirmed && !matches!(self.status(), ConfirmationStatus::Failed(_)))
859                    || self.status().is_pending() =>
860            {
861                self.status = status;
862                self.datetime = datetime;
863            }
864            _ => (),
865        }
866    }
867}
868
869#[cfg(any(test, feature = "test-features"))]
870impl WalletTransaction {
871    /// Creates a minimal `WalletTransaction` for testing purposes.
872    ///
873    /// Constructs a valid v5 transaction with empty bundles and the given `txid` and `status`.
874    /// The transaction's expiry height is set to 0 (never expires).
875    pub fn new_for_test(txid: TxId, status: ConfirmationStatus) -> Self {
876        Self::new_for_test_with_expiry(txid, status, BlockHeight::from_u32(0))
877    }
878
879    /// Creates a minimal `WalletTransaction` for testing purposes, with the given expiry height.
880    ///
881    /// Constructs a valid v5 transaction with empty bundles and the given `txid` and `status`.
882    pub fn new_for_test_with_expiry(
883        txid: TxId,
884        status: ConfirmationStatus,
885        expiry_height: BlockHeight,
886    ) -> Self {
887        use zcash_primitives::transaction::{TransactionData, TxVersion};
888        use zcash_protocol::consensus::BranchId;
889
890        let transaction = TransactionData::from_parts(
891            TxVersion::V6,
892            BranchId::Nu6_3,
893            0,
894            expiry_height,
895            None,
896            None,
897            None,
898            None,
899        )
900        .freeze()
901        .expect("empty v5 transaction should always be valid");
902
903        Self {
904            txid,
905            status,
906            transaction,
907            datetime: 0,
908            transparent_coins: Vec::new(),
909            sapling_notes: Vec::new(),
910            orchard_notes: Vec::new(),
911            ironwood_notes: Vec::new(),
912            outgoing_sapling_notes: Vec::new(),
913            outgoing_orchard_notes: Vec::new(),
914            outgoing_ironwood_notes: Vec::new(),
915        }
916    }
917
918    /// As [`Self::new_for_test`], with received sapling notes attached.
919    #[must_use]
920    pub fn with_sapling_notes_for_test(mut self, sapling_notes: Vec<SaplingNote>) -> Self {
921        self.sapling_notes = sapling_notes;
922        self
923    }
924
925    /// As [`Self::new_for_test`], with outgoing sapling notes attached.
926    #[must_use]
927    pub fn with_outgoing_sapling_notes_for_test(
928        mut self,
929        outgoing_sapling_notes: Vec<OutgoingSaplingNote>,
930    ) -> Self {
931        self.outgoing_sapling_notes = outgoing_sapling_notes;
932        self
933    }
934
935    /// As [`Self::new_for_test`], with received transparent coins attached.
936    #[must_use]
937    pub fn with_transparent_coins_for_test(
938        mut self,
939        transparent_coins: Vec<TransparentCoin>,
940    ) -> Self {
941        self.transparent_coins = transparent_coins;
942        self
943    }
944
945    /// As [`Self::new_for_test`], with received and outgoing orchard notes
946    /// attached, for tests exercising summary/value-transfer derivation
947    /// without a chain.
948    pub fn new_for_test_with_orchard_notes(
949        txid: TxId,
950        status: ConfirmationStatus,
951        orchard_notes: Vec<OrchardNote>,
952        outgoing_orchard_notes: Vec<OutgoingOrchardNote>,
953    ) -> Self {
954        let mut transaction = Self::new_for_test(txid, status);
955        transaction.orchard_notes = orchard_notes;
956        transaction.outgoing_orchard_notes = outgoing_orchard_notes;
957        transaction
958    }
959
960    /// As [`Self::new_for_test`], with received and outgoing ironwood notes
961    /// attached, for tests exercising summary/value-transfer derivation
962    /// without a chain.
963    pub fn new_for_test_with_ironwood_notes(
964        txid: TxId,
965        status: ConfirmationStatus,
966        ironwood_notes: Vec<IronwoodNote>,
967        outgoing_ironwood_notes: Vec<OutgoingIronwoodNote>,
968    ) -> Self {
969        let mut transaction = Self::new_for_test(txid, status);
970        transaction.ironwood_notes = ironwood_notes;
971        transaction.outgoing_ironwood_notes = outgoing_ironwood_notes;
972        transaction
973    }
974}
975
976#[cfg(any(test, feature = "test-features"))]
977impl SyncState {
978    /// Creates sync state with the given scan ranges, for tests exercising
979    /// spendability/witness gating without a chain.
980    pub fn new_for_test(scan_ranges: Vec<ScanRange>) -> Self {
981        let mut sync_state = Self::new();
982        sync_state.scan_ranges = scan_ranges;
983        sync_state
984    }
985}
986
987#[cfg(any(test, feature = "test-features"))]
988impl<N, Nf: Copy, P> WalletNote<N, Nf, P> {
989    /// Creates a minimal received note for testing purposes.
990    pub fn new_for_test(
991        output_id: OutputId,
992        account_id: zip32::AccountId,
993        scope: zip32::Scope,
994        note: N,
995        memo: Memo,
996        position: Option<Position>,
997    ) -> Self {
998        Self {
999            output_id,
1000            key_id: KeyId::from_parts(account_id, scope),
1001            note,
1002            nullifier: None,
1003            position,
1004            memo,
1005            spending_transaction: None,
1006            refetch_nullifier_ranges: Vec::new(),
1007            marker: std::marker::PhantomData,
1008        }
1009    }
1010
1011    /// Attaches a nullifier, for tests exercising spend paths, since the
1012    /// spendable-note filter requires a known nullifier.
1013    #[must_use]
1014    pub fn with_nullifier_for_test(mut self, nullifier: Nf) -> Self {
1015        self.nullifier = Some(nullifier);
1016        self
1017    }
1018}
1019
1020#[cfg(feature = "test-features")]
1021impl<N, P> OutgoingNote<N, P> {
1022    /// Creates a minimal outgoing note for testing purposes.
1023    pub fn new_for_test(
1024        output_id: OutputId,
1025        account_id: zip32::AccountId,
1026        scope: zip32::Scope,
1027        note: N,
1028        memo: Memo,
1029        recipient_full_unified_address: Option<UnifiedAddress>,
1030    ) -> Self {
1031        Self {
1032            output_id,
1033            key_id: KeyId::from_parts(account_id, scope),
1034            note,
1035            memo,
1036            recipient_full_unified_address,
1037            marker: std::marker::PhantomData,
1038        }
1039    }
1040}
1041
1042#[cfg(feature = "wallet_essentials")]
1043impl WalletTransaction {
1044    /// Returns the total value sent to receivers, excluding value sent to the wallet's own addresses.
1045    #[must_use]
1046    pub fn total_value_sent(&self) -> u64 {
1047        let transparent_value_sent = self
1048            .transaction
1049            .transparent_bundle()
1050            .map_or(0, |bundle| {
1051                bundle
1052                    .vout
1053                    .iter()
1054                    .map(|output| output.value().into_u64())
1055                    .sum()
1056            })
1057            .saturating_sub(self.total_output_value::<TransparentCoin>());
1058
1059        let sapling_value_sent =
1060            self.total_external_outgoing_note_value::<OutgoingSaplingNote, SaplingNote>();
1061        let orchard_value_sent =
1062            self.total_external_outgoing_note_value::<OutgoingOrchardNote, OrchardNote>();
1063        let ironwood_value_sent =
1064            self.total_external_outgoing_note_value::<OutgoingIronwoodNote, IronwoodNote>();
1065
1066        transparent_value_sent + sapling_value_sent + orchard_value_sent + ironwood_value_sent
1067    }
1068
1069    /// Returns total sum of all output values.
1070    #[must_use]
1071    pub fn total_value_received(&self) -> u64 {
1072        self.total_output_value::<TransparentCoin>()
1073            + self.total_output_value::<SaplingNote>()
1074            + self.total_output_value::<OrchardNote>()
1075            + self.total_output_value::<IronwoodNote>()
1076    }
1077
1078    /// Returns total sum of output values for a given pool.
1079    #[must_use]
1080    pub fn total_output_value<Op: OutputInterface>(&self) -> u64 {
1081        Op::transaction_outputs(self)
1082            .iter()
1083            .map(OutputInterface::value)
1084            .sum()
1085    }
1086
1087    /// Returns total sum of outgoing note values for a given shielded pool.
1088    #[must_use]
1089    pub fn total_outgoing_note_value<Op: OutgoingNoteInterface>(&self) -> u64 {
1090        Op::transaction_outgoing_notes(self)
1091            .iter()
1092            .map(OutgoingNoteInterface::value)
1093            .sum()
1094    }
1095
1096    /// Returns total sum of outgoing note values for outputs that are not wallet-owned.
1097    #[must_use]
1098    pub fn total_external_outgoing_note_value<Outgoing, Incoming>(&self) -> u64
1099    where
1100        Outgoing: OutgoingNoteInterface,
1101        Incoming: OutputInterface,
1102    {
1103        Outgoing::transaction_outgoing_notes(self)
1104            .iter()
1105            .filter(|outgoing_note| {
1106                !Incoming::transaction_outputs(self)
1107                    .iter()
1108                    .any(|wallet_note| wallet_note.output_id() == outgoing_note.output_id())
1109            })
1110            .map(OutgoingNoteInterface::value)
1111            .sum()
1112    }
1113}
1114
1115impl std::fmt::Debug for WalletTransaction {
1116    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1117        f.debug_struct("WalletTransaction")
1118            .field("txid", &self.txid)
1119            .field("confirmation_status", &self.status)
1120            .field("datetime", &self.datetime)
1121            .field("transparent_coins", &self.transparent_coins)
1122            .field("sapling_notes", &self.sapling_notes)
1123            .field("orchard_notes", &self.orchard_notes)
1124            .field("ironwood_notes", &self.ironwood_notes)
1125            .field("outgoing_sapling_notes", &self.outgoing_sapling_notes)
1126            .field("outgoing_orchard_notes", &self.outgoing_orchard_notes)
1127            .field("outgoing_ironwood_notes", &self.outgoing_ironwood_notes)
1128            .finish()
1129    }
1130}
1131
1132/// Provides a common API for all key identifiers.
1133pub trait KeyIdInterface {
1134    /// Account ID.
1135    fn account_id(&self) -> zip32::AccountId;
1136}
1137
1138/// Provides a common API for all output types.
1139pub trait OutputInterface: Sized {
1140    /// Identifier for key used to decrypt output.
1141    type KeyId: KeyIdInterface;
1142    /// Transaction input type associated with spend detection of output.
1143    type Input: Clone + Debug + PartialEq + Eq + PartialOrd + Ord;
1144
1145    /// Output's associated pool type.
1146    const POOL_TYPE: PoolType;
1147
1148    /// Output ID.
1149    fn output_id(&self) -> OutputId;
1150
1151    /// Identifier for key used to decrypt output.
1152    fn key_id(&self) -> Self::KeyId;
1153
1154    /// Transaction ID of transaction this output was spent.
1155    /// If `None`, output is not spent.
1156    fn spending_transaction(&self) -> Option<TxId>;
1157
1158    /// Sets spending transaction.
1159    fn set_spending_transaction(&mut self, spending_transaction: Option<TxId>);
1160
1161    /// Note value..
1162    // TODO: change to Zatoshis checked type
1163    fn value(&self) -> u64;
1164
1165    /// Returns the type used to link with transaction inputs for spend detection.
1166    /// Returns `None` in the case the nullifier is not available for shielded outputs.
1167    ///
1168    /// Nullifier for shielded outputs.
1169    /// Outpoint for transparent outputs.
1170    fn spend_link(&self) -> Option<Self::Input>;
1171
1172    /// Inputs within `transaction` used to detect an output's spend status.
1173    ///
1174    /// Nullifiers for shielded outputs.
1175    /// Out points for transparent outputs.
1176    fn transaction_inputs(transaction: &WalletTransaction) -> Vec<&Self::Input>;
1177
1178    /// Outputs within `transaction`.
1179    fn transaction_outputs(transaction: &WalletTransaction) -> &[Self];
1180}
1181
1182/// Provides a common API for all shielded output types.
1183pub trait NoteInterface: OutputInterface {
1184    /// Decrypted note type.
1185    type ZcashNote;
1186    /// Nullifier type.
1187    type Nullifier: Copy + Clone + PartialEq + Eq + PartialOrd + Ord;
1188
1189    /// Note's associated shielded protocol.
1190    const SHIELDED_PROTOCOL: ShieldedPool;
1191
1192    /// Decrypted note with recipient and value
1193    fn note(&self) -> &Self::ZcashNote;
1194
1195    /// Derived nullifier
1196    fn nullifier(&self) -> Option<Self::Nullifier>;
1197
1198    /// Commitment tree leaf position
1199    fn position(&self) -> Option<Position>;
1200
1201    /// Memo
1202    fn memo(&self) -> &Memo;
1203
1204    /// List of block ranges where the nullifiers must be re-fetched to guarantee the note has not been spent.
1205    /// These scan ranges were marked `ScannedWithoutMapping` or `RefetchingNullifiers` priority before this note was
1206    /// scanned, meaning the nullifiers were discarded due to memory constraints and will be re-fetched later in the
1207    /// sync process.
1208    fn refetch_nullifier_ranges(&self) -> &[Range<BlockHeight>];
1209}
1210
1211///  Transparent coin (output) with metadata relevant to the wallet.
1212#[derive(Debug, Clone)]
1213pub struct TransparentCoin {
1214    /// Output ID.
1215    pub(crate) output_id: OutputId,
1216    /// Identifier for key used to derive address.
1217    pub(crate) key_id: TransparentAddressId,
1218    /// Encoded transparent address.
1219    pub(crate) address: String,
1220    /// Script.
1221    pub(crate) script: Script,
1222    /// Coin value.
1223    pub(crate) value: Zatoshis,
1224    /// Transaction ID of transaction this output was spent.
1225    /// If `None`, output is not spent.
1226    pub(crate) spending_transaction: Option<TxId>,
1227}
1228
1229#[cfg(feature = "test-features")]
1230impl TransparentCoin {
1231    /// Creates a minimal received coin for testing purposes. The script
1232    /// must be the real locking script for `address`: spendable-output
1233    /// selection reconstructs the recipient from it and silently drops
1234    /// coins whose script does not parse to an address.
1235    pub fn new_for_test(
1236        output_id: OutputId,
1237        key_id: TransparentAddressId,
1238        address: String,
1239        script: Script,
1240        value: Zatoshis,
1241    ) -> Self {
1242        Self {
1243            output_id,
1244            key_id,
1245            address,
1246            script,
1247            value,
1248            spending_transaction: None,
1249        }
1250    }
1251}
1252
1253impl TransparentCoin {
1254    /// Address received to.
1255    #[must_use]
1256    pub fn address(&self) -> &str {
1257        &self.address
1258    }
1259
1260    /// Script.
1261    #[must_use]
1262    pub fn script(&self) -> &Script {
1263        &self.script
1264    }
1265}
1266
1267impl OutputInterface for TransparentCoin {
1268    type KeyId = TransparentAddressId;
1269    type Input = OutPoint;
1270
1271    const POOL_TYPE: PoolType = PoolType::Transparent;
1272
1273    fn output_id(&self) -> OutputId {
1274        self.output_id
1275    }
1276
1277    fn key_id(&self) -> Self::KeyId {
1278        self.key_id
1279    }
1280
1281    fn spending_transaction(&self) -> Option<TxId> {
1282        self.spending_transaction
1283    }
1284
1285    fn set_spending_transaction(&mut self, spending_transaction: Option<TxId>) {
1286        self.spending_transaction = spending_transaction;
1287    }
1288
1289    fn value(&self) -> u64 {
1290        self.value.into_u64()
1291    }
1292
1293    fn spend_link(&self) -> Option<Self::Input> {
1294        Some(self.output_id.into())
1295    }
1296
1297    fn transaction_inputs(transaction: &WalletTransaction) -> Vec<&Self::Input> {
1298        transaction.outpoints()
1299    }
1300
1301    fn transaction_outputs(transaction: &WalletTransaction) -> &[Self] {
1302        &transaction.transparent_coins
1303    }
1304}
1305
1306/// The network upgrade at which a shielded pool begins to exist.
1307///
1308/// This is the workspace's single pool-to-upgrade mapping: upstream
1309/// `zcash_protocol` deliberately ships
1310/// `ShieldedPool` and `NetworkUpgrade` as unrelated enums, so the mapping
1311/// is defined exactly once, here. Every height clamp involving a pool's
1312/// existence derives from it. A second mapping anywhere in the workspace
1313/// is a defect.
1314#[must_use]
1315pub fn pool_upgrade(pool: ShieldedPool) -> consensus::NetworkUpgrade {
1316    match pool {
1317        ShieldedPool::Sapling => consensus::NetworkUpgrade::Sapling,
1318        ShieldedPool::Orchard => consensus::NetworkUpgrade::Nu5,
1319        ShieldedPool::Ironwood => consensus::NetworkUpgrade::Nu6_3,
1320    }
1321}
1322
1323/// A pool's activation height: the block height at which the pool begins
1324/// to exist on the chain.
1325///
1326/// The only constructor is the derivation [`PoolActivation::of`], so a
1327/// value of this type carries its provenance: it came from the chain's
1328/// consensus parameters through [`pool_upgrade`], never from arithmetic a
1329/// call site invented. Seams that must not run without an activation floor
1330/// (the migration schedule planner, for one) demand this type in their
1331/// signatures, making floor omission unrepresentable.
1332#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1333pub struct PoolActivation(BlockHeight);
1334
1335impl PoolActivation {
1336    /// Derives the pool's activation height from the chain's consensus
1337    /// parameters. `None` when the chain never activates the pool's
1338    /// upgrade, a real state (regtest schedules may defer an upgrade
1339    /// indefinitely), in which the pool simply cannot be scheduled
1340    /// against.
1341    pub fn of(
1342        consensus_parameters: &impl consensus::Parameters,
1343        pool: ShieldedPool,
1344    ) -> Option<Self> {
1345        consensus_parameters
1346            .activation_height(pool_upgrade(pool))
1347            .map(Self)
1348    }
1349
1350    /// The activation height.
1351    #[must_use]
1352    pub fn height(&self) -> BlockHeight {
1353        self.0
1354    }
1355
1356    /// The later of the activation and `height`: the effective floor for
1357    /// heights that must not precede the pool's existence (a wallet
1358    /// Birthday clamped to the pool's activation, the lower bound of a
1359    /// scan range). Birthday remains a wallet property throughout. The
1360    /// pool contributes only its activation height.
1361    #[must_use]
1362    pub fn max_with(&self, height: BlockHeight) -> BlockHeight {
1363        self.0.max(height)
1364    }
1365
1366    /// A fabricated activation for tests exercising floor arithmetic
1367    /// without a full parameter set. Production code has no path here:
1368    /// the derivation constructor is the only other way in.
1369    #[cfg(any(test, feature = "test-features"))]
1370    #[must_use]
1371    pub fn new_for_test(height: BlockHeight) -> Self {
1372        Self(height)
1373    }
1374}
1375
1376/// Marker for the Sapling pool, distinguishing note types whose inner data
1377/// is otherwise identical.
1378#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1379pub struct Sapling;
1380
1381/// Marker for the Orchard pool, distinguishing note types whose inner data
1382/// is otherwise identical.
1383#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1384pub struct Orchard;
1385
1386/// Marker for the Ironwood pool. Ironwood reuses Orchard's note and
1387/// nullifier types (note version V3), so without the marker its wallet note
1388/// type would be the same type as the Orchard one.
1389#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1390pub struct Ironwood;
1391
1392/// Wallet note, shielded output with metadata relevant to the wallet. `P` is
1393/// the pool marker ([`Sapling`], [`Orchard`] or [`Ironwood`]).
1394#[derive(Debug, Clone)]
1395pub struct WalletNote<N, Nf: Copy, P> {
1396    /// Output ID.
1397    pub(crate) output_id: OutputId,
1398    /// Identifier for key used to decrypt output.
1399    pub(crate) key_id: KeyId,
1400    /// Decrypted note with recipient and value.
1401    pub(crate) note: N,
1402    /// Derived nullifier.
1403    pub(crate) nullifier: Option<Nf>, //TODO: syncing without nullifier deriving key
1404    /// Commitment tree leaf position.
1405    pub(crate) position: Option<Position>,
1406    /// Memo.
1407    pub(crate) memo: Memo,
1408    /// Transaction ID of transaction this output was spent.
1409    /// If `None`, output is not spent.
1410    pub(crate) spending_transaction: Option<TxId>,
1411    /// List of block ranges where the nullifiers must be re-fetched to guarantee the note has not been spent.
1412    /// These scan ranges were marked `ScannedWithoutMapping` or `RefetchingNullifiers` priority before this note was
1413    /// scanned, meaning the nullifiers were discarded due to memory constraints and will be re-fetched later in the
1414    /// sync process.
1415    pub(crate) refetch_nullifier_ranges: Vec<Range<BlockHeight>>,
1416    /// Pool marker.
1417    pub(crate) marker: PhantomData<P>,
1418}
1419
1420/// Sapling note.
1421pub type SaplingNote = WalletNote<sapling_crypto::Note, sapling_crypto::Nullifier, Sapling>;
1422
1423impl OutputInterface for SaplingNote {
1424    type KeyId = KeyId;
1425    type Input = sapling_crypto::Nullifier;
1426
1427    const POOL_TYPE: PoolType = PoolType::Shielded(ShieldedPool::Sapling);
1428
1429    fn output_id(&self) -> OutputId {
1430        self.output_id
1431    }
1432
1433    fn key_id(&self) -> KeyId {
1434        self.key_id
1435    }
1436
1437    fn spending_transaction(&self) -> Option<TxId> {
1438        self.spending_transaction
1439    }
1440
1441    fn set_spending_transaction(&mut self, spending_transaction: Option<TxId>) {
1442        self.spending_transaction = spending_transaction;
1443    }
1444
1445    fn value(&self) -> u64 {
1446        self.note.value().inner()
1447    }
1448
1449    fn spend_link(&self) -> Option<Self::Input> {
1450        self.nullifier
1451    }
1452
1453    fn transaction_inputs(transaction: &WalletTransaction) -> Vec<&Self::Input> {
1454        transaction.sapling_nullifiers()
1455    }
1456
1457    fn transaction_outputs(transaction: &WalletTransaction) -> &[Self] {
1458        &transaction.sapling_notes
1459    }
1460}
1461
1462impl NoteInterface for SaplingNote {
1463    type ZcashNote = sapling_crypto::Note;
1464    type Nullifier = Self::Input;
1465
1466    const SHIELDED_PROTOCOL: ShieldedPool = ShieldedPool::Sapling;
1467
1468    fn note(&self) -> &Self::ZcashNote {
1469        &self.note
1470    }
1471
1472    fn nullifier(&self) -> Option<Self::Nullifier> {
1473        self.nullifier
1474    }
1475
1476    fn position(&self) -> Option<Position> {
1477        self.position
1478    }
1479
1480    fn memo(&self) -> &Memo {
1481        &self.memo
1482    }
1483
1484    fn refetch_nullifier_ranges(&self) -> &[Range<BlockHeight>] {
1485        &self.refetch_nullifier_ranges
1486    }
1487}
1488
1489/// Orchard note.
1490pub type OrchardNote = WalletNote<orchard::Note, orchard::note::Nullifier, Orchard>;
1491
1492impl OutputInterface for OrchardNote {
1493    type KeyId = KeyId;
1494    type Input = orchard::note::Nullifier;
1495
1496    const POOL_TYPE: PoolType = PoolType::Shielded(ShieldedPool::Orchard);
1497
1498    fn output_id(&self) -> OutputId {
1499        self.output_id
1500    }
1501
1502    fn key_id(&self) -> KeyId {
1503        self.key_id
1504    }
1505
1506    fn spending_transaction(&self) -> Option<TxId> {
1507        self.spending_transaction
1508    }
1509
1510    fn set_spending_transaction(&mut self, spending_transaction: Option<TxId>) {
1511        self.spending_transaction = spending_transaction;
1512    }
1513
1514    fn value(&self) -> u64 {
1515        self.note.value().inner()
1516    }
1517
1518    fn spend_link(&self) -> Option<Self::Input> {
1519        self.nullifier
1520    }
1521
1522    fn transaction_inputs(transaction: &WalletTransaction) -> Vec<&Self::Input> {
1523        transaction.orchard_nullifiers()
1524    }
1525
1526    fn transaction_outputs(transaction: &WalletTransaction) -> &[Self] {
1527        &transaction.orchard_notes
1528    }
1529}
1530
1531impl NoteInterface for OrchardNote {
1532    type ZcashNote = orchard::Note;
1533    type Nullifier = Self::Input;
1534
1535    const SHIELDED_PROTOCOL: ShieldedPool = ShieldedPool::Orchard;
1536
1537    fn note(&self) -> &Self::ZcashNote {
1538        &self.note
1539    }
1540
1541    fn nullifier(&self) -> Option<Self::Nullifier> {
1542        self.spend_link()
1543    }
1544
1545    fn position(&self) -> Option<Position> {
1546        self.position
1547    }
1548
1549    fn memo(&self) -> &Memo {
1550        &self.memo
1551    }
1552
1553    fn refetch_nullifier_ranges(&self) -> &[Range<BlockHeight>] {
1554        &self.refetch_nullifier_ranges
1555    }
1556}
1557
1558/// Ironwood note. Shares Orchard's note and nullifier types (version V3) but
1559/// belongs to the Ironwood pool with its own note commitment tree.
1560pub type IronwoodNote = WalletNote<orchard::Note, orchard::note::Nullifier, Ironwood>;
1561
1562impl OutputInterface for IronwoodNote {
1563    type KeyId = KeyId;
1564    type Input = orchard::note::Nullifier;
1565
1566    const POOL_TYPE: PoolType = PoolType::Shielded(ShieldedPool::Ironwood);
1567
1568    fn output_id(&self) -> OutputId {
1569        self.output_id
1570    }
1571
1572    fn key_id(&self) -> KeyId {
1573        self.key_id
1574    }
1575
1576    fn spending_transaction(&self) -> Option<TxId> {
1577        self.spending_transaction
1578    }
1579
1580    fn set_spending_transaction(&mut self, spending_transaction: Option<TxId>) {
1581        self.spending_transaction = spending_transaction;
1582    }
1583
1584    fn value(&self) -> u64 {
1585        self.note.value().inner()
1586    }
1587
1588    fn spend_link(&self) -> Option<Self::Input> {
1589        self.nullifier
1590    }
1591
1592    fn transaction_inputs(transaction: &WalletTransaction) -> Vec<&Self::Input> {
1593        transaction.ironwood_nullifiers()
1594    }
1595
1596    fn transaction_outputs(transaction: &WalletTransaction) -> &[Self] {
1597        &transaction.ironwood_notes
1598    }
1599}
1600
1601impl NoteInterface for IronwoodNote {
1602    type ZcashNote = orchard::Note;
1603    type Nullifier = Self::Input;
1604
1605    const SHIELDED_PROTOCOL: ShieldedPool = ShieldedPool::Ironwood;
1606
1607    fn note(&self) -> &Self::ZcashNote {
1608        &self.note
1609    }
1610
1611    fn nullifier(&self) -> Option<Self::Nullifier> {
1612        self.spend_link()
1613    }
1614
1615    fn position(&self) -> Option<Position> {
1616        self.position
1617    }
1618
1619    fn memo(&self) -> &Memo {
1620        &self.memo
1621    }
1622
1623    fn refetch_nullifier_ranges(&self) -> &[Range<BlockHeight>] {
1624        &self.refetch_nullifier_ranges
1625    }
1626}
1627
1628/// Provides a common API for all outgoing note types.
1629pub trait OutgoingNoteInterface: Sized {
1630    /// Decrypted note type.
1631    type ZcashNote;
1632    /// Address type.
1633    type Address: Clone + Copy + Debug + PartialEq + Eq;
1634    /// Encoding error
1635    type Error: Debug + std::error::Error;
1636
1637    /// Note's associated shielded protocol.
1638    const SHIELDED_PROTOCOL: ShieldedPool;
1639
1640    /// Output ID.
1641    fn output_id(&self) -> OutputId;
1642
1643    /// Identifier for key used to decrypt outgoing note.
1644    fn key_id(&self) -> KeyId;
1645
1646    /// Note value.
1647    fn value(&self) -> u64;
1648
1649    /// Decrypted note with recipient and value.
1650    fn note(&self) -> &Self::ZcashNote;
1651
1652    /// Memo.
1653    fn memo(&self) -> &Memo;
1654
1655    /// Recipient address.
1656    fn recipient(&self) -> Self::Address;
1657
1658    /// Recipient unified address as given by recipient and recorded in an encoded memo (all original receivers).
1659    fn recipient_full_unified_address(&self) -> Option<&UnifiedAddress>;
1660
1661    /// Encoded recipient address recorded in note on chain (single receiver).
1662    fn encoded_recipient<P>(&self, parameters: &P) -> Result<String, Self::Error>
1663    where
1664        P: consensus::Parameters + consensus::NetworkConstants;
1665
1666    /// Encoded recipient unified address as given by recipient and recorded in an encoded memo (all original receivers).
1667    fn encoded_recipient_full_unified_address<P>(&self, consensus_parameters: &P) -> Option<String>
1668    where
1669        P: consensus::Parameters + consensus::NetworkConstants;
1670
1671    /// Outgoing notes within `transaction`.
1672    fn transaction_outgoing_notes(transaction: &WalletTransaction) -> &[Self];
1673}
1674
1675/// Note sent from this capability to a recipient. `P` is the pool marker
1676/// ([`Sapling`], [`Orchard`] or [`Ironwood`]).
1677#[derive(Debug, Clone, PartialEq)]
1678pub struct OutgoingNote<N, P> {
1679    /// Output ID.
1680    pub(crate) output_id: OutputId,
1681    /// Identifier for key used to decrypt output.
1682    pub(crate) key_id: KeyId,
1683    /// Decrypted note with recipient and value.
1684    pub(crate) note: N,
1685    /// Memo.
1686    pub(crate) memo: Memo,
1687    /// Recipient's full unified address from encoded memo.
1688    pub(crate) recipient_full_unified_address: Option<UnifiedAddress>,
1689    /// Pool marker.
1690    pub(crate) marker: PhantomData<P>,
1691}
1692
1693/// Outgoing sapling note.
1694pub type OutgoingSaplingNote = OutgoingNote<sapling_crypto::Note, Sapling>;
1695
1696impl OutgoingNoteInterface for OutgoingSaplingNote {
1697    type ZcashNote = sapling_crypto::Note;
1698    type Address = sapling_crypto::PaymentAddress;
1699    type Error = Infallible;
1700
1701    const SHIELDED_PROTOCOL: ShieldedPool = ShieldedPool::Sapling;
1702
1703    fn output_id(&self) -> OutputId {
1704        self.output_id
1705    }
1706
1707    fn key_id(&self) -> KeyId {
1708        self.key_id
1709    }
1710
1711    fn value(&self) -> u64 {
1712        self.note.value().inner()
1713    }
1714
1715    fn note(&self) -> &Self::ZcashNote {
1716        &self.note
1717    }
1718
1719    fn memo(&self) -> &Memo {
1720        &self.memo
1721    }
1722
1723    fn recipient(&self) -> Self::Address {
1724        self.note.recipient()
1725    }
1726
1727    fn recipient_full_unified_address(&self) -> Option<&UnifiedAddress> {
1728        self.recipient_full_unified_address.as_ref()
1729    }
1730
1731    fn encoded_recipient<P>(&self, consensus_parameters: &P) -> Result<String, Self::Error>
1732    where
1733        P: consensus::Parameters + consensus::NetworkConstants,
1734    {
1735        Ok(encode_payment_address(
1736            zcash_address::constants::hrp_sapling_payment_address(
1737                consensus_parameters.network_type(),
1738            ),
1739            &self.note().recipient(),
1740        ))
1741    }
1742
1743    fn encoded_recipient_full_unified_address<P>(&self, consensus_parameters: &P) -> Option<String>
1744    where
1745        P: consensus::Parameters + consensus::NetworkConstants,
1746    {
1747        self.recipient_full_unified_address
1748            .as_ref()
1749            .map(|unified_address| unified_address.encode(consensus_parameters))
1750    }
1751
1752    fn transaction_outgoing_notes(transaction: &WalletTransaction) -> &[Self] {
1753        &transaction.outgoing_sapling_notes
1754    }
1755}
1756
1757/// Outgoing orchard note.
1758pub type OutgoingOrchardNote = OutgoingNote<orchard::Note, Orchard>;
1759
1760impl OutgoingNoteInterface for OutgoingOrchardNote {
1761    type ZcashNote = orchard::Note;
1762    type Address = orchard::Address;
1763    type Error = ParseError;
1764
1765    const SHIELDED_PROTOCOL: ShieldedPool = ShieldedPool::Orchard;
1766
1767    fn output_id(&self) -> OutputId {
1768        self.output_id
1769    }
1770
1771    fn key_id(&self) -> KeyId {
1772        self.key_id
1773    }
1774
1775    fn value(&self) -> u64 {
1776        self.note.value().inner()
1777    }
1778
1779    fn note(&self) -> &Self::ZcashNote {
1780        &self.note
1781    }
1782
1783    fn memo(&self) -> &Memo {
1784        &self.memo
1785    }
1786
1787    fn recipient(&self) -> Self::Address {
1788        self.note.recipient()
1789    }
1790
1791    fn recipient_full_unified_address(&self) -> Option<&UnifiedAddress> {
1792        self.recipient_full_unified_address.as_ref()
1793    }
1794
1795    fn encoded_recipient<P>(&self, parameters: &P) -> Result<String, Self::Error>
1796    where
1797        P: consensus::Parameters + consensus::NetworkConstants,
1798    {
1799        keys::encode_orchard_receiver(parameters, &self.note().recipient())
1800    }
1801
1802    fn encoded_recipient_full_unified_address<P>(&self, consensus_parameters: &P) -> Option<String>
1803    where
1804        P: consensus::Parameters + consensus::NetworkConstants,
1805    {
1806        self.recipient_full_unified_address
1807            .as_ref()
1808            .map(|unified_address| unified_address.encode(consensus_parameters))
1809    }
1810
1811    fn transaction_outgoing_notes(transaction: &WalletTransaction) -> &[Self] {
1812        &transaction.outgoing_orchard_notes
1813    }
1814}
1815
1816/// Outgoing ironwood note. Shares Orchard's note and receiver types.
1817pub type OutgoingIronwoodNote = OutgoingNote<orchard::Note, Ironwood>;
1818
1819impl OutgoingNoteInterface for OutgoingIronwoodNote {
1820    type ZcashNote = orchard::Note;
1821    type Address = orchard::Address;
1822    type Error = ParseError;
1823
1824    const SHIELDED_PROTOCOL: ShieldedPool = ShieldedPool::Ironwood;
1825
1826    fn output_id(&self) -> OutputId {
1827        self.output_id
1828    }
1829
1830    fn key_id(&self) -> KeyId {
1831        self.key_id
1832    }
1833
1834    fn value(&self) -> u64 {
1835        self.note.value().inner()
1836    }
1837
1838    fn note(&self) -> &Self::ZcashNote {
1839        &self.note
1840    }
1841
1842    fn memo(&self) -> &Memo {
1843        &self.memo
1844    }
1845
1846    fn recipient(&self) -> Self::Address {
1847        self.note.recipient()
1848    }
1849
1850    fn recipient_full_unified_address(&self) -> Option<&UnifiedAddress> {
1851        self.recipient_full_unified_address.as_ref()
1852    }
1853
1854    fn encoded_recipient<P>(&self, parameters: &P) -> Result<String, Self::Error>
1855    where
1856        P: consensus::Parameters + consensus::NetworkConstants,
1857    {
1858        keys::encode_orchard_receiver(parameters, &self.note().recipient())
1859    }
1860
1861    fn encoded_recipient_full_unified_address<P>(&self, consensus_parameters: &P) -> Option<String>
1862    where
1863        P: consensus::Parameters + consensus::NetworkConstants,
1864    {
1865        self.recipient_full_unified_address
1866            .as_ref()
1867            .map(|unified_address| unified_address.encode(consensus_parameters))
1868    }
1869
1870    fn transaction_outgoing_notes(transaction: &WalletTransaction) -> &[Self] {
1871        &transaction.outgoing_ironwood_notes
1872    }
1873}
1874
1875/// Reduces the wallet to what a build that never tracked `pool` would have
1876/// left behind: the very same scanned ranges, and none of that pool's data.
1877///
1878/// This is the state a wallet reaches by syncing across a pool's activation
1879/// on a build that predates the pool, which is not otherwise constructible
1880/// from outside. Scan-range priorities are deliberately untouched, since the
1881/// defect being reproduced is precisely that the ranges still claim to be
1882/// scanned.
1883#[cfg(any(test, feature = "test-features"))]
1884pub fn strip_pool_tracking_for_test<W>(wallet: &mut W, pool: ShieldedPool) -> Result<(), W::Error>
1885where
1886    W: traits::SyncWallet
1887        + traits::SyncBlocks
1888        + traits::SyncTransactions
1889        + traits::SyncNullifiers
1890        + traits::SyncShardTrees,
1891{
1892    for block in wallet.get_wallet_blocks_mut()?.values_mut() {
1893        let tree_bounds = &mut block.tree_bounds;
1894        match pool {
1895            ShieldedPool::Sapling => {
1896                tree_bounds.sapling_initial_tree_size = 0;
1897                tree_bounds.sapling_final_tree_size = 0;
1898            }
1899            ShieldedPool::Orchard => {
1900                tree_bounds.orchard_initial_tree_size = 0;
1901                tree_bounds.orchard_final_tree_size = 0;
1902            }
1903            ShieldedPool::Ironwood => {
1904                tree_bounds.ironwood_initial_tree_size = 0;
1905                tree_bounds.ironwood_final_tree_size = 0;
1906            }
1907        }
1908    }
1909    for transaction in wallet.get_wallet_transactions_mut()?.values_mut() {
1910        match pool {
1911            ShieldedPool::Sapling => {
1912                transaction.sapling_notes.clear();
1913                transaction.outgoing_sapling_notes.clear();
1914            }
1915            ShieldedPool::Orchard => {
1916                transaction.orchard_notes.clear();
1917                transaction.outgoing_orchard_notes.clear();
1918            }
1919            ShieldedPool::Ironwood => {
1920                transaction.ironwood_notes.clear();
1921                transaction.outgoing_ironwood_notes.clear();
1922            }
1923        }
1924    }
1925
1926    let nullifiers = wallet.get_nullifiers_mut()?;
1927    match pool {
1928        ShieldedPool::Sapling => nullifiers.sapling.clear(),
1929        ShieldedPool::Orchard => nullifiers.orchard.clear(),
1930        ShieldedPool::Ironwood => nullifiers.ironwood.clear(),
1931    }
1932
1933    let empty = ShardTrees::new();
1934    let shard_trees = wallet.get_shard_trees_mut()?;
1935    match pool {
1936        ShieldedPool::Sapling => shard_trees.sapling = empty.sapling,
1937        ShieldedPool::Orchard => shard_trees.orchard = empty.orchard,
1938        ShieldedPool::Ironwood => shard_trees.ironwood = empty.ironwood,
1939    }
1940
1941    let sync_state = wallet.get_sync_state_mut()?;
1942    match pool {
1943        ShieldedPool::Sapling => sync_state.sapling_shard_ranges.clear(),
1944        ShieldedPool::Orchard => sync_state.orchard_shard_ranges.clear(),
1945        ShieldedPool::Ironwood => sync_state.ironwood_shard_ranges.clear(),
1946    }
1947
1948    Ok(())
1949}
1950
1951// TODO: allow consumer to define shard store. memory shard store has infallible error type but other may not so error
1952// handling will need to replace expects
1953/// Type alias for sapling memory shard store
1954pub type SaplingShardStore = MemoryShardStore<sapling_crypto::Node, BlockHeight>;
1955
1956/// Type alias for orchard memory shard store
1957pub type OrchardShardStore = MemoryShardStore<MerkleHashOrchard, BlockHeight>;
1958
1959/// Shard tree wallet data struct
1960#[derive(Debug)]
1961pub struct ShardTrees {
1962    /// Sapling shard tree
1963    pub sapling: ShardTree<
1964        SaplingShardStore,
1965        { sapling_crypto::NOTE_COMMITMENT_TREE_DEPTH },
1966        { witness::SHARD_HEIGHT },
1967    >,
1968    /// Orchard shard tree
1969    pub orchard: ShardTree<
1970        OrchardShardStore,
1971        { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
1972        { witness::SHARD_HEIGHT },
1973    >,
1974    /// Ironwood shard tree. Ironwood reuses the Orchard note commitment tree
1975    /// structure (same hash and depth), so it has the same shape as `orchard`.
1976    pub ironwood: ShardTree<
1977        OrchardShardStore,
1978        { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
1979        { witness::SHARD_HEIGHT },
1980    >,
1981}
1982
1983impl ShardTrees {
1984    /// Create new `ShardTrees`
1985    #[must_use]
1986    pub fn new() -> Self {
1987        Self {
1988            sapling: empty_shard_tree(),
1989            orchard: empty_shard_tree(),
1990            ironwood: empty_shard_tree(),
1991        }
1992    }
1993
1994    /// Replaces one pool's shard tree with an empty tree, leaving the other
1995    /// pools alone.
1996    ///
1997    /// The empty tree is that pool's correct state at its own activation, so
1998    /// this is the rollback a pool needs when its recorded history cannot be
1999    /// trusted and no checkpoint survives to roll back to.
2000    pub(crate) fn clear_pool(&mut self, pool: ShieldedPool) {
2001        tracing::info!("Clearing {pool:?} shard tree.");
2002        match pool {
2003            ShieldedPool::Sapling => self.sapling = empty_shard_tree(),
2004            ShieldedPool::Orchard => self.orchard = empty_shard_tree(),
2005            ShieldedPool::Ironwood => self.ironwood = empty_shard_tree(),
2006        }
2007    }
2008}
2009
2010impl Default for ShardTrees {
2011    fn default() -> Self {
2012        Self::new()
2013    }
2014}
2015
2016/// An empty shard tree with the crate's standard checkpoint retention and
2017/// the zero-height checkpoint that every fresh tree must carry.
2018pub(crate) fn empty_shard_tree<H, const DEPTH: u8, const SHARD_HEIGHT: u8>()
2019-> ShardTree<MemoryShardStore<H, BlockHeight>, DEPTH, SHARD_HEIGHT>
2020where
2021    H: incrementalmerkletree::Hashable + Clone + PartialEq,
2022{
2023    let mut tree = ShardTree::new(
2024        MemoryShardStore::empty(),
2025        SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE as usize,
2026    );
2027
2028    // `NotAboveNewest` is impossible on an empty checkpoint store.
2029    assert_eq!(
2030        tree.append_checkpoint(BlockHeight::from_u32(0))
2031            .expect("should never fail"),
2032        CheckpointAppendOutcome::Appended
2033    );
2034
2035    tree
2036}
2037
2038use shardtree::store::ShardStore;
2039
2040pub(crate) trait SyncDomain {
2041    const SHIELDED_PROTOCOL: ShieldedPool;
2042
2043    type Note: NoteInterface;
2044    type ShardStore: ShardStore<CheckpointId = BlockHeight>;
2045
2046    fn notes_mut(wallet_transaction: &mut WalletTransaction) -> Vec<&mut Self::Note>;
2047}
2048
2049impl SyncDomain for Sapling {
2050    const SHIELDED_PROTOCOL: ShieldedPool = ShieldedPool::Sapling;
2051
2052    type Note = SaplingNote;
2053    type ShardStore = SaplingShardStore;
2054
2055    fn notes_mut(wallet_transaction: &mut WalletTransaction) -> Vec<&mut Self::Note> {
2056        wallet_transaction.sapling_notes_mut()
2057    }
2058}
2059
2060impl SyncDomain for Orchard {
2061    const SHIELDED_PROTOCOL: ShieldedPool = ShieldedPool::Orchard;
2062
2063    type Note = OrchardNote;
2064    type ShardStore = OrchardShardStore;
2065
2066    fn notes_mut(wallet_transaction: &mut WalletTransaction) -> Vec<&mut Self::Note> {
2067        wallet_transaction.orchard_notes_mut()
2068    }
2069}
2070
2071impl SyncDomain for Ironwood {
2072    const SHIELDED_PROTOCOL: ShieldedPool = ShieldedPool::Ironwood;
2073
2074    type Note = IronwoodNote;
2075    // Ironwood reuses the Orchard note commitment tree hash, so the same
2076    // store type serves its (separate) tree.
2077    type ShardStore = OrchardShardStore;
2078
2079    fn notes_mut(wallet_transaction: &mut WalletTransaction) -> Vec<&mut Self::Note> {
2080        wallet_transaction.ironwood_notes_mut()
2081    }
2082}
2083
2084#[cfg(test)]
2085mod test {
2086    use zcash_protocol::TxId;
2087    use zcash_protocol::consensus::BlockHeight;
2088    use zingo_status::confirmation_status::ConfirmationStatus;
2089
2090    use super::WalletTransaction;
2091
2092    fn transaction_with_status(status: ConfirmationStatus) -> WalletTransaction {
2093        WalletTransaction::new_for_test(TxId::from_bytes([0; 32]), status)
2094    }
2095
2096    #[test]
2097    fn update_status_follows_transaction_lifecycle() {
2098        let mut transaction =
2099            transaction_with_status(ConfirmationStatus::Calculated(BlockHeight::from_u32(10)));
2100
2101        transaction.update_status(
2102            ConfirmationStatus::Transmitted(BlockHeight::from_u32(10)),
2103            1,
2104            false,
2105        );
2106        assert!(matches!(
2107            transaction.status(),
2108            ConfirmationStatus::Transmitted(_)
2109        ));
2110
2111        transaction.update_status(
2112            ConfirmationStatus::Mempool(BlockHeight::from_u32(11)),
2113            2,
2114            false,
2115        );
2116        assert!(matches!(
2117            transaction.status(),
2118            ConfirmationStatus::Mempool(_)
2119        ));
2120
2121        transaction.update_status(
2122            ConfirmationStatus::Confirmed(BlockHeight::from_u32(11)),
2123            3,
2124            false,
2125        );
2126        assert!(matches!(
2127            transaction.status(),
2128            ConfirmationStatus::Confirmed(_)
2129        ));
2130    }
2131
2132    #[test]
2133    fn update_status_failed_returns_to_mempool() {
2134        let mut transaction =
2135            transaction_with_status(ConfirmationStatus::Failed(BlockHeight::from_u32(10)));
2136
2137        transaction.update_status(
2138            ConfirmationStatus::Mempool(BlockHeight::from_u32(11)),
2139            1,
2140            false,
2141        );
2142        assert!(matches!(
2143            transaction.status(),
2144            ConfirmationStatus::Mempool(_)
2145        ));
2146    }
2147
2148    #[test]
2149    fn update_status_failed_is_not_directly_confirmable() {
2150        // confirming a failed transaction is handled by replacing the whole record during block
2151        // scanning, not by a status update.
2152        let mut transaction =
2153            transaction_with_status(ConfirmationStatus::Failed(BlockHeight::from_u32(10)));
2154
2155        transaction.update_status(
2156            ConfirmationStatus::Confirmed(BlockHeight::from_u32(11)),
2157            1,
2158            false,
2159        );
2160        assert!(matches!(
2161            transaction.status(),
2162            ConfirmationStatus::Failed(_)
2163        ));
2164    }
2165
2166    #[test]
2167    fn update_status_confirmed_is_terminal() {
2168        let mut transaction =
2169            transaction_with_status(ConfirmationStatus::Confirmed(BlockHeight::from_u32(10)));
2170
2171        transaction.update_status(
2172            ConfirmationStatus::Mempool(BlockHeight::from_u32(11)),
2173            1,
2174            false,
2175        );
2176        assert!(matches!(
2177            transaction.status(),
2178            ConfirmationStatus::Confirmed(_)
2179        ));
2180
2181        transaction.update_status(
2182            ConfirmationStatus::Failed(BlockHeight::from_u32(11)),
2183            2,
2184            false,
2185        );
2186        assert!(matches!(
2187            transaction.status(),
2188            ConfirmationStatus::Confirmed(_)
2189        ));
2190    }
2191
2192    #[test]
2193    fn update_status_forces_confirmed_to_fail() {
2194        let mut transaction =
2195            transaction_with_status(ConfirmationStatus::Confirmed(BlockHeight::from_u32(10)));
2196
2197        transaction.update_status(
2198            ConfirmationStatus::Failed(BlockHeight::from_u32(11)),
2199            2,
2200            true,
2201        );
2202        assert!(matches!(
2203            transaction.status(),
2204            ConfirmationStatus::Failed(_)
2205        ));
2206    }
2207}