1use 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#[derive(Debug, Clone, Copy)]
56pub struct ScanTarget {
57 block_height: BlockHeight,
58 txid: Option<TxId>,
59 pools: ScanTargetPools,
60 origin: ScanTargetOrigin,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub(crate) enum ScanTargetOrigin {
66 Internal,
69 External,
72}
73
74#[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 #[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 #[must_use]
145 pub fn block_height(&self) -> BlockHeight {
146 self.block_height
147 }
148
149 #[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 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#[derive(Debug, Clone)]
209pub struct InitialSyncState {
210 pub(crate) sync_start_height: BlockHeight,
215 pub(crate) wallet_tree_bounds: TreeBounds,
217 pub(crate) previously_scanned_blocks: u32,
219 pub(crate) previously_scanned_sapling_outputs: u32,
221 pub(crate) previously_scanned_orchard_outputs: u32,
223 pub(crate) previously_scanned_ironwood_outputs: u32,
225}
226
227impl InitialSyncState {
228 #[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#[derive(Debug, Clone)]
257pub struct SyncState {
258 pub(crate) scan_ranges: Vec<ScanRange>,
261 pub(crate) sapling_shard_ranges: Vec<Range<BlockHeight>>,
266 pub(crate) orchard_shard_ranges: Vec<Range<BlockHeight>>,
271 pub(crate) ironwood_shard_ranges: Vec<Range<BlockHeight>>,
276 pub(crate) scan_targets: BTreeSet<ScanTarget>,
278 pub(crate) initial_sync_state: InitialSyncState,
280 pub(crate) transparent_scan_floor: Option<BlockHeight>,
291}
292
293impl SyncState {
294 #[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 #[must_use]
310 pub fn scan_ranges(&self) -> &[ScanRange] {
311 &self.scan_ranges
312 }
313
314 #[must_use]
316 pub fn sapling_shard_ranges(&self) -> &[Range<BlockHeight>] {
317 &self.sapling_shard_ranges
318 }
319
320 #[must_use]
322 pub fn orchard_shard_ranges(&self) -> &[Range<BlockHeight>] {
323 &self.orchard_shard_ranges
324 }
325
326 #[must_use]
328 pub fn ironwood_shard_ranges(&self) -> &[Range<BlockHeight>] {
329 &self.ironwood_shard_ranges
330 }
331
332 #[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 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 #[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 #[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 #[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 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 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
417pub enum SyncMode {
418 NotRunning,
420 Paused,
422 Running,
424 Shutdown,
426}
427
428impl SyncMode {
429 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 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 pub fn on_completion(self) -> Self {
449 match self {
450 Self::Running => Self::Shutdown,
451 other => other,
452 }
453 }
454
455 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 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#[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#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
497pub struct OutputId {
498 txid: TxId,
500 output_index: u32,
502}
503
504impl OutputId {
505 #[must_use]
507 pub fn new(txid: TxId, output_index: u32) -> Self {
508 OutputId { txid, output_index }
509 }
510
511 #[must_use]
513 pub fn txid(&self) -> TxId {
514 self.txid
515 }
516
517 #[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#[derive(Debug)]
551pub struct NullifierMap {
552 pub sapling: BTreeMap<sapling_crypto::Nullifier, ScanTarget>,
554 pub orchard: BTreeMap<orchard::note::Nullifier, ScanTarget>,
556 pub ironwood: BTreeMap<orchard::note::Nullifier, ScanTarget>,
558}
559
560impl NullifierMap {
561 #[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 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#[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 #[must_use]
621 pub fn block_height(&self) -> BlockHeight {
622 self.block_height
623 }
624
625 #[must_use]
627 pub fn block_hash(&self) -> BlockHash {
628 self.block_hash
629 }
630
631 #[must_use]
633 pub fn prev_hash(&self) -> BlockHash {
634 self.prev_hash
635 }
636
637 #[must_use]
639 pub fn time(&self) -> u32 {
640 self.time
641 }
642
643 #[must_use]
645 pub fn txids(&self) -> &[TxId] {
646 &self.txids
647 }
648
649 #[must_use]
651 pub fn tree_bounds(&self) -> TreeBounds {
652 self.tree_bounds
653 }
654}
655
656pub 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 #[must_use]
674 pub fn txid(&self) -> TxId {
675 self.txid
676 }
677
678 #[must_use]
680 pub fn status(&self) -> ConfirmationStatus {
681 self.status
682 }
683
684 #[must_use]
686 pub fn transaction(&self) -> &zcash_primitives::transaction::Transaction {
687 &self.transaction
688 }
689
690 #[must_use]
692 pub fn datetime(&self) -> u32 {
693 self.datetime
694 }
695
696 #[must_use]
698 pub fn transparent_coins(&self) -> &[TransparentCoin] {
699 &self.transparent_coins
700 }
701
702 pub fn transparent_coins_mut(&mut self) -> Vec<&mut TransparentCoin> {
704 self.transparent_coins.iter_mut().collect()
705 }
706
707 #[must_use]
709 pub fn sapling_notes(&self) -> &[SaplingNote] {
710 &self.sapling_notes
711 }
712
713 pub fn sapling_notes_mut(&mut self) -> Vec<&mut SaplingNote> {
715 self.sapling_notes.iter_mut().collect()
716 }
717
718 #[must_use]
720 pub fn orchard_notes(&self) -> &[OrchardNote] {
721 &self.orchard_notes
722 }
723
724 pub fn orchard_notes_mut(&mut self) -> Vec<&mut OrchardNote> {
726 self.orchard_notes.iter_mut().collect()
727 }
728
729 #[must_use]
731 pub fn ironwood_notes(&self) -> &[IronwoodNote] {
732 &self.ironwood_notes
733 }
734
735 pub fn ironwood_notes_mut(&mut self) -> Vec<&mut IronwoodNote> {
737 self.ironwood_notes.iter_mut().collect()
738 }
739
740 #[must_use]
742 pub fn outgoing_sapling_notes(&self) -> &[OutgoingSaplingNote] {
743 &self.outgoing_sapling_notes
744 }
745
746 #[must_use]
748 pub fn outgoing_orchard_notes(&self) -> &[OutgoingOrchardNote] {
749 &self.outgoing_orchard_notes
750 }
751
752 #[must_use]
754 pub fn outgoing_ironwood_notes(&self) -> &[OutgoingIronwoodNote] {
755 &self.outgoing_ironwood_notes
756 }
757
758 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 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 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 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 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 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 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 #[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 #[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 #[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 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 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 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 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 #[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 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 #[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 #[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 #[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 #[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 #[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
1132pub trait KeyIdInterface {
1134 fn account_id(&self) -> zip32::AccountId;
1136}
1137
1138pub trait OutputInterface: Sized {
1140 type KeyId: KeyIdInterface;
1142 type Input: Clone + Debug + PartialEq + Eq + PartialOrd + Ord;
1144
1145 const POOL_TYPE: PoolType;
1147
1148 fn output_id(&self) -> OutputId;
1150
1151 fn key_id(&self) -> Self::KeyId;
1153
1154 fn spending_transaction(&self) -> Option<TxId>;
1157
1158 fn set_spending_transaction(&mut self, spending_transaction: Option<TxId>);
1160
1161 fn value(&self) -> u64;
1164
1165 fn spend_link(&self) -> Option<Self::Input>;
1171
1172 fn transaction_inputs(transaction: &WalletTransaction) -> Vec<&Self::Input>;
1177
1178 fn transaction_outputs(transaction: &WalletTransaction) -> &[Self];
1180}
1181
1182pub trait NoteInterface: OutputInterface {
1184 type ZcashNote;
1186 type Nullifier: Copy + Clone + PartialEq + Eq + PartialOrd + Ord;
1188
1189 const SHIELDED_PROTOCOL: ShieldedPool;
1191
1192 fn note(&self) -> &Self::ZcashNote;
1194
1195 fn nullifier(&self) -> Option<Self::Nullifier>;
1197
1198 fn position(&self) -> Option<Position>;
1200
1201 fn memo(&self) -> &Memo;
1203
1204 fn refetch_nullifier_ranges(&self) -> &[Range<BlockHeight>];
1209}
1210
1211#[derive(Debug, Clone)]
1213pub struct TransparentCoin {
1214 pub(crate) output_id: OutputId,
1216 pub(crate) key_id: TransparentAddressId,
1218 pub(crate) address: String,
1220 pub(crate) script: Script,
1222 pub(crate) value: Zatoshis,
1224 pub(crate) spending_transaction: Option<TxId>,
1227}
1228
1229#[cfg(feature = "test-features")]
1230impl TransparentCoin {
1231 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 #[must_use]
1256 pub fn address(&self) -> &str {
1257 &self.address
1258 }
1259
1260 #[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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1333pub struct PoolActivation(BlockHeight);
1334
1335impl PoolActivation {
1336 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 #[must_use]
1352 pub fn height(&self) -> BlockHeight {
1353 self.0
1354 }
1355
1356 #[must_use]
1362 pub fn max_with(&self, height: BlockHeight) -> BlockHeight {
1363 self.0.max(height)
1364 }
1365
1366 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1379pub struct Sapling;
1380
1381#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1384pub struct Orchard;
1385
1386#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1390pub struct Ironwood;
1391
1392#[derive(Debug, Clone)]
1395pub struct WalletNote<N, Nf: Copy, P> {
1396 pub(crate) output_id: OutputId,
1398 pub(crate) key_id: KeyId,
1400 pub(crate) note: N,
1402 pub(crate) nullifier: Option<Nf>, pub(crate) position: Option<Position>,
1406 pub(crate) memo: Memo,
1408 pub(crate) spending_transaction: Option<TxId>,
1411 pub(crate) refetch_nullifier_ranges: Vec<Range<BlockHeight>>,
1416 pub(crate) marker: PhantomData<P>,
1418}
1419
1420pub 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
1489pub 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
1558pub 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
1628pub trait OutgoingNoteInterface: Sized {
1630 type ZcashNote;
1632 type Address: Clone + Copy + Debug + PartialEq + Eq;
1634 type Error: Debug + std::error::Error;
1636
1637 const SHIELDED_PROTOCOL: ShieldedPool;
1639
1640 fn output_id(&self) -> OutputId;
1642
1643 fn key_id(&self) -> KeyId;
1645
1646 fn value(&self) -> u64;
1648
1649 fn note(&self) -> &Self::ZcashNote;
1651
1652 fn memo(&self) -> &Memo;
1654
1655 fn recipient(&self) -> Self::Address;
1657
1658 fn recipient_full_unified_address(&self) -> Option<&UnifiedAddress>;
1660
1661 fn encoded_recipient<P>(&self, parameters: &P) -> Result<String, Self::Error>
1663 where
1664 P: consensus::Parameters + consensus::NetworkConstants;
1665
1666 fn encoded_recipient_full_unified_address<P>(&self, consensus_parameters: &P) -> Option<String>
1668 where
1669 P: consensus::Parameters + consensus::NetworkConstants;
1670
1671 fn transaction_outgoing_notes(transaction: &WalletTransaction) -> &[Self];
1673}
1674
1675#[derive(Debug, Clone, PartialEq)]
1678pub struct OutgoingNote<N, P> {
1679 pub(crate) output_id: OutputId,
1681 pub(crate) key_id: KeyId,
1683 pub(crate) note: N,
1685 pub(crate) memo: Memo,
1687 pub(crate) recipient_full_unified_address: Option<UnifiedAddress>,
1689 pub(crate) marker: PhantomData<P>,
1691}
1692
1693pub 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
1757pub 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
1816pub 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#[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
1951pub type SaplingShardStore = MemoryShardStore<sapling_crypto::Node, BlockHeight>;
1955
1956pub type OrchardShardStore = MemoryShardStore<MerkleHashOrchard, BlockHeight>;
1958
1959#[derive(Debug)]
1961pub struct ShardTrees {
1962 pub sapling: ShardTree<
1964 SaplingShardStore,
1965 { sapling_crypto::NOTE_COMMITMENT_TREE_DEPTH },
1966 { witness::SHARD_HEIGHT },
1967 >,
1968 pub orchard: ShardTree<
1970 OrchardShardStore,
1971 { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
1972 { witness::SHARD_HEIGHT },
1973 >,
1974 pub ironwood: ShardTree<
1977 OrchardShardStore,
1978 { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
1979 { witness::SHARD_HEIGHT },
1980 >,
1981}
1982
1983impl ShardTrees {
1984 #[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 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
2016pub(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 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 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 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}