1use std::{
4 cell::Cell,
5 collections::{BTreeMap, HashMap, HashSet},
6 convert::Infallible,
7 fmt,
8 hash::Hash,
9 num::NonZeroU32,
10 ops::Range,
11 time::Duration,
12};
13
14#[cfg(feature = "pczt")]
15use super::wallet::{create_pczt_from_proposal, extract_and_store_transaction_from_pczt};
16use assert_matches::assert_matches;
17use group::ff::Field;
18use incrementalmerkletree::{Marking, Position, Retention};
19use nonempty::NonEmpty;
20use rand::{CryptoRng, Rng, RngExt, SeedableRng};
21use rand_chacha::ChaChaRng;
22use secrecy::{ExposeSecret, Secret, SecretVec};
23use shardtree::{
24 ShardTree,
25 error::ShardTreeError,
26 store::{ShardStore as _, memory::MemoryShardStore},
27};
28use subtle::ConditionallySelectable;
29#[cfg(feature = "transparent-inputs")]
30use {
31 super::{
32 CoinbaseFilter, TransactionsInvolvingAddress, TransparentBalances,
33 wallet::{
34 input_selection::ShieldingSelector, propose_shielding, propose_shielding_coinbase,
35 shield_transparent_funds,
36 },
37 },
38 crate::wallet::TransparentAddressMetadata,
39 ::transparent::{
40 address::{Script, TransparentAddress},
41 bundle::OutPoint,
42 keys::TransparentKeyScope,
43 },
44 zcash_keys::keys::transparent::gap_limits::GapLimits,
45};
46
47use ::sapling::{
48 Rseed,
49 note_encryption::{SaplingDomain, sapling_note_encryption},
50 prover::mock::{MockOutputProver, MockSpendProver},
51 util::generate_random_rseed,
52 value::NoteValue as SaplingNoteValue,
53 zip32::{DiversifiableFullViewingKey, ExtendedSpendingKey},
54};
55use zcash_address::ZcashAddress;
56use zcash_keys::{
57 address::{Address, UnifiedAddress},
58 keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey},
59};
60use zcash_note_encryption::Domain;
61use zcash_primitives::{
62 block::BlockHash,
63 transaction::{Transaction, TxId, components::sapling::zip212_enforcement, fees::FeeRule},
64};
65#[cfg(feature = "pczt")]
66use zcash_proofs::prover::LocalTxProver;
67use zcash_protocol::{
68 ShieldedPool,
69 consensus::{self, BlockHeight, Network, NetworkUpgrade, Parameters as _},
70 local_consensus::LocalNetwork,
71 memo::{Memo, MemoBytes},
72 value::{ZatBalance, Zatoshis},
73};
74#[cfg(feature = "transparent-key-import")]
75use zcash_script::script;
76use zip32::{DiversifierIndex, Scope};
77use zip321::Payment;
78#[cfg(feature = "orchard")]
79use {
80 super::ORCHARD_SHARD_HEIGHT,
81 crate::proto::compact_formats::CompactOrchardAction,
82 ::orchard::{
83 keys::{FullViewingKey as OrchardFullViewingKey, SpendingKey as OrchardSpendingKey},
84 note::{ExtractedNoteCommitment, Note as OrchardNote, NoteVersion, RandomSeed, Rho},
85 note_encryption::{IronwoodDomain, IronwoodNoteEncryption},
86 tree::MerkleHashOrchard,
87 value::NoteValue as OrchardNoteValue,
88 },
89 group::ff::PrimeField,
90 pasta_curves::pallas,
91 zcash_note_encryption::ShieldedOutput,
92 zcash_protocol::consensus::NetworkConstants,
93};
94
95use super::{
96 Account, AccountBalance, AccountBirthday, AccountMeta, AccountPurpose, AccountSource,
97 AddressInfo, BlockMetadata, ChangeReceivers, DecryptedTransaction, InputSource, NoteFilter,
98 NullifierQuery, ReceivedNotes, ReceivedTransactionOutput, SAPLING_SHARD_HEIGHT, ScannedBlock,
99 SeedRelevance, SentTransaction, TransactionDataRequest, TransactionStatus,
100 WalletCommitmentTrees, WalletRead, WalletSummary, WalletTest, WalletWrite, Zip32Derivation,
101 anchor_retention::AnchorRetentionInterval,
102 chain::{BlockSource, ChainState, CommitmentTreeRoot, ScanSummary, scan_cached_blocks},
103 error::Error,
104 scanning::{ScanPriority, ScanRange},
105 wallet::{
106 ConfirmationsPolicy, SpendingKeys, create_proposed_transactions,
107 input_selection::{
108 GreedyInputSelector, InputSelector, LockFilter, LockedInputPolicy, SpendPolicy,
109 },
110 propose_send_max_transfer, propose_standard_transfer_to_address, propose_transfer,
111 },
112};
113
114#[cfg(feature = "pczt")]
115fn real_test_prover() -> &'static LocalTxProver {
116 use std::sync::OnceLock;
117
118 static PROVER: OnceLock<LocalTxProver> = OnceLock::new();
119 PROVER.get_or_init(LocalTxProver::bundled)
120}
121use crate::{
122 data_api::{
123 MaxSpendMode, OutputLockStore, TargetValue,
124 error::{LockError, RewindError},
125 wallet::TargetHeight,
126 },
127 fees::{
128 ChangeStrategy, DustOutputPolicy, StandardFeeRule,
129 standard::{self, SingleOutputChangeStrategy},
130 },
131 proposal::Proposal,
132 proto::compact_formats::{
133 self, CompactBlock, CompactSaplingOutput, CompactSaplingSpend, CompactTx,
134 },
135 util::testing::FixedClock,
136 wallet::{
137 LockOwner, Note, NoteId, OutputRef, OvkPolicy, ReceivedNote, WalletTransparentOutput,
138 },
139};
140
141pub mod pool;
142pub mod sapling;
143
144#[cfg(feature = "orchard")]
145pub mod orchard;
146#[cfg(feature = "transparent-inputs")]
147pub mod transparent;
148
149#[derive(Debug)]
151pub struct TransactionSummary<AccountId> {
152 account_id: AccountId,
153 txid: TxId,
154 expiry_height: Option<BlockHeight>,
155 mined_height: Option<BlockHeight>,
156 account_value_delta: ZatBalance,
157 total_spent: Zatoshis,
158 total_received: Zatoshis,
159 fee_paid: Option<Zatoshis>,
160 spent_note_count: usize,
161 has_change: bool,
162 sent_note_count: usize,
163 received_note_count: usize,
164 memo_count: usize,
165 expired_unmined: bool,
166 is_shielding: bool,
167 pool_crossing_value: Option<Zatoshis>,
168}
169
170impl<AccountId> TransactionSummary<AccountId> {
171 #[allow(clippy::too_many_arguments)]
176 pub fn from_parts(
177 account_id: AccountId,
178 txid: TxId,
179 expiry_height: Option<BlockHeight>,
180 mined_height: Option<BlockHeight>,
181 account_value_delta: ZatBalance,
182 total_spent: Zatoshis,
183 total_received: Zatoshis,
184 fee_paid: Option<Zatoshis>,
185 spent_note_count: usize,
186 has_change: bool,
187 sent_note_count: usize,
188 received_note_count: usize,
189 memo_count: usize,
190 expired_unmined: bool,
191 is_shielding: bool,
192 pool_crossing_value: Option<Zatoshis>,
193 ) -> Self {
194 Self {
195 account_id,
196 txid,
197 expiry_height,
198 mined_height,
199 account_value_delta,
200 total_spent,
201 total_received,
202 fee_paid,
203 spent_note_count,
204 has_change,
205 sent_note_count,
206 received_note_count,
207 memo_count,
208 expired_unmined,
209 is_shielding,
210 pool_crossing_value,
211 }
212 }
213
214 pub fn account_id(&self) -> &AccountId {
217 &self.account_id
218 }
219
220 pub fn txid(&self) -> TxId {
222 self.txid
223 }
224
225 pub fn expiry_height(&self) -> Option<BlockHeight> {
230 self.expiry_height
231 }
232
233 pub fn mined_height(&self) -> Option<BlockHeight> {
236 self.mined_height
237 }
238
239 pub fn account_value_delta(&self) -> ZatBalance {
244 self.account_value_delta
245 }
246
247 pub fn total_spent(&self) -> Zatoshis {
249 self.total_spent
250 }
251
252 pub fn total_received(&self) -> Zatoshis {
254 self.total_received
255 }
256
257 pub fn fee_paid(&self) -> Option<Zatoshis> {
259 self.fee_paid
260 }
261
262 pub fn spent_note_count(&self) -> usize {
264 self.spent_note_count
265 }
266
267 pub fn has_change(&self) -> bool {
271 self.has_change
272 }
273
274 pub fn sent_note_count(&self) -> usize {
277 self.sent_note_count
278 }
279
280 pub fn received_note_count(&self) -> usize {
283 self.received_note_count
284 }
285
286 pub fn expired_unmined(&self) -> bool {
289 self.expired_unmined
290 }
291
292 pub fn memo_count(&self) -> usize {
294 self.memo_count
295 }
296
297 pub fn is_shielding(&self) -> bool {
310 self.is_shielding
311 }
312
313 pub fn is_pool_crossing(&self) -> bool {
330 self.pool_crossing_value.is_some()
331 }
332
333 pub fn pool_crossing_value(&self) -> Option<Zatoshis> {
337 self.pool_crossing_value
338 }
339}
340
341#[derive(Clone, Debug)]
343pub struct CachedBlock {
344 chain_state: ChainState,
345 sapling_end_size: u32,
346 orchard_end_size: u32,
347 ironwood_end_size: u32,
348}
349
350impl CachedBlock {
351 pub fn none(block_height: BlockHeight) -> Self {
357 Self {
358 chain_state: ChainState::empty(block_height, BlockHash([0; 32])),
359 sapling_end_size: 0,
360 orchard_end_size: 0,
361 ironwood_end_size: 0,
362 }
363 }
364
365 pub fn at(
367 chain_state: ChainState,
368 sapling_end_size: u32,
369 orchard_end_size: u32,
370 ironwood_end_size: u32,
371 ) -> Self {
372 assert_eq!(
373 chain_state.final_sapling_tree().tree_size() as u32,
374 sapling_end_size
375 );
376 #[cfg(feature = "orchard")]
377 assert_eq!(
378 chain_state.final_orchard_tree().tree_size() as u32,
379 orchard_end_size
380 );
381 #[cfg(feature = "orchard")]
382 assert_eq!(
383 chain_state.final_ironwood_tree().tree_size() as u32,
384 ironwood_end_size
385 );
386
387 Self {
388 chain_state,
389 sapling_end_size,
390 orchard_end_size,
391 ironwood_end_size,
392 }
393 }
394
395 fn roll_forward(&self, cb: &CompactBlock) -> Self {
396 assert_eq!(self.chain_state.block_height() + 1, cb.height());
397
398 let sapling_final_tree = cb.vtx.iter().flat_map(|tx| tx.outputs.iter()).fold(
399 self.chain_state.final_sapling_tree().clone(),
400 |mut acc, c_out| {
401 acc.append(::sapling::Node::from_cmu(&c_out.cmu().unwrap()));
402 acc
403 },
404 );
405 let sapling_end_size = sapling_final_tree.tree_size() as u32;
406
407 #[cfg(feature = "orchard")]
408 let orchard_final_tree = cb.vtx.iter().flat_map(|tx| tx.actions.iter()).fold(
409 self.chain_state.final_orchard_tree().clone(),
410 |mut acc, c_act| {
411 acc.append(MerkleHashOrchard::from_cmx(&c_act.cmx().unwrap()));
412 acc
413 },
414 );
415 #[cfg(feature = "orchard")]
416 let orchard_end_size = orchard_final_tree.tree_size() as u32;
417 #[cfg(not(feature = "orchard"))]
418 let orchard_end_size = cb.vtx.iter().fold(self.orchard_end_size, |sz, tx| {
419 sz + (tx.actions.len() as u32)
420 });
421
422 #[cfg(feature = "orchard")]
423 let ironwood_final_tree = cb
424 .vtx
425 .iter()
426 .flat_map(|tx| tx.ironwood_actions.iter())
427 .fold(
428 self.chain_state.final_ironwood_tree().clone(),
429 |mut acc, c_act| {
430 acc.append(MerkleHashOrchard::from_cmx(&c_act.cmx().unwrap()));
431 acc
432 },
433 );
434 #[cfg(feature = "orchard")]
435 let ironwood_end_size = ironwood_final_tree.tree_size() as u32;
436 #[cfg(not(feature = "orchard"))]
437 let ironwood_end_size = cb.vtx.iter().fold(self.ironwood_end_size, |sz, tx| {
438 sz + (tx.ironwood_actions.len() as u32)
439 });
440
441 Self {
442 chain_state: ChainState::new(
443 cb.height(),
444 cb.hash(),
445 sapling_final_tree,
446 #[cfg(feature = "orchard")]
447 orchard_final_tree,
448 #[cfg(feature = "orchard")]
449 ironwood_final_tree,
450 ),
451 sapling_end_size,
452 orchard_end_size,
453 ironwood_end_size,
454 }
455 }
456
457 pub fn chain_state(&self) -> &ChainState {
459 &self.chain_state
460 }
461
462 pub fn height(&self) -> BlockHeight {
464 self.chain_state.block_height()
465 }
466
467 pub fn sapling_end_size(&self) -> u32 {
469 self.sapling_end_size
470 }
471
472 pub fn orchard_end_size(&self) -> u32 {
474 self.orchard_end_size
475 }
476
477 pub fn ironwood_end_size(&self) -> u32 {
479 self.ironwood_end_size
480 }
481}
482
483#[derive(Clone)]
489pub struct TestAccount<A> {
490 account: A,
491 usk: UnifiedSpendingKey,
492 birthday: AccountBirthday,
493}
494
495impl<A> TestAccount<A> {
496 pub fn account(&self) -> &A {
498 &self.account
499 }
500
501 pub fn usk(&self) -> &UnifiedSpendingKey {
503 &self.usk
504 }
505
506 pub fn birthday(&self) -> &AccountBirthday {
508 &self.birthday
509 }
510}
511
512impl<A: Account> Account for TestAccount<A> {
513 type AccountId = A::AccountId;
514
515 fn id(&self) -> Self::AccountId {
516 self.account.id()
517 }
518
519 fn name(&self) -> Option<&str> {
520 self.account.name()
521 }
522
523 fn birthday_height(&self) -> BlockHeight {
524 self.account.birthday_height()
525 }
526
527 fn source(&self) -> &AccountSource {
528 self.account.source()
529 }
530
531 fn ufvk(&self) -> Option<&zcash_keys::keys::UnifiedFullViewingKey> {
532 self.account.ufvk()
533 }
534
535 fn uivk(&self) -> zcash_keys::keys::UnifiedIncomingViewingKey {
536 self.account.uivk()
537 }
538}
539
540pub trait Reset: WalletTest + Sized {
543 type Handle;
545
546 fn reset<C>(st: &mut TestState<C, Self, LocalNetwork>) -> Self::Handle;
553}
554
555const TEST_CLOCK_EPOCH_OFFSET: Duration = Duration::from_secs(1740441600);
559
560pub struct TestState<Cache, DataStore: WalletTest, Network> {
561 cache: Cache,
562 cached_blocks: BTreeMap<BlockHeight, CachedBlock>,
563 latest_block_height: Option<BlockHeight>,
564 wallet_data: DataStore,
565 network: Network,
566 test_account: Option<(SecretVec<u8>, TestAccount<DataStore::Account>)>,
567 rng: ChaChaRng,
568 clock: FixedClock,
569}
570
571impl<Cache, DataStore: WalletTest, Network> TestState<Cache, DataStore, Network> {
572 pub fn clock(&self) -> &FixedClock {
574 &self.clock
575 }
576
577 pub fn wallet(&self) -> &DataStore {
579 &self.wallet_data
580 }
581
582 pub fn wallet_mut(&mut self) -> &mut DataStore {
584 &mut self.wallet_data
585 }
586
587 pub fn rng_mut(&mut self) -> &mut ChaChaRng {
589 &mut self.rng
590 }
591
592 pub fn network(&self) -> &Network {
594 &self.network
595 }
596}
597
598impl<Cache, DataStore: WalletTest, Network> Drop for TestState<Cache, DataStore, Network> {
599 fn drop(&mut self) {
600 self.wallet_data.finally();
601 }
602}
603
604impl<Cache, DataStore: WalletTest, Network: consensus::Parameters>
605 TestState<Cache, DataStore, Network>
606{
607 pub fn sapling_activation_height(&self) -> BlockHeight {
609 self.network
610 .activation_height(NetworkUpgrade::Sapling)
611 .expect("Sapling activation height must be known.")
612 }
613
614 #[allow(dead_code)]
616 pub fn nu5_activation_height(&self) -> BlockHeight {
617 self.network
618 .activation_height(NetworkUpgrade::Nu5)
619 .expect("NU5 activation height must be known.")
620 }
621
622 pub fn test_seed(&self) -> Option<&SecretVec<u8>> {
624 self.test_account.as_ref().map(|(seed, _)| seed)
625 }
626
627 pub fn test_account(&self) -> Option<&TestAccount<<DataStore as WalletRead>::Account>> {
629 self.test_account.as_ref().map(|(_, acct)| acct)
630 }
631
632 pub fn test_account_sapling(&self) -> Option<&DiversifiableFullViewingKey> {
634 let (_, acct) = self.test_account.as_ref()?;
635 let ufvk = acct.ufvk()?;
636 ufvk.sapling()
637 }
638
639 #[cfg(feature = "orchard")]
641 pub fn test_account_orchard(&self) -> Option<&::orchard::keys::FullViewingKey> {
642 let (_, acct) = self.test_account.as_ref()?;
643 let ufvk = acct.ufvk()?;
644 ufvk.orchard()
645 }
646}
647
648impl<Cache, DataStore, Network> TestState<Cache, DataStore, Network>
649where
650 DataStore: WalletTest + WalletWrite,
651 Network: consensus::Parameters,
652{
653 pub fn create_account_from_test_seed(
663 &mut self,
664 account_name: &str,
665 ) -> (<DataStore as WalletRead>::AccountId, UnifiedSpendingKey) {
666 let seed = SecretVec::new(
667 self.test_seed()
668 .expect("the test state was built with a seed")
669 .expose_secret()
670 .clone(),
671 );
672 let birthday = self
673 .test_account()
674 .expect("the test state was built with an account")
675 .birthday()
676 .clone();
677 self.wallet_mut()
678 .create_account(account_name, &seed, &birthday, None)
679 .expect("creates a further account under the test seed")
680 }
681}
682
683impl<Cache: TestCache, DataStore, Network> TestState<Cache, DataStore, Network>
684where
685 Network: consensus::Parameters,
686 DataStore: WalletTest + WalletWrite,
687 <Cache::BlockSource as BlockSource>::Error: fmt::Debug,
688{
689 #[cfg(feature = "unstable")]
691 pub fn cache(&self) -> &Cache::BlockSource {
692 self.cache.block_source()
693 }
694
695 pub fn latest_cached_block(&self) -> Option<&CachedBlock> {
698 self.latest_block_height
699 .as_ref()
700 .and_then(|h| self.cached_blocks.get(h))
701 }
702
703 fn latest_cached_block_below_height(&self, height: BlockHeight) -> Option<&CachedBlock> {
704 self.cached_blocks.range(..height).last().map(|(_, b)| b)
705 }
706
707 fn cache_block(
708 &mut self,
709 prev_block: &CachedBlock,
710 compact_block: CompactBlock,
711 ) -> Cache::InsertResult {
712 self.cached_blocks.insert(
713 compact_block.height(),
714 prev_block.roll_forward(&compact_block),
715 );
716 self.cache.insert(&compact_block)
717 }
718
719 pub fn generate_next_block<Fvk: TestFvk>(
724 &mut self,
725 recipient_fvk: &Fvk,
726 recipient_address_type: AddressType,
727 value: Zatoshis,
728 ) -> (BlockHeight, Cache::InsertResult, Fvk::Nullifier) {
729 let (height, res, nfs) = self.generate_next_block_multi(&[FakeCompactOutput::new(
730 recipient_fvk,
731 recipient_address_type,
732 value,
733 )]);
734
735 (height, res, nfs[0])
736 }
737
738 pub fn generate_next_block_multi<Fvk: TestFvk>(
741 &mut self,
742 outputs: &[FakeCompactOutput<Fvk>],
743 ) -> (BlockHeight, Cache::InsertResult, Vec<Fvk::Nullifier>) {
744 let pre_activation_block = CachedBlock::none(self.sapling_activation_height() - 1);
745 let prior_cached_block = self.latest_cached_block().unwrap_or(&pre_activation_block);
746 let height = prior_cached_block.height() + 1;
747
748 let (res, nfs) = self.generate_block_at(
749 height,
750 prior_cached_block.chain_state.block_hash(),
751 outputs,
752 prior_cached_block.sapling_end_size,
753 prior_cached_block.orchard_end_size,
754 prior_cached_block.ironwood_end_size,
755 false,
756 );
757
758 (height, res, nfs)
759 }
760
761 #[cfg(feature = "transparent-inputs")]
769 pub fn generate_next_block_transparent(
770 &mut self,
771 vin: &[OutPoint],
772 vout: &[(TransparentAddress, Zatoshis)],
773 ) -> (BlockHeight, Cache::InsertResult, TxId) {
774 let pre_activation_block = CachedBlock::none(self.sapling_activation_height() - 1);
775 let prior_cached_block = self
776 .latest_cached_block()
777 .unwrap_or(&pre_activation_block)
778 .clone();
779 let height = prior_cached_block.height() + 1;
780
781 let mut coinbase = fake_compact_tx(&mut self.rng);
782 coinbase.index = 0;
783
784 let mut ctx = fake_compact_tx(&mut self.rng);
785 ctx.index = 1;
786 ctx.vin = vin
787 .iter()
788 .map(|outpoint| compact_formats::CompactTxIn {
789 prevout_txid: outpoint.hash().to_vec(),
790 prevout_index: outpoint.n(),
791 })
792 .collect();
793 ctx.vout = vout
794 .iter()
795 .map(|(addr, value)| compact_formats::TxOut {
796 value: value.into_u64(),
797 script_pub_key: Script::from(addr.script()).0.0,
798 })
799 .collect();
800 let txid = TxId::from_bytes(ctx.txid[..].try_into().expect("txid is 32 bytes"));
801
802 let mut cb = CompactBlock {
803 hash: {
804 let mut hash = vec![0; 32];
805 self.rng.fill_bytes(&mut hash);
806 hash
807 },
808 height: height.into(),
809 ..Default::default()
810 };
811 cb.prev_hash
812 .extend_from_slice(&prior_cached_block.chain_state.block_hash().0);
813 cb.vtx.push(coinbase);
814 cb.vtx.push(ctx);
815 cb.chain_metadata = Some(compact_formats::ChainMetadata {
816 sapling_commitment_tree_size: prior_cached_block.sapling_end_size,
817 orchard_commitment_tree_size: prior_cached_block.orchard_end_size,
818 ironwood_commitment_tree_size: prior_cached_block.ironwood_end_size,
819 });
820
821 let res = self.cache_block(&prior_cached_block, cb);
822 self.latest_block_height = Some(height);
823
824 (height, res, txid)
825 }
826
827 pub fn generate_empty_block(&mut self) -> (BlockHeight, Cache::InsertResult) {
829 let new_hash = {
830 let mut hash = vec![0; 32];
831 self.rng.fill_bytes(&mut hash);
832 hash
833 };
834
835 let pre_activation_block = CachedBlock::none(self.sapling_activation_height() - 1);
836 let prior_cached_block = self
837 .latest_cached_block()
838 .unwrap_or(&pre_activation_block)
839 .clone();
840 let new_height = prior_cached_block.height() + 1;
841
842 let mut cb = CompactBlock {
843 hash: new_hash,
844 height: new_height.into(),
845 ..Default::default()
846 };
847 cb.prev_hash
848 .extend_from_slice(&prior_cached_block.chain_state.block_hash().0);
849
850 cb.chain_metadata = Some(compact_formats::ChainMetadata {
851 sapling_commitment_tree_size: prior_cached_block.sapling_end_size,
852 orchard_commitment_tree_size: prior_cached_block.orchard_end_size,
853 ironwood_commitment_tree_size: prior_cached_block.ironwood_end_size,
854 });
855
856 let res = self.cache_block(&prior_cached_block, cb);
857 self.latest_block_height = Some(new_height);
858
859 (new_height, res)
860 }
861
862 #[allow(clippy::too_many_arguments)]
868 pub fn generate_block_at<Fvk: TestFvk>(
869 &mut self,
870 height: BlockHeight,
871 prev_hash: BlockHash,
872 outputs: &[FakeCompactOutput<Fvk>],
873 initial_sapling_tree_size: u32,
874 initial_orchard_tree_size: u32,
875 initial_ironwood_tree_size: u32,
876 allow_broken_hash_chain: bool,
877 ) -> (Cache::InsertResult, Vec<Fvk::Nullifier>) {
878 let mut prior_cached_block = self
879 .latest_cached_block_below_height(height)
880 .cloned()
881 .unwrap_or_else(|| CachedBlock::none(self.sapling_activation_height() - 1));
882 assert!(prior_cached_block.chain_state.block_height() < height);
883 assert!(prior_cached_block.sapling_end_size <= initial_sapling_tree_size);
884 assert!(prior_cached_block.orchard_end_size <= initial_orchard_tree_size);
885 assert!(prior_cached_block.ironwood_end_size <= initial_ironwood_tree_size);
886
887 if prior_cached_block.chain_state.block_height() == height - 1 {
891 if !allow_broken_hash_chain {
892 assert_eq!(prev_hash, prior_cached_block.chain_state.block_hash());
893 }
894 } else {
895 let final_sapling_tree =
896 (prior_cached_block.sapling_end_size..initial_sapling_tree_size).fold(
897 prior_cached_block.chain_state.final_sapling_tree().clone(),
898 |mut acc, _| {
899 acc.append(::sapling::Node::from_scalar(bls12_381::Scalar::random(
900 &mut self.rng,
901 )));
902 acc
903 },
904 );
905
906 #[cfg(feature = "orchard")]
907 let final_orchard_tree =
908 (prior_cached_block.orchard_end_size..initial_orchard_tree_size).fold(
909 prior_cached_block.chain_state.final_orchard_tree().clone(),
910 |mut acc, _| {
911 acc.append(MerkleHashOrchard::random(&mut self.rng));
912 acc
913 },
914 );
915
916 #[cfg(feature = "orchard")]
917 let final_ironwood_tree =
918 (prior_cached_block.ironwood_end_size..initial_ironwood_tree_size).fold(
919 prior_cached_block.chain_state.final_ironwood_tree().clone(),
920 |mut acc, _| {
921 acc.append(MerkleHashOrchard::random(&mut self.rng));
922 acc
923 },
924 );
925
926 prior_cached_block = CachedBlock::at(
927 ChainState::new(
928 height - 1,
929 prev_hash,
930 final_sapling_tree,
931 #[cfg(feature = "orchard")]
932 final_orchard_tree,
933 #[cfg(feature = "orchard")]
934 final_ironwood_tree,
935 ),
936 initial_sapling_tree_size,
937 initial_orchard_tree_size,
938 initial_ironwood_tree_size,
939 );
940
941 self.cached_blocks
942 .insert(height - 1, prior_cached_block.clone());
943 }
944
945 let (cb, nfs) = fake_compact_block(
946 &self.network,
947 height,
948 prev_hash,
949 None,
950 outputs,
951 initial_sapling_tree_size,
952 initial_orchard_tree_size,
953 initial_ironwood_tree_size,
954 &mut self.rng,
955 );
956 assert_eq!(cb.height(), height);
957
958 let res = self.cache_block(&prior_cached_block, cb);
959 self.latest_block_height = Some(height);
960
961 (res, nfs)
962 }
963
964 pub fn generate_next_block_spending<Fvk: TestFvk>(
967 &mut self,
968 sender_fvk: &Fvk,
969 note: (Fvk::Nullifier, Zatoshis),
970 to: impl Into<Address>,
971 value: Zatoshis,
972 ) -> (BlockHeight, Cache::InsertResult) {
973 let prior_cached_block = self
974 .latest_cached_block()
975 .cloned()
976 .unwrap_or_else(|| CachedBlock::none(self.sapling_activation_height() - 1));
977 let height = prior_cached_block.height() + 1;
978
979 let cb = fake_compact_block_spending(
980 &self.network,
981 height,
982 prior_cached_block.chain_state.block_hash(),
983 note,
984 sender_fvk,
985 to.into(),
986 value,
987 prior_cached_block.sapling_end_size,
988 prior_cached_block.orchard_end_size,
989 prior_cached_block.ironwood_end_size,
990 &mut self.rng,
991 );
992 assert_eq!(cb.height(), height);
993
994 let res = self.cache_block(&prior_cached_block, cb);
995 self.latest_block_height = Some(height);
996
997 (height, res)
998 }
999
1000 pub fn generate_next_block_including(
1006 &mut self,
1007 txid: TxId,
1008 ) -> (BlockHeight, Cache::InsertResult) {
1009 let tx = self
1010 .wallet()
1011 .get_transaction(txid)
1012 .unwrap()
1013 .expect("TxId should exist in the wallet");
1014
1015 self.generate_next_block_from_tx(1, &tx)
1019 }
1020
1021 pub fn generate_next_block_from_tx(
1027 &mut self,
1028 tx_index: usize,
1029 tx: &Transaction,
1030 ) -> (BlockHeight, Cache::InsertResult) {
1031 let prior_cached_block = self
1032 .latest_cached_block()
1033 .cloned()
1034 .unwrap_or_else(|| CachedBlock::none(self.sapling_activation_height() - 1));
1035 let height = prior_cached_block.height() + 1;
1036
1037 let cb = fake_compact_block_from_tx(
1038 height,
1039 prior_cached_block.chain_state.block_hash(),
1040 tx_index,
1041 tx,
1042 prior_cached_block.sapling_end_size,
1043 prior_cached_block.orchard_end_size,
1044 prior_cached_block.ironwood_end_size,
1045 &mut self.rng,
1046 );
1047 assert_eq!(cb.height(), height);
1048
1049 let res = self.cache_block(&prior_cached_block, cb);
1050 self.latest_block_height = Some(height);
1051
1052 (height, res)
1053 }
1054
1055 pub fn truncate_to_height(&mut self, height: BlockHeight) {
1059 self.wallet_mut().truncate_to_height(height).unwrap();
1060 self.cache.truncate_to_height(height);
1061 self.cached_blocks.split_off(&(height + 1));
1062 self.latest_block_height = Some(height);
1063 }
1064
1065 pub fn truncate_to_height_retaining_cache(&mut self, height: BlockHeight) {
1069 self.wallet_mut().truncate_to_height(height).unwrap();
1070 self.latest_block_height = Some(height);
1071 }
1072}
1073
1074impl<Cache, DbT, ParamsT> TestState<Cache, DbT, ParamsT>
1075where
1076 Cache: TestCache,
1077 <Cache::BlockSource as BlockSource>::Error: fmt::Debug,
1078 ParamsT: consensus::Parameters + Send + 'static,
1079 DbT: InputSource + WalletTest + WalletWrite + WalletCommitmentTrees,
1080 <DbT as WalletRead>::AccountId:
1081 std::fmt::Debug + ConditionallySelectable + Default + Send + Sync + 'static,
1082{
1083 pub fn scan_cached_blocks(&mut self, from_height: BlockHeight, limit: usize) -> ScanSummary {
1085 let result = self.try_scan_cached_blocks(from_height, limit);
1086 assert_matches!(result, Ok(_));
1087 result.unwrap()
1088 }
1089
1090 pub fn try_scan_cached_blocks(
1092 &mut self,
1093 from_height: BlockHeight,
1094 limit: usize,
1095 ) -> Result<
1096 ScanSummary,
1097 super::chain::error::Error<
1098 <DbT as WalletRead>::Error,
1099 <Cache::BlockSource as BlockSource>::Error,
1100 >,
1101 > {
1102 let prior_cached_block = self
1103 .latest_cached_block_below_height(from_height)
1104 .cloned()
1105 .unwrap_or_else(|| CachedBlock::none(from_height - 1));
1106
1107 scan_cached_blocks(
1108 &self.network,
1109 self.cache.block_source(),
1110 &mut self.wallet_data,
1111 from_height,
1112 &prior_cached_block.chain_state,
1113 limit,
1114 )
1115 }
1116
1117 pub fn try_scan_cached_blocks_with_state(
1124 &mut self,
1125 from_height: BlockHeight,
1126 from_state: &ChainState,
1127 limit: usize,
1128 ) -> Result<
1129 ScanSummary,
1130 super::chain::error::Error<
1131 <DbT as WalletRead>::Error,
1132 <Cache::BlockSource as BlockSource>::Error,
1133 >,
1134 > {
1135 scan_cached_blocks(
1136 &self.network,
1137 self.cache.block_source(),
1138 &mut self.wallet_data,
1139 from_height,
1140 from_state,
1141 limit,
1142 )
1143 }
1144
1145 pub fn put_subtree_roots(
1147 &mut self,
1148 sapling_start_index: u64,
1149 sapling_roots: &[CommitmentTreeRoot<::sapling::Node>],
1150 #[cfg(feature = "orchard")] orchard_start_index: u64,
1151 #[cfg(feature = "orchard")] orchard_roots: &[CommitmentTreeRoot<MerkleHashOrchard>],
1152 ) -> Result<(), ShardTreeError<<DbT as WalletCommitmentTrees>::Error>> {
1153 self.wallet_mut()
1154 .put_sapling_subtree_roots(sapling_start_index, sapling_roots)?;
1155
1156 #[cfg(feature = "orchard")]
1157 self.wallet_mut()
1158 .put_orchard_subtree_roots(orchard_start_index, orchard_roots)?;
1159
1160 Ok(())
1161 }
1162
1163 pub fn generate_and_scan_empty_blocks(&mut self, n: usize) -> BlockHeight {
1170 for _ in 0..n {
1171 let (height, _) = self.generate_empty_block();
1172 self.scan_cached_blocks(height, 1);
1173 }
1174 self.wallet()
1175 .block_fully_scanned()
1176 .expect("the wallet reports its fully-scanned block")
1177 .expect("the wallet is fully scanned")
1178 .block_height()
1179 }
1180
1181 #[cfg(feature = "orchard")]
1187 pub fn orchard_anchor_at(
1188 &mut self,
1189 height: BlockHeight,
1190 ) -> Result<Option<::orchard::Anchor>, ShardTreeError<<DbT as WalletCommitmentTrees>::Error>>
1191 {
1192 self.wallet_mut()
1193 .with_orchard_tree_mut(|tree| tree.root_at_checkpoint_id(&height))
1194 .map(|root| root.map(::orchard::Anchor::from))
1195 }
1196}
1197
1198impl<Cache, DbT, ParamsT, AccountIdT, ErrT> TestState<Cache, DbT, ParamsT>
1199where
1200 ParamsT: consensus::Parameters + Send + 'static,
1201 AccountIdT: std::fmt::Debug + std::cmp::Eq + std::hash::Hash,
1202 ErrT: std::fmt::Debug,
1203 DbT: InputSource<AccountId = AccountIdT, Error = ErrT>
1204 + WalletTest
1205 + WalletRead<AccountId = AccountIdT, Error = ErrT>
1206 + WalletWrite
1207 + WalletCommitmentTrees,
1208 <DbT as WalletRead>::AccountId: ConditionallySelectable + Default + Send + 'static,
1209{
1210 pub fn create_standard_transaction(
1214 &mut self,
1215 from_account: &TestAccount<DbT::Account>,
1216 to: ZcashAddress,
1217 value: Zatoshis,
1218 ) -> Result<
1219 NonEmpty<TxId>,
1220 super::wallet::TransferErrT<
1221 DbT,
1222 GreedyInputSelector<DbT>,
1223 standard::MultiOutputChangeStrategy<DbT>,
1224 >,
1225 > {
1226 let input_selector = GreedyInputSelector::new();
1227
1228 #[cfg(not(feature = "orchard"))]
1229 let fallback_change_pool = ShieldedPool::Sapling;
1230 #[cfg(feature = "orchard")]
1231 let fallback_change_pool = ShieldedPool::Orchard;
1232
1233 let change_strategy =
1234 single_output_change_strategy(StandardFeeRule::Zip317, None, fallback_change_pool);
1235
1236 let request =
1237 zip321::TransactionRequest::new(vec![Payment::without_memo(to, value)]).unwrap();
1238
1239 self.spend(
1240 &input_selector,
1241 &change_strategy,
1242 from_account.usk(),
1243 request,
1244 OvkPolicy::Sender,
1245 ConfirmationsPolicy::MIN,
1246 )
1247 }
1248
1249 #[allow(clippy::type_complexity)]
1251 pub fn spend<InputsT, ChangeT>(
1252 &mut self,
1253 input_selector: &InputsT,
1254 change_strategy: &ChangeT,
1255 usk: &UnifiedSpendingKey,
1256 request: zip321::TransactionRequest,
1257 ovk_policy: OvkPolicy,
1258 confirmations_policy: ConfirmationsPolicy,
1259 ) -> Result<NonEmpty<TxId>, super::wallet::TransferErrT<DbT, InputsT, ChangeT>>
1260 where
1261 InputsT: InputSelector<InputSource = DbT>,
1262 ChangeT: ChangeStrategy<MetaSource = DbT>,
1263 {
1264 let network = self.network().clone();
1265
1266 let account = self
1267 .wallet()
1268 .get_account_for_ufvk(&usk.to_unified_full_viewing_key())
1269 .map_err(Error::DataSource)?
1270 .ok_or(Error::KeyNotRecognized)?;
1271
1272 let proposal = propose_transfer(
1273 self.wallet_mut(),
1274 &network,
1275 account.id(),
1276 input_selector,
1277 change_strategy,
1278 request,
1279 confirmations_policy,
1280 &SpendPolicy::default(),
1281 None,
1282 None,
1283 )?;
1284
1285 let clock = self.clock.clone();
1286 create_proposed_transactions(
1287 &mut self.wallet_data,
1288 &network,
1289 &clock,
1290 &mut self.rng,
1291 &MockSpendProver,
1292 &MockOutputProver,
1293 &SpendingKeys::from_unified_spending_key(usk.clone()),
1294 ovk_policy,
1295 &proposal,
1296 None,
1297 )
1298 }
1299
1300 #[allow(clippy::type_complexity)]
1302 pub fn propose_transfer<InputsT, ChangeT>(
1303 &mut self,
1304 spend_from_account: <DbT as InputSource>::AccountId,
1305 input_selector: &InputsT,
1306 change_strategy: &ChangeT,
1307 request: zip321::TransactionRequest,
1308 confirmations_policy: ConfirmationsPolicy,
1309 ) -> Result<
1310 Proposal<ChangeT::FeeRule, <DbT as InputSource>::NoteRef>,
1311 super::wallet::ProposeTransferErrT<DbT, Infallible, InputsT, ChangeT>,
1312 >
1313 where
1314 InputsT: InputSelector<InputSource = DbT>,
1315 ChangeT: ChangeStrategy<MetaSource = DbT>,
1316 {
1317 let network = self.network().clone();
1318 propose_transfer::<_, _, _, _, Infallible>(
1319 self.wallet_mut(),
1320 &network,
1321 spend_from_account,
1322 input_selector,
1323 change_strategy,
1324 request,
1325 confirmations_policy,
1326 &SpendPolicy::default(),
1327 None,
1328 None,
1329 )
1330 }
1331
1332 #[allow(clippy::type_complexity)]
1340 pub fn propose_transfer_with_policy<InputsT, ChangeT>(
1341 &mut self,
1342 spend_from_account: <DbT as InputSource>::AccountId,
1343 input_selector: &InputsT,
1344 change_strategy: &ChangeT,
1345 request: zip321::TransactionRequest,
1346 confirmations_policy: ConfirmationsPolicy,
1347 spend_policy: &SpendPolicy,
1348 ) -> Result<
1349 Proposal<ChangeT::FeeRule, <DbT as InputSource>::NoteRef>,
1350 super::wallet::ProposeTransferErrT<DbT, Infallible, InputsT, ChangeT>,
1351 >
1352 where
1353 InputsT: InputSelector<InputSource = DbT>,
1354 ChangeT: ChangeStrategy<MetaSource = DbT>,
1355 {
1356 let network = self.network().clone();
1357 propose_transfer::<_, _, _, _, Infallible>(
1358 self.wallet_mut(),
1359 &network,
1360 spend_from_account,
1361 input_selector,
1362 change_strategy,
1363 request,
1364 confirmations_policy,
1365 spend_policy,
1366 None,
1367 None,
1368 )
1369 }
1370
1371 #[allow(clippy::type_complexity)]
1373 pub fn propose_send_max_transfer<FeeRuleT>(
1374 &mut self,
1375 spend_from_account: <DbT as InputSource>::AccountId,
1376 fee_rule: &FeeRuleT,
1377 to: ZcashAddress,
1378 memo: Option<MemoBytes>,
1379 mode: MaxSpendMode,
1380 confirmations_policy: ConfirmationsPolicy,
1381 ) -> Result<
1382 Proposal<FeeRuleT, <DbT as InputSource>::NoteRef>,
1383 super::wallet::ProposeSendMaxErrT<DbT, Infallible, FeeRuleT>,
1384 >
1385 where
1386 FeeRuleT: FeeRule + Clone,
1387 {
1388 let network = self.network().clone();
1389 propose_send_max_transfer::<_, _, _, Infallible>(
1390 self.wallet_mut(),
1391 &network,
1392 spend_from_account,
1393 &[ShieldedPool::Sapling, ShieldedPool::Orchard],
1394 fee_rule,
1395 to,
1396 memo,
1397 mode,
1398 confirmations_policy,
1399 &LockedInputPolicy::Exclude,
1400 None,
1401 )
1402 }
1403
1404 #[allow(clippy::type_complexity)]
1406 #[allow(clippy::too_many_arguments)]
1407 pub fn propose_standard_transfer<CommitmentTreeErrT>(
1408 &mut self,
1409 spend_from_account: <DbT as InputSource>::AccountId,
1410 fee_rule: StandardFeeRule,
1411 confirmations_policy: ConfirmationsPolicy,
1412 to: &Address,
1413 amount: Zatoshis,
1414 memo: Option<MemoBytes>,
1415 change_memo: Option<MemoBytes>,
1416 fallback_change_pool: ShieldedPool,
1417 ) -> Result<
1418 Proposal<StandardFeeRule, <DbT as InputSource>::NoteRef>,
1419 super::wallet::ProposeTransferErrT<
1420 DbT,
1421 CommitmentTreeErrT,
1422 GreedyInputSelector<DbT>,
1423 SingleOutputChangeStrategy<DbT>,
1424 >,
1425 > {
1426 let network = self.network().clone();
1427 let result = propose_standard_transfer_to_address::<_, _, CommitmentTreeErrT>(
1428 self.wallet_mut(),
1429 &network,
1430 fee_rule,
1431 spend_from_account,
1432 confirmations_policy,
1433 to,
1434 amount,
1435 memo,
1436 change_memo,
1437 fallback_change_pool,
1438 None,
1439 None,
1440 );
1441
1442 if let Ok(proposal) = &result {
1443 check_proposal_serialization_roundtrip(&network, self.wallet(), proposal);
1444 }
1445
1446 result
1447 }
1448
1449 #[cfg(feature = "transparent-inputs")]
1453 #[allow(clippy::type_complexity)]
1454 #[allow(clippy::too_many_arguments)]
1455 #[allow(dead_code)]
1456 pub fn propose_shielding<InputsT, ChangeT>(
1457 &mut self,
1458 input_selector: &InputsT,
1459 change_strategy: &ChangeT,
1460 shielding_threshold: Zatoshis,
1461 from_addrs: &[TransparentAddress],
1462 to_account: <InputsT::InputSource as InputSource>::AccountId,
1463 confirmations_policy: ConfirmationsPolicy,
1464 output_filter: CoinbaseFilter,
1465 ) -> Result<
1466 Proposal<ChangeT::FeeRule, Infallible>,
1467 super::wallet::ProposeShieldingErrT<DbT, Infallible, InputsT, ChangeT>,
1468 >
1469 where
1470 InputsT: ShieldingSelector<InputSource = DbT>,
1471 ChangeT: ChangeStrategy<MetaSource = DbT>,
1472 {
1473 let network = self.network().clone();
1474 propose_shielding::<_, _, _, _, Infallible>(
1475 self.wallet_mut(),
1476 &network,
1477 input_selector,
1478 change_strategy,
1479 shielding_threshold,
1480 from_addrs,
1481 to_account,
1482 confirmations_policy,
1483 output_filter,
1484 None,
1485 )
1486 }
1487
1488 #[cfg(feature = "transparent-inputs")]
1492 #[allow(clippy::type_complexity)]
1493 #[allow(clippy::too_many_arguments)]
1494 #[allow(dead_code)]
1495 pub fn propose_shielding_coinbase<InputsT, FeeRuleT>(
1496 &mut self,
1497 input_selector: &InputsT,
1498 fee_rule: &FeeRuleT,
1499 shielding_threshold: Zatoshis,
1500 from_addrs: &[TransparentAddress],
1501 to_address: zcash_address::ZcashAddress,
1502 memo: Option<zcash_protocol::memo::MemoBytes>,
1503 limit: Option<usize>,
1504 ) -> Result<
1505 Proposal<FeeRuleT, Infallible>,
1506 super::wallet::ProposeShieldingCoinbaseErrT<DbT, Infallible, InputsT, FeeRuleT>,
1507 >
1508 where
1509 InputsT: ShieldingSelector<InputSource = DbT>,
1510 FeeRuleT: zcash_primitives::transaction::fees::FeeRule + Clone,
1511 {
1512 let network = self.network().clone();
1513 propose_shielding_coinbase::<_, _, _, _, Infallible>(
1514 self.wallet_mut(),
1515 &network,
1516 input_selector,
1517 fee_rule,
1518 shielding_threshold,
1519 from_addrs,
1520 to_address,
1521 memo,
1522 limit,
1523 None,
1524 )
1525 }
1526
1527 #[allow(clippy::type_complexity)]
1529 pub fn create_proposed_transactions<InputsErrT, FeeRuleT, ChangeErrT, N>(
1530 &mut self,
1531 usk: &UnifiedSpendingKey,
1532 ovk_policy: OvkPolicy,
1533 proposal: &Proposal<FeeRuleT, N>,
1534 ) -> Result<NonEmpty<TxId>, super::wallet::CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>>
1535 where
1536 FeeRuleT: FeeRule,
1537 {
1538 let network = self.network().clone();
1539 let clock = self.clock.clone();
1540 create_proposed_transactions(
1541 &mut self.wallet_data,
1542 &network,
1543 &clock,
1544 &mut self.rng,
1545 &MockSpendProver,
1546 &MockOutputProver,
1547 &SpendingKeys::from_unified_spending_key(usk.clone()),
1548 ovk_policy,
1549 proposal,
1550 None,
1551 )
1552 }
1553
1554 #[cfg(feature = "pczt")]
1558 #[allow(clippy::type_complexity)]
1559 pub fn create_pczt_from_proposal<InputsErrT, FeeRuleT, ChangeErrT>(
1560 &mut self,
1561 spend_from_account: <DbT as InputSource>::AccountId,
1562 ovk_policy: OvkPolicy,
1563 proposal: &Proposal<FeeRuleT, <DbT as InputSource>::NoteRef>,
1564 expiry_height: Option<BlockHeight>,
1565 ) -> Result<
1566 pczt::Pczt,
1567 super::wallet::CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, DbT::NoteRef>,
1568 >
1569 where
1570 <DbT as WalletRead>::AccountId: serde::Serialize,
1571 FeeRuleT: FeeRule,
1572 {
1573 let network = self.network().clone();
1574
1575 let clock = self.clock.clone();
1576 create_pczt_from_proposal(
1577 &mut self.wallet_data,
1578 &network,
1579 &clock,
1580 &mut self.rng,
1581 spend_from_account,
1582 ovk_policy,
1583 proposal,
1584 expiry_height,
1585 ::zcash_primitives::transaction::builder::BundlePadding::DEFAULT,
1586 )
1587 }
1588
1589 #[cfg(feature = "pczt")]
1593 #[allow(clippy::type_complexity)]
1594 pub fn extract_and_store_transaction_from_pczt(
1595 &mut self,
1596 pczt: pczt::Pczt,
1597 ) -> Result<TxId, super::wallet::ExtractErrT<DbT, DbT::NoteRef>>
1598 where
1599 <DbT as WalletRead>::AccountId: serde::de::DeserializeOwned,
1600 {
1601 let prover = real_test_prover();
1602 let (spend_vk, output_vk) = prover.verifying_keys();
1603
1604 let clock = self.clock.clone();
1605 extract_and_store_transaction_from_pczt(
1606 &mut self.wallet_data,
1607 &clock,
1608 &mut self.rng,
1609 pczt,
1610 Some((&spend_vk, &output_vk)),
1611 None,
1612 )
1613 }
1614
1615 #[cfg(feature = "transparent-inputs")]
1619 #[allow(clippy::type_complexity)]
1620 #[allow(clippy::too_many_arguments)]
1621 pub fn shield_transparent_funds<InputsT, ChangeT>(
1622 &mut self,
1623 input_selector: &InputsT,
1624 change_strategy: &ChangeT,
1625 shielding_threshold: Zatoshis,
1626 usk: &UnifiedSpendingKey,
1627 from_addrs: &[TransparentAddress],
1628 to_account: <DbT as InputSource>::AccountId,
1629 confirmations_policy: ConfirmationsPolicy,
1630 ) -> Result<NonEmpty<TxId>, super::wallet::ShieldErrT<DbT, InputsT, ChangeT>>
1631 where
1632 InputsT: ShieldingSelector<InputSource = DbT>,
1633 ChangeT: ChangeStrategy<MetaSource = DbT>,
1634 {
1635 let network = self.network().clone();
1636 let clock = self.clock.clone();
1637 shield_transparent_funds(
1638 &mut self.wallet_data,
1639 &network,
1640 &clock,
1641 &mut self.rng,
1642 &MockSpendProver,
1643 &MockOutputProver,
1644 input_selector,
1645 change_strategy,
1646 shielding_threshold,
1647 &SpendingKeys::from_unified_spending_key(usk.clone()),
1648 from_addrs,
1649 to_account,
1650 confirmations_policy,
1651 )
1652 }
1653
1654 fn with_account_balance<T, F: FnOnce(&AccountBalance) -> T>(
1655 &self,
1656 account: AccountIdT,
1657 confirmations_policy: ConfirmationsPolicy,
1658 f: F,
1659 ) -> T {
1660 let binding = self
1661 .wallet()
1662 .get_wallet_summary(confirmations_policy)
1663 .unwrap()
1664 .unwrap();
1665 f(binding.account_balances().get(&account).unwrap())
1666 }
1667
1668 pub fn get_total_balance(&self, account: AccountIdT) -> Zatoshis {
1670 self.with_account_balance(account, ConfirmationsPolicy::MIN, |balance| balance.total())
1671 }
1672
1673 pub fn get_locked_balance(&self, account: AccountIdT) -> Zatoshis {
1675 self.with_account_balance(account, ConfirmationsPolicy::MIN, |balance| {
1676 balance.locked_value()
1677 })
1678 }
1679
1680 pub fn get_spendable_balance(
1683 &self,
1684 account: AccountIdT,
1685 confirmations_policy: ConfirmationsPolicy,
1686 ) -> Zatoshis {
1687 self.with_account_balance(account, confirmations_policy, |balance| {
1688 balance.spendable_value()
1689 })
1690 }
1691
1692 pub fn get_pending_shielded_balance(
1695 &self,
1696 account: AccountIdT,
1697 confirmations_policy: ConfirmationsPolicy,
1698 ) -> Zatoshis {
1699 self.with_account_balance(account, confirmations_policy, |balance| {
1700 balance.value_pending_spendability() + balance.change_pending_confirmation()
1701 })
1702 .unwrap()
1703 }
1704
1705 #[allow(dead_code)]
1708 pub fn get_pending_change(
1709 &self,
1710 account: AccountIdT,
1711 confirmations_policy: ConfirmationsPolicy,
1712 ) -> Zatoshis {
1713 self.with_account_balance(account, confirmations_policy, |balance| {
1714 balance.change_pending_confirmation()
1715 })
1716 }
1717
1718 pub fn get_wallet_summary(
1720 &self,
1721 confirmations_policy: ConfirmationsPolicy,
1722 ) -> Option<WalletSummary<AccountIdT>> {
1723 self.wallet()
1724 .get_wallet_summary(confirmations_policy)
1725 .unwrap()
1726 }
1727}
1728
1729impl<Cache, DbT, ParamsT, AccountIdT, ErrT> TestState<Cache, DbT, ParamsT>
1730where
1731 ParamsT: consensus::Parameters + Send + 'static,
1732 AccountIdT: std::cmp::Eq + std::hash::Hash,
1733 ErrT: std::fmt::Debug,
1734 DbT: InputSource<AccountId = AccountIdT, Error = ErrT>
1735 + WalletTest
1736 + WalletRead<AccountId = AccountIdT, Error = ErrT>
1737 + WalletWrite
1738 + WalletCommitmentTrees,
1739 <DbT as WalletRead>::AccountId: ConditionallySelectable + Default + Send + 'static,
1740{
1741 #[allow(dead_code)]
1743 pub fn get_tx_from_history(
1744 &self,
1745 txid: TxId,
1746 ) -> Result<Option<TransactionSummary<AccountIdT>>, ErrT> {
1747 let history = self.wallet().get_tx_history()?;
1748 Ok(history.into_iter().find(|tx| tx.txid() == txid))
1749 }
1750}
1751
1752impl<Cache, DbT: WalletRead + Reset> TestState<Cache, DbT, LocalNetwork> {
1753 pub fn reset(&mut self) -> DbT::Handle {
1760 self.latest_block_height = None;
1761 self.test_account = None;
1762 DbT::reset(self)
1763 }
1764
1765 }
1783
1784pub fn single_output_change_strategy<DbT: InputSource>(
1785 fee_rule: StandardFeeRule,
1786 change_memo: Option<&str>,
1787 fallback_change_pool: ShieldedPool,
1788) -> standard::SingleOutputChangeStrategy<DbT> {
1789 let change_memo = change_memo.map(|m| MemoBytes::from(m.parse::<Memo>().unwrap()));
1790 standard::SingleOutputChangeStrategy::new(
1791 fee_rule,
1792 change_memo,
1793 fallback_change_pool,
1794 DustOutputPolicy::default(),
1795 )
1796}
1797
1798fn check_proposal_serialization_roundtrip<ParamsT: consensus::Parameters, DbT: InputSource>(
1801 params: &ParamsT,
1802 wallet_data: &DbT,
1803 proposal: &Proposal<StandardFeeRule, DbT::NoteRef>,
1804) {
1805 let proposal_proto = crate::proto::proposal::Proposal::from_standard_proposal(proposal);
1806 let deserialized_proposal = proposal_proto.try_into_standard_proposal(params, wallet_data);
1807 assert_matches!(deserialized_proposal, Ok(r) if &r == proposal);
1808}
1809
1810pub struct InitialChainState {
1816 pub chain_state: ChainState,
1818 pub prior_sapling_roots: Vec<CommitmentTreeRoot<::sapling::Node>>,
1820 #[cfg(feature = "orchard")]
1822 pub prior_orchard_roots: Vec<CommitmentTreeRoot<MerkleHashOrchard>>,
1823}
1824
1825pub trait DataStoreFactory {
1827 type Error: core::fmt::Debug;
1828 type AccountId: std::fmt::Debug
1829 + ConditionallySelectable
1830 + Default
1831 + Hash
1832 + Eq
1833 + Send
1834 + Sync
1835 + 'static;
1836 type Account: Account<AccountId = Self::AccountId> + Clone;
1837 type DsError: core::fmt::Debug;
1838 type DataStore: InputSource<AccountId = Self::AccountId, Error = Self::DsError>
1839 + WalletRead<AccountId = Self::AccountId, Account = Self::Account, Error = Self::DsError>
1840 + WalletTest
1841 + WalletWrite
1842 + WalletCommitmentTrees;
1843
1844 fn new_data_store(
1846 &self,
1847 network: LocalNetwork,
1848 anchor_retention_interval: Option<AnchorRetentionInterval>,
1849 #[cfg(feature = "transparent-inputs")] gap_limits: Option<GapLimits>,
1850 ) -> Result<Self::DataStore, Self::Error>;
1851}
1852
1853pub struct TestBuilder<Cache, DataStoreFactory> {
1855 rng: ChaChaRng,
1856 network: LocalNetwork,
1857 cache: Cache,
1858 ds_factory: DataStoreFactory,
1859 initial_chain_state: Option<InitialChainState>,
1860 account_birthday: Option<AccountBirthday>,
1861 account_index: Option<zip32::AccountId>,
1862 anchor_retention_interval: Option<AnchorRetentionInterval>,
1863 #[cfg(feature = "transparent-inputs")]
1864 gap_limits: Option<GapLimits>,
1865}
1866
1867impl TestBuilder<(), ()> {
1868 pub const DEFAULT_NETWORK: LocalNetwork = LocalNetwork {
1873 overwinter: Some(BlockHeight::from_u32(1)),
1874 sapling: Some(BlockHeight::from_u32(100_000)),
1875 blossom: Some(BlockHeight::from_u32(100_000)),
1876 heartwood: Some(BlockHeight::from_u32(100_000)),
1877 canopy: Some(BlockHeight::from_u32(100_000)),
1878 nu5: Some(BlockHeight::from_u32(100_000)),
1879 nu6: None,
1880 nu6_1: None,
1881 nu6_2: None,
1882 nu6_3: None,
1883 nu7: None,
1884 #[cfg(zcash_unstable = "nutachyon")]
1885 nu_tachyon: None,
1886 };
1887
1888 pub fn new() -> Self {
1890 TestBuilder {
1891 rng: ChaChaRng::seed_from_u64(0),
1892 network: Self::DEFAULT_NETWORK,
1893 cache: (),
1894 ds_factory: (),
1895 initial_chain_state: None,
1896 account_birthday: None,
1897 account_index: None,
1898 anchor_retention_interval: None,
1899 #[cfg(feature = "transparent-inputs")]
1900 gap_limits: None,
1901 }
1902 }
1903}
1904
1905impl Default for TestBuilder<(), ()> {
1906 fn default() -> Self {
1907 Self::new()
1908 }
1909}
1910
1911impl<A> TestBuilder<(), A> {
1912 pub fn with_block_cache<C>(self, cache: C) -> TestBuilder<C, A> {
1914 TestBuilder {
1915 rng: self.rng,
1916 network: self.network,
1917 cache,
1918 ds_factory: self.ds_factory,
1919 initial_chain_state: self.initial_chain_state,
1920 account_birthday: self.account_birthday,
1921 account_index: self.account_index,
1922 anchor_retention_interval: self.anchor_retention_interval,
1923 #[cfg(feature = "transparent-inputs")]
1924 gap_limits: self.gap_limits,
1925 }
1926 }
1927}
1928
1929impl<A> TestBuilder<A, ()> {
1930 pub fn with_data_store_factory<DsFactory>(
1932 self,
1933 ds_factory: DsFactory,
1934 ) -> TestBuilder<A, DsFactory> {
1935 TestBuilder {
1936 rng: self.rng,
1937 network: self.network,
1938 cache: self.cache,
1939 ds_factory,
1940 initial_chain_state: self.initial_chain_state,
1941 account_birthday: self.account_birthday,
1942 account_index: self.account_index,
1943 anchor_retention_interval: self.anchor_retention_interval,
1944 #[cfg(feature = "transparent-inputs")]
1945 gap_limits: self.gap_limits,
1946 }
1947 }
1948}
1949
1950impl<A, B> TestBuilder<A, B> {
1951 pub fn with_network(mut self, network: LocalNetwork) -> Self {
1957 self.network = network;
1958 self
1959 }
1960
1961 pub fn with_anchor_retention_interval(mut self, interval: AnchorRetentionInterval) -> Self {
1967 self.anchor_retention_interval = Some(interval);
1968 self
1969 }
1970
1971 #[cfg(feature = "transparent-inputs")]
1972 pub fn with_gap_limits(self, gap_limits: GapLimits) -> TestBuilder<A, B> {
1973 TestBuilder {
1974 rng: self.rng,
1975 network: self.network,
1976 cache: self.cache,
1977 ds_factory: self.ds_factory,
1978 initial_chain_state: self.initial_chain_state,
1979 account_birthday: self.account_birthday,
1980 account_index: self.account_index,
1981 anchor_retention_interval: self.anchor_retention_interval,
1982 gap_limits: Some(gap_limits),
1983 }
1984 }
1985}
1986
1987impl<Cache, DsFactory> TestBuilder<Cache, DsFactory> {
1988 pub fn with_initial_chain_state(
2073 mut self,
2074 chain_state: impl FnOnce(&mut ChaChaRng, &LocalNetwork) -> InitialChainState,
2075 ) -> Self {
2076 assert!(self.initial_chain_state.is_none());
2077 assert!(self.account_birthday.is_none());
2078 self.initial_chain_state = Some(chain_state(&mut self.rng, &self.network));
2079 self
2080 }
2081
2082 pub fn with_account_from_sapling_activation(mut self, prev_hash: BlockHash) -> Self {
2090 assert!(self.account_birthday.is_none());
2091 self.account_birthday = Some(AccountBirthday::from_parts(
2092 ChainState::empty(
2093 self.network
2094 .activation_height(NetworkUpgrade::Sapling)
2095 .unwrap()
2096 - 1,
2097 prev_hash,
2098 ),
2099 None,
2100 ));
2101 self
2102 }
2103
2104 pub fn with_account_having_current_birthday(mut self) -> Self {
2113 assert!(self.account_birthday.is_none());
2114 assert!(self.initial_chain_state.is_some());
2115 self.account_birthday = Some(AccountBirthday::from_parts(
2116 self.initial_chain_state
2117 .as_ref()
2118 .unwrap()
2119 .chain_state
2120 .clone(),
2121 None,
2122 ));
2123 self
2124 }
2125
2126 pub fn set_account_index(mut self, index: zip32::AccountId) -> Self {
2135 assert!(self.account_index.is_none());
2136 self.account_index = Some(index);
2137 self
2138 }
2139}
2140
2141impl<Cache, DsFactory: DataStoreFactory> TestBuilder<Cache, DsFactory> {
2142 pub fn build(self) -> TestState<Cache, DsFactory::DataStore, LocalNetwork> {
2144 let mut cached_blocks = BTreeMap::new();
2145 let mut wallet_data = self
2146 .ds_factory
2147 .new_data_store(
2148 self.network,
2149 self.anchor_retention_interval,
2150 #[cfg(feature = "transparent-inputs")]
2151 self.gap_limits,
2152 )
2153 .unwrap();
2154
2155 if let Some(initial_state) = &self.initial_chain_state {
2156 wallet_data
2157 .put_sapling_subtree_roots(0, &initial_state.prior_sapling_roots)
2158 .unwrap();
2159 wallet_data
2160 .with_sapling_tree_mut(|t| {
2161 t.insert_frontier(
2162 initial_state.chain_state.final_sapling_tree().clone(),
2163 Retention::Checkpoint {
2164 id: initial_state.chain_state.block_height(),
2165 marking: Marking::Reference,
2166 },
2167 )
2168 })
2169 .unwrap();
2170
2171 #[cfg(feature = "orchard")]
2172 {
2173 wallet_data
2174 .put_orchard_subtree_roots(0, &initial_state.prior_orchard_roots)
2175 .unwrap();
2176 wallet_data
2177 .with_orchard_tree_mut(|t| {
2178 t.insert_frontier(
2179 initial_state.chain_state.final_orchard_tree().clone(),
2180 Retention::Checkpoint {
2181 id: initial_state.chain_state.block_height(),
2182 marking: Marking::Reference,
2183 },
2184 )
2185 })
2186 .unwrap();
2187 }
2188
2189 let final_sapling_tree_size =
2190 initial_state.chain_state.final_sapling_tree().tree_size() as u32;
2191 let _final_orchard_tree_size = 0;
2192 #[cfg(feature = "orchard")]
2193 let _final_orchard_tree_size =
2194 initial_state.chain_state.final_orchard_tree().tree_size() as u32;
2195 let _final_ironwood_tree_size = 0;
2196 #[cfg(feature = "orchard")]
2197 let _final_ironwood_tree_size =
2198 initial_state.chain_state.final_ironwood_tree().tree_size() as u32;
2199
2200 cached_blocks.insert(
2201 initial_state.chain_state.block_height(),
2202 CachedBlock {
2203 chain_state: initial_state.chain_state.clone(),
2204 sapling_end_size: final_sapling_tree_size,
2205 orchard_end_size: _final_orchard_tree_size,
2206 ironwood_end_size: _final_ironwood_tree_size,
2207 },
2208 );
2209 };
2210
2211 let test_account = self.account_birthday.map(|birthday| {
2212 let seed = Secret::new(vec![0u8; 32]);
2213 let (account, usk) = match self.account_index {
2214 Some(index) => wallet_data
2215 .import_account_hd("", &seed, index, &birthday, None)
2216 .expect("test account import succeeds"),
2217 None => {
2218 let result = wallet_data
2219 .create_account("", &seed, &birthday, None)
2220 .expect("test account creation succeeds");
2221 (
2222 wallet_data
2223 .get_account(result.0)
2224 .expect("retrieval of just-created account succeeds")
2225 .expect("an account was created"),
2226 result.1,
2227 )
2228 }
2229 };
2230 (
2231 seed,
2232 TestAccount {
2233 account,
2234 usk,
2235 birthday,
2236 },
2237 )
2238 });
2239
2240 TestState {
2241 cache: self.cache,
2242 cached_blocks,
2243 latest_block_height: self
2244 .initial_chain_state
2245 .map(|s| s.chain_state.block_height()),
2246 wallet_data,
2247 network: self.network,
2248 test_account,
2249 rng: self.rng,
2250 clock: FixedClock::new(std::time::UNIX_EPOCH + TEST_CLOCK_EPOCH_OFFSET),
2251 }
2252 }
2253}
2254
2255pub trait TestFvk: Clone {
2257 type Nullifier: Copy;
2260 type OutgoingViewingKey;
2262
2263 fn to_ovk(&self, scope: zip32::Scope) -> Self::OutgoingViewingKey;
2266
2267 fn ovk_bytes(&self, scope: zip32::Scope) -> [u8; 32];
2270
2271 fn add_spend<R: Rng + CryptoRng>(&self, ctx: &mut CompactTx, nf: Self::Nullifier, rng: &mut R);
2274
2275 #[allow(clippy::too_many_arguments)]
2285 fn add_output<P: consensus::Parameters, R: Rng + CryptoRng>(
2286 &self,
2287 ctx: &mut CompactTx,
2288 params: &P,
2289 height: BlockHeight,
2290 sender_ovk: Option<&Self::OutgoingViewingKey>,
2291 recipient_address_type: AddressType,
2292 value: Zatoshis,
2293 initial_sapling_tree_size: u32,
2294 rng: &mut R,
2297 ) -> Self::Nullifier;
2298
2299 #[allow(clippy::too_many_arguments)]
2313 fn add_logical_action<P: consensus::Parameters, R: Rng + CryptoRng>(
2314 &self,
2315 ctx: &mut CompactTx,
2316 params: &P,
2317 height: BlockHeight,
2318 nf_to_reveal_in_spend: Self::Nullifier,
2319 sender_ovk: Option<&Self::OutgoingViewingKey>,
2320 recipient_address_type: AddressType,
2321 value: Zatoshis,
2322 initial_sapling_tree_size: u32,
2323 rng: &mut R,
2326 ) -> Self::Nullifier;
2327}
2328
2329impl<A: TestFvk> TestFvk for &A {
2330 type Nullifier = A::Nullifier;
2331 type OutgoingViewingKey = A::OutgoingViewingKey;
2332
2333 fn to_ovk(&self, scope: zip32::Scope) -> Self::OutgoingViewingKey {
2334 (*self).to_ovk(scope)
2335 }
2336
2337 fn ovk_bytes(&self, scope: zip32::Scope) -> [u8; 32] {
2338 (*self).ovk_bytes(scope)
2339 }
2340
2341 fn add_spend<R: Rng + CryptoRng>(&self, ctx: &mut CompactTx, nf: Self::Nullifier, rng: &mut R) {
2342 (*self).add_spend(ctx, nf, rng)
2343 }
2344
2345 fn add_output<P: consensus::Parameters, R: Rng + CryptoRng>(
2346 &self,
2347 ctx: &mut CompactTx,
2348 params: &P,
2349 height: BlockHeight,
2350 sender_ovk: Option<&Self::OutgoingViewingKey>,
2351 recipient_address_type: AddressType,
2352 value: Zatoshis,
2353 initial_sapling_tree_size: u32,
2354 rng: &mut R,
2357 ) -> Self::Nullifier {
2358 (*self).add_output(
2359 ctx,
2360 params,
2361 height,
2362 sender_ovk,
2363 recipient_address_type,
2364 value,
2365 initial_sapling_tree_size,
2366 rng,
2367 )
2368 }
2369
2370 fn add_logical_action<P: consensus::Parameters, R: Rng + CryptoRng>(
2371 &self,
2372 ctx: &mut CompactTx,
2373 params: &P,
2374 height: BlockHeight,
2375 nf_to_reveal_in_spend: Self::Nullifier,
2376 sender_ovk: Option<&Self::OutgoingViewingKey>,
2377 recipient_address_type: AddressType,
2378 value: Zatoshis,
2379 initial_sapling_tree_size: u32,
2380 rng: &mut R,
2383 ) -> Self::Nullifier {
2384 (*self).add_logical_action(
2385 ctx,
2386 params,
2387 height,
2388 nf_to_reveal_in_spend,
2389 sender_ovk,
2390 recipient_address_type,
2391 value,
2392 initial_sapling_tree_size,
2393 rng,
2394 )
2395 }
2396}
2397
2398impl TestFvk for DiversifiableFullViewingKey {
2399 type Nullifier = ::sapling::Nullifier;
2400 type OutgoingViewingKey = ::sapling::keys::OutgoingViewingKey;
2401
2402 fn to_ovk(&self, scope: zip32::Scope) -> Self::OutgoingViewingKey {
2403 self.to_ovk(scope)
2404 }
2405
2406 fn ovk_bytes(&self, scope: zip32::Scope) -> [u8; 32] {
2407 self.to_ovk(scope).0
2408 }
2409
2410 fn add_spend<R: Rng + CryptoRng>(&self, ctx: &mut CompactTx, nf: Self::Nullifier, _: &mut R) {
2411 let cspend = CompactSaplingSpend { nf: nf.to_vec() };
2412 ctx.spends.push(cspend);
2413 }
2414
2415 fn add_output<P: consensus::Parameters, R: Rng + CryptoRng>(
2416 &self,
2417 ctx: &mut CompactTx,
2418 params: &P,
2419 height: BlockHeight,
2420 sender_ovk: Option<&Self::OutgoingViewingKey>,
2421 recipient_address_type: AddressType,
2422 value: Zatoshis,
2423 initial_sapling_tree_size: u32,
2424 rng: &mut R,
2425 ) -> Self::Nullifier {
2426 let recipient = match recipient_address_type {
2427 AddressType::DefaultExternal => self.default_address().1,
2428 AddressType::DiversifiedExternal(idx) => self.find_address(idx).unwrap().1,
2429 AddressType::Internal => self.change_address().1,
2430 };
2431
2432 let position = initial_sapling_tree_size + ctx.outputs.len() as u32;
2433
2434 let (cout, note) =
2435 compact_sapling_output(params, height, recipient, value, sender_ovk.copied(), rng);
2436 ctx.outputs.push(cout);
2437
2438 note.nf(self.fvk().vk.nk(), u64::from(position))
2439 }
2440
2441 #[allow(clippy::too_many_arguments)]
2442 fn add_logical_action<P: consensus::Parameters, R: Rng + CryptoRng>(
2443 &self,
2444 ctx: &mut CompactTx,
2445 params: &P,
2446 height: BlockHeight,
2447 nf_to_reveal_in_spend: Self::Nullifier,
2448 sender_ovk: Option<&Self::OutgoingViewingKey>,
2449 recipient_address_type: AddressType,
2450 value: Zatoshis,
2451 initial_sapling_tree_size: u32,
2452 rng: &mut R,
2453 ) -> Self::Nullifier {
2454 self.add_spend(ctx, nf_to_reveal_in_spend, rng);
2455 self.add_output(
2456 ctx,
2457 params,
2458 height,
2459 sender_ovk,
2460 recipient_address_type,
2461 value,
2462 initial_sapling_tree_size,
2463 rng,
2464 )
2465 }
2466}
2467
2468#[cfg(feature = "orchard")]
2469impl TestFvk for ::orchard::keys::FullViewingKey {
2470 type Nullifier = ::orchard::note::Nullifier;
2471 type OutgoingViewingKey = ::orchard::keys::OutgoingViewingKey;
2472
2473 fn to_ovk(&self, scope: zip32::Scope) -> Self::OutgoingViewingKey {
2474 self.to_ovk(scope)
2475 }
2476
2477 fn ovk_bytes(&self, scope: zip32::Scope) -> [u8; 32] {
2478 *self.to_ovk(scope).as_ref()
2479 }
2480
2481 fn add_spend<R: Rng + CryptoRng>(
2482 &self,
2483 ctx: &mut CompactTx,
2484 nullifier_to_reveal: Self::Nullifier,
2485 rng: &mut R,
2486 ) {
2487 let recipient = loop {
2490 let mut bytes = [0; 32];
2491 rng.fill_bytes(&mut bytes);
2492 let sk = ::orchard::keys::SpendingKey::from_bytes(bytes);
2493 if sk.is_some().into() {
2494 break ::orchard::keys::FullViewingKey::from(&sk.unwrap())
2495 .address_at(0u32, zip32::Scope::External);
2496 }
2497 };
2498
2499 let (cact, _) =
2500 compact_orchard_action(nullifier_to_reveal, recipient, Zatoshis::ZERO, None, rng);
2501 ctx.actions.push(cact);
2502 }
2503
2504 fn add_output<P: consensus::Parameters, R: Rng + CryptoRng>(
2505 &self,
2506 ctx: &mut CompactTx,
2507 _: &P,
2508 _: BlockHeight,
2509 sender_ovk: Option<&Self::OutgoingViewingKey>,
2510 recipient_address_type: AddressType,
2511 value: Zatoshis,
2512 _: u32, mut rng: &mut R,
2514 ) -> Self::Nullifier {
2515 let nullifier_to_reveal =
2517 ::orchard::note::Nullifier::from_bytes(&pallas::Base::random(&mut rng).to_repr())
2518 .unwrap();
2519
2520 let (j, scope) = match recipient_address_type {
2521 AddressType::DefaultExternal => (0u32.into(), zip32::Scope::External),
2522 AddressType::DiversifiedExternal(idx) => (idx, zip32::Scope::External),
2523 AddressType::Internal => (0u32.into(), zip32::Scope::Internal),
2524 };
2525
2526 let (cact, note) = compact_orchard_action(
2527 nullifier_to_reveal,
2528 self.address_at(j, scope),
2529 value,
2530 sender_ovk,
2531 rng,
2532 );
2533 ctx.actions.push(cact);
2534
2535 note.nullifier(self)
2536 }
2537
2538 fn add_logical_action<P: consensus::Parameters, R: Rng + CryptoRng>(
2539 &self,
2540 ctx: &mut CompactTx,
2541 _: &P,
2542 _: BlockHeight,
2543 nf_to_reveal_in_spend: Self::Nullifier,
2544 sender_ovk: Option<&Self::OutgoingViewingKey>,
2545 recipient_address_type: AddressType,
2546 value: Zatoshis,
2547 _: u32, rng: &mut R,
2549 ) -> Self::Nullifier {
2550 let (j, scope) = match recipient_address_type {
2551 AddressType::DefaultExternal => (0u32.into(), zip32::Scope::External),
2552 AddressType::DiversifiedExternal(idx) => (idx, zip32::Scope::External),
2553 AddressType::Internal => (0u32.into(), zip32::Scope::Internal),
2554 };
2555
2556 let (cact, note) = compact_orchard_action(
2557 nf_to_reveal_in_spend,
2558 self.address_at(j, scope),
2559 value,
2560 sender_ovk,
2561 rng,
2562 );
2563 ctx.actions.push(cact);
2564
2565 note.nullifier(self)
2567 }
2568}
2569
2570#[cfg(feature = "orchard")]
2575#[derive(Clone)]
2576pub struct IronwoodFvk(pub ::orchard::keys::FullViewingKey);
2577
2578#[cfg(feature = "orchard")]
2579impl TestFvk for IronwoodFvk {
2580 type Nullifier = ::orchard::note::Nullifier;
2581 type OutgoingViewingKey = ::orchard::keys::OutgoingViewingKey;
2582
2583 fn to_ovk(&self, scope: zip32::Scope) -> Self::OutgoingViewingKey {
2584 self.0.to_ovk(scope)
2585 }
2586
2587 fn ovk_bytes(&self, scope: zip32::Scope) -> [u8; 32] {
2588 *self.0.to_ovk(scope).as_ref()
2589 }
2590
2591 fn add_spend<R: Rng + CryptoRng>(
2592 &self,
2593 ctx: &mut CompactTx,
2594 nullifier_to_reveal: Self::Nullifier,
2595 rng: &mut R,
2596 ) {
2597 let recipient = loop {
2600 let mut bytes = [0; 32];
2601 rng.fill_bytes(&mut bytes);
2602 let sk = ::orchard::keys::SpendingKey::from_bytes(bytes);
2603 if sk.is_some().into() {
2604 break ::orchard::keys::FullViewingKey::from(&sk.unwrap())
2605 .address_at(0u32, zip32::Scope::External);
2606 }
2607 };
2608
2609 let (cact, _) =
2610 compact_ironwood_action(nullifier_to_reveal, recipient, Zatoshis::ZERO, None, rng);
2611 ctx.ironwood_actions.push(cact);
2612 }
2613
2614 fn add_output<P: consensus::Parameters, R: Rng + CryptoRng>(
2615 &self,
2616 ctx: &mut CompactTx,
2617 _: &P,
2618 _: BlockHeight,
2619 sender_ovk: Option<&Self::OutgoingViewingKey>,
2620 recipient_address_type: AddressType,
2621 value: Zatoshis,
2622 _: u32, mut rng: &mut R,
2624 ) -> Self::Nullifier {
2625 let nullifier_to_reveal =
2627 ::orchard::note::Nullifier::from_bytes(&pallas::Base::random(&mut rng).to_repr())
2628 .unwrap();
2629
2630 let (j, scope) = match recipient_address_type {
2631 AddressType::DefaultExternal => (0u32.into(), zip32::Scope::External),
2632 AddressType::DiversifiedExternal(idx) => (idx, zip32::Scope::External),
2633 AddressType::Internal => (0u32.into(), zip32::Scope::Internal),
2634 };
2635
2636 let (cact, note) = compact_ironwood_action(
2637 nullifier_to_reveal,
2638 self.0.address_at(j, scope),
2639 value,
2640 sender_ovk,
2641 rng,
2642 );
2643 ctx.ironwood_actions.push(cact);
2644
2645 note.nullifier(&self.0)
2646 }
2647
2648 fn add_logical_action<P: consensus::Parameters, R: Rng + CryptoRng>(
2649 &self,
2650 ctx: &mut CompactTx,
2651 _: &P,
2652 _: BlockHeight,
2653 nf_to_reveal_in_spend: Self::Nullifier,
2654 sender_ovk: Option<&Self::OutgoingViewingKey>,
2655 recipient_address_type: AddressType,
2656 value: Zatoshis,
2657 _: u32, rng: &mut R,
2659 ) -> Self::Nullifier {
2660 let (j, scope) = match recipient_address_type {
2661 AddressType::DefaultExternal => (0u32.into(), zip32::Scope::External),
2662 AddressType::DiversifiedExternal(idx) => (idx, zip32::Scope::External),
2663 AddressType::Internal => (0u32.into(), zip32::Scope::Internal),
2664 };
2665
2666 let (cact, note) = compact_ironwood_action(
2667 nf_to_reveal_in_spend,
2668 self.0.address_at(j, scope),
2669 value,
2670 sender_ovk,
2671 rng,
2672 );
2673 ctx.ironwood_actions.push(cact);
2674
2675 note.nullifier(&self.0)
2677 }
2678}
2679
2680#[derive(Clone, Copy, Debug)]
2684pub enum AddressType {
2685 DefaultExternal,
2687 #[allow(dead_code)]
2690 DiversifiedExternal(DiversifierIndex),
2691 Internal,
2696}
2697
2698fn compact_sapling_output<P: consensus::Parameters, R: Rng + CryptoRng>(
2702 params: &P,
2703 height: BlockHeight,
2704 recipient: ::sapling::PaymentAddress,
2705 value: Zatoshis,
2706 sender_ovk: Option<::sapling::keys::OutgoingViewingKey>,
2707 rng: &mut R,
2708) -> (CompactSaplingOutput, ::sapling::Note) {
2709 let rseed = generate_random_rseed(zip212_enforcement(params, height), rng);
2710 let note = ::sapling::Note::from_parts(
2711 recipient,
2712 ::sapling::value::NoteValue::from_raw(value.into_u64()),
2713 rseed,
2714 );
2715 let encryptor = sapling_note_encryption(
2716 sender_ovk,
2717 note.clone(),
2718 MemoBytes::empty().into_bytes(),
2719 rng,
2720 );
2721 let cmu = note.cmu().to_bytes().to_vec();
2722 let ephemeral_key = SaplingDomain::epk_bytes(encryptor.epk()).0.to_vec();
2723 let enc_ciphertext = encryptor.encrypt_note_plaintext();
2724
2725 (
2726 CompactSaplingOutput {
2727 cmu,
2728 ephemeral_key,
2729 ciphertext: enc_ciphertext.0[..::sapling::note_encryption::COMPACT_NOTE_SIZE].to_vec(),
2730 },
2731 note,
2732 )
2733}
2734
2735#[cfg(feature = "orchard")]
2739fn compact_orchard_action<R: Rng + CryptoRng>(
2740 nf_old: ::orchard::note::Nullifier,
2741 recipient: ::orchard::Address,
2742 value: Zatoshis,
2743 sender_ovk: Option<&::orchard::keys::OutgoingViewingKey>,
2744 rng: &mut R,
2745) -> (CompactOrchardAction, ::orchard::Note) {
2746 let (compact_action, note) = ::orchard::note_encryption::testing::fake_compact_action(
2747 rng,
2748 nf_old,
2749 recipient,
2750 ::orchard::value::NoteValue::from_raw(value.into_u64()),
2751 sender_ovk.cloned(),
2752 );
2753
2754 (
2755 CompactOrchardAction {
2756 nullifier: compact_action.nullifier().to_bytes().to_vec(),
2757 cmx: compact_action.cmx().to_bytes().to_vec(),
2758 ephemeral_key:
2759 ShieldedOutput::<::orchard::note_encryption::OrchardDomain>::ephemeral_key(
2760 &compact_action,
2761 )
2762 .0
2763 .to_vec(),
2764 ciphertext:
2765 ShieldedOutput::<::orchard::note_encryption::OrchardDomain>::enc_ciphertext_compact(
2766 &compact_action,
2767 )
2768 .0
2769 .to_vec(),
2770 },
2771 note,
2772 )
2773}
2774
2775#[cfg(feature = "orchard")]
2780fn compact_ironwood_action<R: Rng + CryptoRng>(
2781 nf_old: ::orchard::note::Nullifier,
2782 recipient: ::orchard::Address,
2783 value: Zatoshis,
2784 sender_ovk: Option<&::orchard::keys::OutgoingViewingKey>,
2785 rng: &mut R,
2786) -> (CompactOrchardAction, ::orchard::Note) {
2787 let rho = Rho::from_bytes(&nf_old.to_bytes()).unwrap();
2791 let rseed = loop {
2792 let mut bytes = [0u8; 32];
2793 rng.fill_bytes(&mut bytes);
2794 if let Some(rseed) = Option::from(RandomSeed::from_bytes(bytes, &rho)) {
2795 break rseed;
2796 }
2797 };
2798 let note = OrchardNote::from_parts(
2799 recipient,
2800 ::orchard::value::NoteValue::from_raw(value.into_u64()),
2801 rho,
2802 rseed,
2803 NoteVersion::V3,
2804 )
2805 .unwrap();
2806 let encryptor = IronwoodNoteEncryption::new(sender_ovk.cloned(), note, [0u8; 512]);
2807 let cmx = ExtractedNoteCommitment::from(note.commitment());
2808 let ephemeral_key = IronwoodDomain::epk_bytes(encryptor.epk());
2809 let enc_ciphertext = encryptor.encrypt_note_plaintext();
2810
2811 (
2812 CompactOrchardAction {
2813 nullifier: nf_old.to_bytes().to_vec(),
2814 cmx: cmx.to_bytes().to_vec(),
2815 ephemeral_key: ephemeral_key.0.to_vec(),
2816 ciphertext: enc_ciphertext.0[..::orchard::note_encryption::COMPACT_NOTE_SIZE].to_vec(),
2817 },
2818 note,
2819 )
2820}
2821
2822fn fake_compact_tx<R: Rng + CryptoRng>(rng: &mut R) -> CompactTx {
2824 let mut ctx = CompactTx::default();
2825 let mut txid = vec![0; 32];
2826 rng.fill_bytes(&mut txid);
2827 ctx.txid = txid;
2828
2829 ctx
2830}
2831
2832#[derive(Clone)]
2838pub struct FakeCompactOutput<Fvk> {
2839 recipient_fvk: Fvk,
2840 recipient_address_type: AddressType,
2841 value: Zatoshis,
2842}
2843
2844impl<Fvk> FakeCompactOutput<Fvk> {
2845 pub fn new(recipient_fvk: Fvk, recipient_address_type: AddressType, value: Zatoshis) -> Self {
2847 Self {
2848 recipient_fvk,
2849 recipient_address_type,
2850 value,
2851 }
2852 }
2853
2854 pub fn random<R: Rng>(rng: &mut R, recipient_fvk: Fvk) -> Self {
2857 Self {
2858 recipient_fvk,
2859 recipient_address_type: AddressType::DefaultExternal,
2860 value: Zatoshis::const_from_u64(rng.random_range(10000..1000000)),
2861 }
2862 }
2863}
2864
2865#[allow(clippy::too_many_arguments)]
2870fn fake_compact_block<P: consensus::Parameters, Fvk: TestFvk>(
2871 params: &P,
2872 height: BlockHeight,
2873 prev_hash: BlockHash,
2874 sender_ovk: Option<&Fvk::OutgoingViewingKey>,
2875 outputs: &[FakeCompactOutput<Fvk>],
2876 initial_sapling_tree_size: u32,
2877 initial_orchard_tree_size: u32,
2878 initial_ironwood_tree_size: u32,
2879 mut rng: impl CryptoRng,
2880) -> (CompactBlock, Vec<Fvk::Nullifier>) {
2881 let mut ctx = fake_compact_tx(&mut rng);
2883 let mut nfs = vec![];
2884 for output in outputs {
2885 let nf = output.recipient_fvk.add_output(
2886 &mut ctx,
2887 params,
2888 height,
2889 sender_ovk,
2890 output.recipient_address_type,
2891 output.value,
2892 initial_sapling_tree_size,
2893 &mut rng,
2894 );
2895 nfs.push(nf);
2896 }
2897
2898 let cb = fake_compact_block_from_compact_tx(
2899 ctx,
2900 height,
2901 prev_hash,
2902 initial_sapling_tree_size,
2903 initial_orchard_tree_size,
2904 initial_ironwood_tree_size,
2905 rng,
2906 );
2907 (cb, nfs)
2908}
2909
2910#[allow(clippy::too_many_arguments)]
2913fn fake_compact_block_from_tx(
2914 height: BlockHeight,
2915 prev_hash: BlockHash,
2916 tx_index: usize,
2917 tx: &Transaction,
2918 initial_sapling_tree_size: u32,
2919 initial_orchard_tree_size: u32,
2920 initial_ironwood_tree_size: u32,
2921 rng: impl Rng,
2922) -> CompactBlock {
2923 let mut ctx = CompactTx {
2925 index: tx_index as u64,
2926 txid: tx.txid().as_ref().to_vec(),
2927 ..Default::default()
2928 };
2929
2930 if let Some(bundle) = tx.sapling_bundle() {
2931 for spend in bundle.shielded_spends() {
2932 ctx.spends.push(spend.into());
2933 }
2934 for output in bundle.shielded_outputs() {
2935 ctx.outputs.push(output.into());
2936 }
2937 }
2938
2939 #[cfg(feature = "orchard")]
2940 if let Some(bundle) = tx.orchard_bundle() {
2941 for action in bundle.actions() {
2942 ctx.actions.push(action.into());
2943 }
2944 }
2945
2946 #[cfg(feature = "orchard")]
2947 if let Some(bundle) = tx.ironwood_bundle() {
2948 for action in bundle.actions() {
2949 ctx.ironwood_actions.push(action.into());
2950 }
2951 }
2952
2953 fake_compact_block_from_compact_tx(
2954 ctx,
2955 height,
2956 prev_hash,
2957 initial_sapling_tree_size,
2958 initial_orchard_tree_size,
2959 initial_ironwood_tree_size,
2960 rng,
2961 )
2962}
2963
2964#[allow(clippy::too_many_arguments)]
2967fn fake_compact_block_spending<P: consensus::Parameters, Fvk: TestFvk>(
2968 params: &P,
2969 height: BlockHeight,
2970 prev_hash: BlockHash,
2971 (nf, in_value): (Fvk::Nullifier, Zatoshis),
2972 sender_fvk: &Fvk,
2973 to: Address,
2974 value: Zatoshis,
2975 initial_sapling_tree_size: u32,
2976 initial_orchard_tree_size: u32,
2977 initial_ironwood_tree_size: u32,
2978 mut rng: impl CryptoRng,
2979) -> CompactBlock {
2980 let mut ctx = fake_compact_tx(&mut rng);
2981
2982 sender_fvk.add_logical_action(
2984 &mut ctx,
2985 params,
2986 height,
2987 nf,
2988 Some(&sender_fvk.to_ovk(zip32::Scope::Internal)),
2989 AddressType::Internal,
2990 (in_value - value).unwrap(),
2991 initial_sapling_tree_size,
2992 &mut rng,
2993 );
2994
2995 let ovk_bytes = sender_fvk.ovk_bytes(zip32::Scope::External);
2997 match to {
2998 Address::Sapling(recipient) => ctx.outputs.push(
2999 compact_sapling_output(
3000 params,
3001 height,
3002 *recipient,
3003 value,
3004 Some(::sapling::keys::OutgoingViewingKey(ovk_bytes)),
3005 &mut rng,
3006 )
3007 .0,
3008 ),
3009 Address::Transparent(_) | Address::Tex(_) => {
3010 panic!("transparent addresses not supported in compact blocks")
3011 }
3012 Address::Unified(ua) => {
3013 let mut done = false;
3016
3017 #[cfg(feature = "orchard")]
3018 if let Some(recipient) = ua.orchard() {
3019 let nullifier = ::orchard::note::Nullifier::from_bytes(
3021 &pallas::Base::random(&mut rng).to_repr(),
3022 )
3023 .unwrap();
3024
3025 ctx.actions.push(
3026 compact_orchard_action(
3027 nullifier,
3028 *recipient,
3029 value,
3030 Some(&::orchard::keys::OutgoingViewingKey::from(ovk_bytes)),
3031 &mut rng,
3032 )
3033 .0,
3034 );
3035 done = true;
3036 }
3037
3038 if !done && let Some(recipient) = ua.sapling() {
3039 ctx.outputs.push(
3040 compact_sapling_output(
3041 params,
3042 height,
3043 *recipient,
3044 value,
3045 Some(::sapling::keys::OutgoingViewingKey(ovk_bytes)),
3046 &mut rng,
3047 )
3048 .0,
3049 );
3050 done = true;
3051 }
3052 if !done {
3053 panic!("No supported shielded receiver to send funds to");
3054 }
3055 }
3056 }
3057
3058 fake_compact_block_from_compact_tx(
3059 ctx,
3060 height,
3061 prev_hash,
3062 initial_sapling_tree_size,
3063 initial_orchard_tree_size,
3064 initial_ironwood_tree_size,
3065 rng,
3066 )
3067}
3068
3069fn fake_compact_block_from_compact_tx(
3070 ctx: CompactTx,
3071 height: BlockHeight,
3072 prev_hash: BlockHash,
3073 initial_sapling_tree_size: u32,
3074 initial_orchard_tree_size: u32,
3075 initial_ironwood_tree_size: u32,
3076 mut rng: impl Rng,
3077) -> CompactBlock {
3078 let mut cb = CompactBlock {
3079 hash: {
3080 let mut hash = vec![0; 32];
3081 rng.fill_bytes(&mut hash);
3082 hash
3083 },
3084 height: height.into(),
3085 ..Default::default()
3086 };
3087 cb.prev_hash.extend_from_slice(&prev_hash.0);
3088 cb.vtx.push(ctx);
3089 cb.chain_metadata = Some(compact_formats::ChainMetadata {
3090 sapling_commitment_tree_size: initial_sapling_tree_size
3091 + cb.vtx.iter().map(|tx| tx.outputs.len() as u32).sum::<u32>(),
3092 orchard_commitment_tree_size: initial_orchard_tree_size
3093 + cb.vtx.iter().map(|tx| tx.actions.len() as u32).sum::<u32>(),
3094 ironwood_commitment_tree_size: initial_ironwood_tree_size
3095 + cb.vtx
3096 .iter()
3097 .map(|tx| tx.ironwood_actions.len() as u32)
3098 .sum::<u32>(),
3099 });
3100 cb
3101}
3102
3103pub trait CacheInsertionResult {
3106 fn txids(&self) -> &[TxId];
3107}
3108
3109pub trait TestCache {
3111 type BsError: core::fmt::Debug;
3112 type BlockSource: BlockSource<Error = Self::BsError>;
3113 type InsertResult: CacheInsertionResult;
3114
3115 fn block_source(&self) -> &Self::BlockSource;
3117
3118 fn insert(&mut self, cb: &CompactBlock) -> Self::InsertResult;
3120
3121 fn truncate_to_height(&mut self, height: BlockHeight);
3124}
3125
3126pub struct NoteCommitments {
3130 sapling: Vec<::sapling::Node>,
3131 #[cfg(feature = "orchard")]
3132 orchard: Vec<MerkleHashOrchard>,
3133}
3134
3135impl NoteCommitments {
3136 pub fn from_compact_block(cb: &CompactBlock) -> Self {
3138 NoteCommitments {
3139 sapling: cb
3140 .vtx
3141 .iter()
3142 .flat_map(|tx| {
3143 tx.outputs
3144 .iter()
3145 .map(|out| ::sapling::Node::from_cmu(&out.cmu().unwrap()))
3146 })
3147 .collect(),
3148 #[cfg(feature = "orchard")]
3149 orchard: cb
3150 .vtx
3151 .iter()
3152 .flat_map(|tx| {
3153 tx.actions
3154 .iter()
3155 .map(|act| MerkleHashOrchard::from_cmx(&act.cmx().unwrap()))
3156 })
3157 .collect(),
3158 }
3159 }
3160
3161 #[allow(dead_code)]
3163 pub fn sapling(&self) -> &[::sapling::Node] {
3164 self.sapling.as_ref()
3165 }
3166
3167 #[cfg(feature = "orchard")]
3169 pub fn orchard(&self) -> &[MerkleHashOrchard] {
3170 self.orchard.as_ref()
3171 }
3172}
3173
3174pub struct MockWalletDb {
3176 pub network: Network,
3177 pub sapling_tree: ShardTree<
3178 MemoryShardStore<::sapling::Node, BlockHeight>,
3179 { SAPLING_SHARD_HEIGHT * 2 },
3180 SAPLING_SHARD_HEIGHT,
3181 >,
3182 #[cfg(feature = "orchard")]
3183 pub orchard_tree: ShardTree<
3184 MemoryShardStore<::orchard::tree::MerkleHashOrchard, BlockHeight>,
3185 { ORCHARD_SHARD_HEIGHT * 2 },
3186 ORCHARD_SHARD_HEIGHT,
3187 >,
3188 account_ids: Vec<u32>,
3189 addresses_by_account: HashMap<u32, Vec<AddressInfo>>,
3190 ufvks_by_account: HashMap<u32, UnifiedFullViewingKey>,
3191}
3192
3193impl MockWalletDb {
3194 pub fn new(network: Network) -> Self {
3196 Self {
3197 network,
3198 sapling_tree: ShardTree::new(MemoryShardStore::empty(), 100),
3199 #[cfg(feature = "orchard")]
3200 orchard_tree: ShardTree::new(MemoryShardStore::empty(), 100),
3201 account_ids: vec![],
3202 addresses_by_account: HashMap::new(),
3203 ufvks_by_account: HashMap::new(),
3204 }
3205 }
3206
3207 #[cfg(test)]
3208 pub(crate) fn from_account_addresses(
3209 network: Network,
3210 entries: impl IntoIterator<Item = (u32, Vec<AddressInfo>)>,
3211 ) -> Self {
3212 let mut wallet = Self::new(network);
3213 for (account_id, addresses) in entries {
3214 wallet.account_ids.push(account_id);
3215 wallet.addresses_by_account.insert(account_id, addresses);
3216 }
3217 wallet.account_ids.sort_unstable();
3218 wallet
3219 }
3220
3221 #[cfg(test)]
3222 pub(crate) fn from_account_ufvks(
3223 network: Network,
3224 entries: impl IntoIterator<Item = (u32, UnifiedFullViewingKey)>,
3225 ) -> Self {
3226 let mut wallet = Self::new(network);
3227 for (account_id, ufvk) in entries {
3228 wallet.account_ids.push(account_id);
3229 wallet.ufvks_by_account.insert(account_id, ufvk);
3230 }
3231 wallet.account_ids.sort_unstable();
3232 wallet
3233 }
3234}
3235
3236impl InputSource for MockWalletDb {
3237 type Error = ();
3238 type NoteRef = u32;
3239 type AccountId = u32;
3240
3241 fn anchor_computable(
3242 &self,
3243 protocol: ShieldedPool,
3244 height: BlockHeight,
3245 ) -> Result<bool, Self::Error> {
3246 match protocol {
3247 ShieldedPool::Sapling => Ok(self
3248 .sapling_tree
3249 .store()
3250 .get_checkpoint(&height)
3251 .map_err(|_| ())?
3252 .is_some()),
3253 #[cfg(feature = "orchard")]
3254 ShieldedPool::Orchard => Ok(self
3255 .orchard_tree
3256 .store()
3257 .get_checkpoint(&height)
3258 .map_err(|_| ())?
3259 .is_some()),
3260 _ => Ok(false),
3263 }
3264 }
3265
3266 fn get_spendable_note(
3267 &self,
3268 _txid: &TxId,
3269 _protocol: ShieldedPool,
3270 _index: u32,
3271 _target_height: TargetHeight,
3272 _lock_filter: LockFilter<'_>,
3273 ) -> Result<Option<ReceivedNote<Self::NoteRef, Note>>, Self::Error> {
3274 Ok(None)
3275 }
3276
3277 fn select_spendable_notes(
3278 &self,
3279 _account: Self::AccountId,
3280 _target_value: TargetValue,
3281 _sources: &[ShieldedPool],
3282 _target_height: TargetHeight,
3283 _confirmations_policy: ConfirmationsPolicy,
3284 _exclude: &[Self::NoteRef],
3285 _lock_filter: LockFilter<'_>,
3286 ) -> Result<ReceivedNotes<Self::NoteRef>, Self::Error> {
3287 Ok(ReceivedNotes::empty())
3288 }
3289
3290 fn select_unspent_notes(
3291 &self,
3292 _account: Self::AccountId,
3293 _sources: &[ShieldedPool],
3294 _target_height: TargetHeight,
3295 _exclude: &[Self::NoteRef],
3296 _lock_filter: LockFilter<'_>,
3297 ) -> Result<ReceivedNotes<Self::NoteRef>, Self::Error> {
3298 Err(())
3299 }
3300
3301 fn get_change_receivers(&self, account: Self::AccountId) -> Result<ChangeReceivers, ()> {
3302 self.ufvks_by_account
3303 .get(&account)
3304 .map(|ufvk| ChangeReceivers::from_uivk(&ufvk.to_unified_incoming_viewing_key()))
3305 .ok_or(())
3306 }
3307
3308 fn get_account_metadata(
3309 &self,
3310 _account: Self::AccountId,
3311 _selector: &NoteFilter,
3312 _target_height: TargetHeight,
3313 _exclude: &[Self::NoteRef],
3314 _lock_filter: LockFilter<'_>,
3315 ) -> Result<AccountMeta, Self::Error> {
3316 Err(())
3317 }
3318}
3319
3320impl WalletRead for MockWalletDb {
3321 type Error = ();
3322 type AccountId = u32;
3323 type Account = (Self::AccountId, UnifiedFullViewingKey, BlockHeight);
3324
3325 fn get_account_ids(&self) -> Result<Vec<Self::AccountId>, Self::Error> {
3326 Ok(self.account_ids.clone())
3327 }
3328
3329 fn get_account(
3330 &self,
3331 account_id: Self::AccountId,
3332 ) -> Result<Option<Self::Account>, Self::Error> {
3333 Ok(self
3334 .ufvks_by_account
3335 .get(&account_id)
3336 .cloned()
3337 .map(|ufvk| (account_id, ufvk, BlockHeight::from(1))))
3338 }
3339
3340 fn get_derived_account(
3341 &self,
3342 _derivation: &Zip32Derivation,
3343 ) -> Result<Option<Self::Account>, Self::Error> {
3344 Ok(None)
3345 }
3346
3347 fn validate_seed(
3348 &self,
3349 _account_id: Self::AccountId,
3350 _seed: &SecretVec<u8>,
3351 ) -> Result<bool, Self::Error> {
3352 Ok(false)
3353 }
3354
3355 fn seed_relevance_to_derived_accounts(
3356 &self,
3357 _seed: &SecretVec<u8>,
3358 ) -> Result<SeedRelevance<Self::AccountId>, Self::Error> {
3359 Ok(SeedRelevance::NoAccounts)
3360 }
3361
3362 fn get_account_for_ufvk(
3363 &self,
3364 _ufvk: &UnifiedFullViewingKey,
3365 ) -> Result<Option<Self::Account>, Self::Error> {
3366 Ok(None)
3367 }
3368
3369 fn list_addresses(&self, account: Self::AccountId) -> Result<Vec<AddressInfo>, Self::Error> {
3370 Ok(self
3371 .addresses_by_account
3372 .get(&account)
3373 .cloned()
3374 .unwrap_or_default())
3375 }
3376
3377 fn find_account_for_address<P: consensus::Parameters>(
3378 &self,
3379 params: &P,
3380 address: &zcash_keys::address::Address,
3381 ) -> Result<Option<Self::AccountId>, super::error::FindAccountForAddressError<Self::Error>>
3382 {
3383 super::defaults::find_account_for_address(self, params, address)
3384 }
3385
3386 fn get_last_generated_address_matching(
3387 &self,
3388 _account: Self::AccountId,
3389 _request: UnifiedAddressRequest,
3390 ) -> Result<Option<UnifiedAddress>, Self::Error> {
3391 Ok(None)
3392 }
3393
3394 fn get_account_birthday(&self, _account: Self::AccountId) -> Result<BlockHeight, Self::Error> {
3395 Err(())
3396 }
3397
3398 fn get_wallet_birthday(&self) -> Result<Option<BlockHeight>, Self::Error> {
3399 Ok(None)
3400 }
3401
3402 fn get_wallet_recover_until(&self) -> Result<Option<BlockHeight>, Self::Error> {
3403 Ok(None)
3404 }
3405
3406 fn get_wallet_summary(
3407 &self,
3408 _confirmations_policy: ConfirmationsPolicy,
3409 ) -> Result<Option<WalletSummary<Self::AccountId>>, Self::Error> {
3410 Ok(None)
3411 }
3412
3413 fn chain_height(&self) -> Result<Option<BlockHeight>, Self::Error> {
3414 Ok(None)
3415 }
3416
3417 fn get_block_hash(&self, _block_height: BlockHeight) -> Result<Option<BlockHash>, Self::Error> {
3418 Ok(None)
3419 }
3420
3421 fn block_metadata(&self, _height: BlockHeight) -> Result<Option<BlockMetadata>, Self::Error> {
3422 Ok(None)
3423 }
3424
3425 fn block_fully_scanned(&self) -> Result<Option<BlockMetadata>, Self::Error> {
3426 Ok(None)
3427 }
3428
3429 fn get_max_height_hash(&self) -> Result<Option<(BlockHeight, BlockHash)>, Self::Error> {
3430 Ok(None)
3431 }
3432
3433 fn block_max_scanned(&self) -> Result<Option<BlockMetadata>, Self::Error> {
3434 Ok(None)
3435 }
3436
3437 fn suggest_scan_ranges(&self) -> Result<Vec<ScanRange>, Self::Error> {
3438 Ok(vec![])
3439 }
3440
3441 fn get_target_and_anchor_heights(
3442 &self,
3443 _min_confirmations: NonZeroU32,
3444 ) -> Result<Option<(TargetHeight, BlockHeight)>, Self::Error> {
3445 Ok(None)
3446 }
3447
3448 fn get_tx_height(&self, _txid: TxId) -> Result<Option<BlockHeight>, Self::Error> {
3449 Ok(None)
3450 }
3451
3452 fn get_unified_full_viewing_keys(
3453 &self,
3454 ) -> Result<HashMap<Self::AccountId, UnifiedFullViewingKey>, Self::Error> {
3455 Ok(HashMap::new())
3456 }
3457
3458 fn get_memo(&self, _id_note: NoteId) -> Result<Option<Memo>, Self::Error> {
3459 Ok(None)
3460 }
3461
3462 fn get_transaction(&self, _txid: TxId) -> Result<Option<Transaction>, Self::Error> {
3463 Ok(None)
3464 }
3465
3466 fn get_sapling_nullifiers(
3467 &self,
3468 _query: NullifierQuery,
3469 ) -> Result<Vec<(Self::AccountId, ::sapling::Nullifier)>, Self::Error> {
3470 Ok(Vec::new())
3471 }
3472
3473 #[cfg(feature = "orchard")]
3474 fn get_orchard_nullifiers(
3475 &self,
3476 _query: NullifierQuery,
3477 ) -> Result<Vec<(Self::AccountId, ::orchard::note::Nullifier)>, Self::Error> {
3478 Ok(Vec::new())
3479 }
3480
3481 #[cfg(feature = "orchard")]
3482 fn get_ironwood_nullifiers(
3483 &self,
3484 _query: NullifierQuery,
3485 ) -> Result<Vec<(Self::AccountId, ::orchard::note::Nullifier)>, Self::Error> {
3486 Ok(Vec::new())
3487 }
3488
3489 #[cfg(feature = "transparent-inputs")]
3490 fn get_unspent_transparent_outpoints(
3491 &self,
3492 ) -> Result<HashMap<OutPoint, Self::AccountId>, Self::Error> {
3493 Ok(HashMap::new())
3494 }
3495
3496 #[cfg(feature = "transparent-inputs")]
3497 fn get_transparent_receiver_accounts(
3498 &self,
3499 ) -> Result<
3500 HashMap<TransparentAddress, (Self::AccountId, Option<TransparentKeyScope>)>,
3501 Self::Error,
3502 > {
3503 Ok(HashMap::new())
3504 }
3505
3506 #[cfg(feature = "transparent-inputs")]
3507 fn get_transparent_receivers(
3508 &self,
3509 _account: Self::AccountId,
3510 _include_change: bool,
3511 _include_standalone: bool,
3512 ) -> Result<HashMap<TransparentAddress, TransparentAddressMetadata>, Self::Error> {
3513 Ok(HashMap::new())
3514 }
3515
3516 #[cfg(feature = "transparent-inputs")]
3517 fn get_transparent_balances(
3518 &self,
3519 _account: Self::AccountId,
3520 _target_height: TargetHeight,
3521 _confirmations_policy: ConfirmationsPolicy,
3522 ) -> Result<TransparentBalances, Self::Error> {
3523 Ok(HashMap::new())
3524 }
3525
3526 #[cfg(feature = "transparent-inputs")]
3527 fn get_transparent_address_metadata(
3528 &self,
3529 _account: Self::AccountId,
3530 _address: &TransparentAddress,
3531 ) -> Result<Option<TransparentAddressMetadata>, Self::Error> {
3532 Ok(None)
3533 }
3534
3535 #[cfg(feature = "transparent-inputs")]
3536 fn utxo_query_height(&self, _account: Self::AccountId) -> Result<BlockHeight, Self::Error> {
3537 Ok(BlockHeight::from(0u32))
3538 }
3539
3540 fn transaction_data_requests(&self) -> Result<Vec<TransactionDataRequest>, Self::Error> {
3541 Ok(vec![])
3542 }
3543
3544 fn get_received_outputs(
3545 &self,
3546 _txid: TxId,
3547 _target_height: TargetHeight,
3548 _confirmations_policy: ConfirmationsPolicy,
3549 ) -> Result<Vec<ReceivedTransactionOutput>, Self::Error> {
3550 Ok(vec![])
3551 }
3552}
3553
3554impl OutputLockStore for MockWalletDb {
3555 type Error = ();
3556 type AccountId = u32;
3557
3558 fn lock_outputs(
3559 &mut self,
3560 _outputs: &[OutputRef],
3561 _owner: LockOwner,
3562 _lock_expiry_height: BlockHeight,
3563 ) -> Result<usize, LockError<Self::Error>> {
3564 Ok(0)
3565 }
3566
3567 fn unlock_output(
3568 &mut self,
3569 _output: &OutputRef,
3570 _owner: LockOwner,
3571 ) -> Result<bool, Self::Error> {
3572 Ok(false)
3573 }
3574
3575 fn clear_locked_outputs(&mut self, _account: Self::AccountId) -> Result<usize, Self::Error> {
3576 Ok(0)
3577 }
3578
3579 fn get_locked_outputs(&self, _account: Self::AccountId) -> Result<Vec<OutputRef>, Self::Error> {
3580 Ok(Vec::new())
3581 }
3582}
3583
3584impl WalletWrite for MockWalletDb {
3585 type UtxoRef = u32;
3586
3587 fn create_account(
3588 &mut self,
3589 _account_name: &str,
3590 seed: &SecretVec<u8>,
3591 _birthday: &AccountBirthday,
3592 _key_source: Option<&str>,
3593 ) -> Result<(<Self as WalletRead>::AccountId, UnifiedSpendingKey), <Self as WalletRead>::Error>
3594 {
3595 let account = zip32::AccountId::ZERO;
3596 UnifiedSpendingKey::from_seed(&self.network, seed.expose_secret(), account)
3597 .map(|k| (u32::from(account), k))
3598 .map_err(|_| ())
3599 }
3600
3601 fn import_account_hd(
3602 &mut self,
3603 _account_name: &str,
3604 _seed: &SecretVec<u8>,
3605 _account_index: zip32::AccountId,
3606 _birthday: &AccountBirthday,
3607 _key_source: Option<&str>,
3608 ) -> Result<(Self::Account, UnifiedSpendingKey), <Self as WalletRead>::Error> {
3609 todo!()
3610 }
3611
3612 fn import_account_ufvk(
3613 &mut self,
3614 _account_name: &str,
3615 _unified_key: &UnifiedFullViewingKey,
3616 _birthday: &AccountBirthday,
3617 _purpose: AccountPurpose,
3618 _key_source: Option<&str>,
3619 ) -> Result<Self::Account, <Self as WalletRead>::Error> {
3620 todo!()
3621 }
3622
3623 fn delete_account(
3624 &mut self,
3625 _account: <Self as WalletRead>::AccountId,
3626 ) -> Result<(), <Self as WalletRead>::Error> {
3627 todo!()
3628 }
3629
3630 fn set_account_name(
3631 &mut self,
3632 _account: <Self as WalletRead>::AccountId,
3633 _name: &str,
3634 ) -> Result<(), <Self as WalletRead>::Error> {
3635 todo!()
3636 }
3637
3638 #[cfg(feature = "transparent-key-import")]
3639 fn import_standalone_transparent_pubkey(
3640 &mut self,
3641 _account: <Self as WalletRead>::AccountId,
3642 _address: secp256k1::PublicKey,
3643 ) -> Result<(), <Self as WalletRead>::Error> {
3644 todo!()
3645 }
3646
3647 #[cfg(feature = "transparent-key-import")]
3648 fn import_standalone_transparent_script(
3649 &mut self,
3650 _account: <Self as WalletRead>::AccountId,
3651 _script: script::Redeem,
3652 ) -> Result<(), <Self as WalletRead>::Error> {
3653 todo!()
3654 }
3655
3656 fn get_next_available_address(
3657 &mut self,
3658 _account: <Self as WalletRead>::AccountId,
3659 _request: UnifiedAddressRequest,
3660 ) -> Result<Option<(UnifiedAddress, DiversifierIndex)>, <Self as WalletRead>::Error> {
3661 Ok(None)
3662 }
3663
3664 fn get_address_for_index(
3665 &mut self,
3666 _account: <Self as WalletRead>::AccountId,
3667 _diversifier_index: DiversifierIndex,
3668 _request: UnifiedAddressRequest,
3669 ) -> Result<Option<UnifiedAddress>, <Self as WalletRead>::Error> {
3670 Ok(None)
3671 }
3672
3673 #[allow(clippy::type_complexity)]
3674 fn put_blocks(
3675 &mut self,
3676 _from_state: &ChainState,
3677 _blocks: Vec<ScannedBlock<<Self as WalletRead>::AccountId>>,
3678 ) -> Result<(), <Self as WalletRead>::Error> {
3679 Ok(())
3680 }
3681
3682 fn update_chain_tip(
3683 &mut self,
3684 _tip_height: BlockHeight,
3685 ) -> Result<(), <Self as WalletRead>::Error> {
3686 Ok(())
3687 }
3688
3689 fn prune_scan_queue_below(
3690 &mut self,
3691 _height: BlockHeight,
3692 _retain_with_priority: Option<ScanPriority>,
3693 ) -> Result<u64, <Self as WalletRead>::Error> {
3694 Ok(0)
3695 }
3696
3697 fn queue_rescan(
3698 &mut self,
3699 _range: Range<BlockHeight>,
3700 ) -> Result<(), <Self as WalletRead>::Error> {
3701 Ok(())
3702 }
3703
3704 fn store_decrypted_tx(
3705 &mut self,
3706 _received_tx: DecryptedTransaction<Transaction, <Self as WalletRead>::AccountId>,
3707 ) -> Result<(), <Self as WalletRead>::Error> {
3708 Ok(())
3709 }
3710
3711 fn set_tx_trust(
3712 &mut self,
3713 _txid: TxId,
3714 _trusted: bool,
3715 ) -> Result<(), <Self as WalletRead>::Error> {
3716 Ok(())
3717 }
3718
3719 fn store_transactions_to_be_sent(
3720 &mut self,
3721 _transactions: &[SentTransaction<<Self as WalletRead>::AccountId>],
3722 ) -> Result<(), <Self as WalletRead>::Error> {
3723 Ok(())
3724 }
3725
3726 fn truncate_to_height(
3727 &mut self,
3728 _block_height: BlockHeight,
3729 ) -> Result<BlockHeight, <Self as WalletRead>::Error> {
3730 Err(())
3731 }
3732
3733 fn truncate_to_chain_state(
3734 &mut self,
3735 _chain_state: ChainState,
3736 ) -> Result<(), <Self as WalletRead>::Error> {
3737 Err(())
3738 }
3739
3740 fn rewind_to_chain_state(
3741 &mut self,
3742 _chain_state: ChainState,
3743 _reset_account_birthdays: HashSet<<Self as WalletRead>::AccountId>,
3744 ) -> Result<(), RewindError<<Self as WalletRead>::AccountId, <Self as WalletRead>::Error>> {
3745 Err(RewindError::DataSource(()))
3746 }
3747
3748 fn put_received_transparent_utxo(
3750 &mut self,
3751 _output: &WalletTransparentOutput<<Self as WalletRead>::AccountId>,
3752 ) -> Result<Self::UtxoRef, <Self as WalletRead>::Error> {
3753 Ok(0)
3754 }
3755
3756 #[cfg(feature = "transparent-inputs")]
3757 fn reserve_next_n_ephemeral_addresses(
3758 &mut self,
3759 _account_id: <Self as WalletRead>::AccountId,
3760 _n: usize,
3761 ) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, <Self as WalletRead>::Error>
3762 {
3763 Err(())
3764 }
3765
3766 #[cfg(feature = "transparent-inputs")]
3767 fn reserve_next_n_internal_addresses(
3768 &mut self,
3769 _account_id: <Self as WalletRead>::AccountId,
3770 _n: usize,
3771 ) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, <Self as WalletRead>::Error>
3772 {
3773 Err(())
3774 }
3775
3776 fn set_transaction_status(
3777 &mut self,
3778 _txid: TxId,
3779 _status: TransactionStatus,
3780 ) -> Result<(), <Self as WalletRead>::Error> {
3781 Ok(())
3782 }
3783
3784 #[cfg(feature = "transparent-inputs")]
3785 fn notify_address_checked(
3786 &mut self,
3787 _request: TransactionsInvolvingAddress,
3788 _as_of_height: BlockHeight,
3789 ) -> Result<(), <Self as WalletRead>::Error> {
3790 Ok(())
3791 }
3792}
3793
3794impl WalletCommitmentTrees for MockWalletDb {
3795 type Error = Infallible;
3796 type SaplingShardStore<'a> = MemoryShardStore<::sapling::Node, BlockHeight>;
3797
3798 fn with_sapling_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
3799 where
3800 for<'a> F: FnMut(
3801 &'a mut ShardTree<
3802 Self::SaplingShardStore<'a>,
3803 { ::sapling::NOTE_COMMITMENT_TREE_DEPTH },
3804 SAPLING_SHARD_HEIGHT,
3805 >,
3806 ) -> Result<A, E>,
3807 E: From<ShardTreeError<Infallible>>,
3808 {
3809 callback(&mut self.sapling_tree)
3810 }
3811
3812 fn put_sapling_subtree_roots(
3813 &mut self,
3814 start_index: u64,
3815 roots: &[CommitmentTreeRoot<::sapling::Node>],
3816 ) -> Result<(), ShardTreeError<Self::Error>> {
3817 self.with_sapling_tree_mut(|t| {
3818 for (root, i) in roots.iter().zip(0u64..) {
3819 let root_addr = incrementalmerkletree::Address::from_parts(
3820 SAPLING_SHARD_HEIGHT.into(),
3821 start_index + i,
3822 );
3823 t.insert(root_addr, *root.root_hash())?;
3824 }
3825 Ok::<_, ShardTreeError<Self::Error>>(())
3826 })?;
3827
3828 Ok(())
3829 }
3830
3831 fn get_sapling_subtree_root(
3832 &mut self,
3833 index: u64,
3834 ) -> Result<Option<::sapling::Node>, ShardTreeError<Self::Error>> {
3835 self.with_sapling_tree_mut(|t| {
3836 let addr =
3837 incrementalmerkletree::Address::from_parts(SAPLING_SHARD_HEIGHT.into(), index);
3838 Ok::<_, ShardTreeError<Self::Error>>(
3839 t.store()
3840 .get_shard(addr)
3841 .map_err(ShardTreeError::Storage)?
3842 .and_then(|shard| match shard.root() {
3843 tree if tree.is_leaf() => tree.leaf_value().copied(),
3844 tree => tree.annotation().and_then(|ann| ann.as_deref().copied()),
3845 }),
3846 )
3847 })
3848 }
3849
3850 #[cfg(feature = "orchard")]
3851 type OrchardShardStore<'a> = MemoryShardStore<::orchard::tree::MerkleHashOrchard, BlockHeight>;
3852
3853 #[cfg(feature = "orchard")]
3854 fn with_orchard_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
3855 where
3856 for<'a> F: FnMut(
3857 &'a mut ShardTree<
3858 Self::OrchardShardStore<'a>,
3859 { ORCHARD_SHARD_HEIGHT * 2 },
3860 ORCHARD_SHARD_HEIGHT,
3861 >,
3862 ) -> Result<A, E>,
3863 E: From<ShardTreeError<Self::Error>>,
3864 {
3865 callback(&mut self.orchard_tree)
3866 }
3867
3868 #[cfg(feature = "orchard")]
3870 fn put_orchard_subtree_roots(
3871 &mut self,
3872 start_index: u64,
3873 roots: &[CommitmentTreeRoot<::orchard::tree::MerkleHashOrchard>],
3874 ) -> Result<(), ShardTreeError<Self::Error>> {
3875 self.with_orchard_tree_mut(|t| {
3876 for (root, i) in roots.iter().zip(0u64..) {
3877 let root_addr = incrementalmerkletree::Address::from_parts(
3878 ORCHARD_SHARD_HEIGHT.into(),
3879 start_index + i,
3880 );
3881 t.insert(root_addr, *root.root_hash())?;
3882 }
3883 Ok::<_, ShardTreeError<Self::Error>>(())
3884 })?;
3885
3886 Ok(())
3887 }
3888
3889 #[cfg(feature = "orchard")]
3890 fn get_orchard_subtree_root(
3891 &mut self,
3892 index: u64,
3893 ) -> Result<Option<::orchard::tree::MerkleHashOrchard>, ShardTreeError<Self::Error>> {
3894 self.with_orchard_tree_mut(|t| {
3895 let addr =
3896 incrementalmerkletree::Address::from_parts(ORCHARD_SHARD_HEIGHT.into(), index);
3897 Ok::<_, ShardTreeError<Self::Error>>(
3898 t.store()
3899 .get_shard(addr)
3900 .map_err(ShardTreeError::Storage)?
3901 .and_then(|shard| match shard.root() {
3902 tree if tree.is_leaf() => tree.leaf_value().copied(),
3903 tree => tree.annotation().and_then(|ann| ann.as_deref().copied()),
3904 }),
3905 )
3906 })
3907 }
3908}
3909
3910#[derive(Clone, Copy)]
3913enum FilterHandling {
3914 Honored,
3916 Ignored,
3919}
3920
3921#[derive(Debug)]
3923#[non_exhaustive]
3924pub enum FixedNotesError {
3925 SelectionLimitReached,
3927 Unsupported,
3929}
3930
3931impl fmt::Display for FixedNotesError {
3932 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3933 match self {
3934 Self::SelectionLimitReached => {
3935 write!(f, "the fixture's selection limit was reached")
3936 }
3937 Self::Unsupported => write!(f, "the fixture does not implement the operation"),
3938 }
3939 }
3940}
3941
3942impl std::error::Error for FixedNotesError {}
3943
3944fn fixed_sapling_note(value: Zatoshis) -> ::sapling::Note {
3946 let (_, recipient) = ExtendedSpendingKey::master(&[])
3947 .expect("the master key of the empty seed is valid")
3948 .default_address();
3949 ::sapling::Note::from_parts(
3950 recipient,
3951 SaplingNoteValue::from_raw(value.into_u64()),
3952 Rseed::AfterZip212([0; 32]),
3953 )
3954}
3955
3956#[cfg(feature = "orchard")]
3959fn fixed_orchard_note(value: Zatoshis, version: NoteVersion) -> OrchardNote {
3960 let sk = OrchardSpendingKey::from_zip32_seed(
3961 &[0; 32],
3962 NetworkConstants::coin_type(&Network::TestNetwork),
3963 zip32::AccountId::ZERO,
3964 )
3965 .expect("the all-zero seed derives a valid Orchard spending key");
3966 let recipient = OrchardFullViewingKey::from(&sk).address_at(0u32, Scope::External);
3967 let rho = Option::from(Rho::from_bytes(&[0; 32])).expect("zero is a canonical rho");
3968 let mut rng = ChaChaRng::from_seed([0; 32]);
3969 let rseed = loop {
3970 if let Some(rseed) = Option::from(RandomSeed::from_bytes(rng.random(), &rho)) {
3971 break rseed;
3972 }
3973 };
3974 Option::from(OrchardNote::from_parts(
3975 recipient,
3976 OrchardNoteValue::from_raw(value.into_u64()),
3977 rho,
3978 rseed,
3979 version,
3980 ))
3981 .expect("the parts form a valid Orchard note")
3982}
3983
3984enum FixedNote {
3986 Sapling(ReceivedNote<u32, ::sapling::Note>),
3987 #[cfg(feature = "orchard")]
3988 Orchard(ReceivedNote<u32, OrchardNote>),
3989 #[cfg(feature = "orchard")]
3990 Ironwood(ReceivedNote<u32, OrchardNote>),
3991}
3992
3993impl FixedNote {
3994 fn new(pool: ShieldedPool, id: u32, value: Zatoshis, output_index: u16) -> Self {
3996 fn received<NoteT>(id: u32, output_index: u16, note: NoteT) -> ReceivedNote<u32, NoteT> {
3997 ReceivedNote::from_parts(
3998 id,
3999 TxId::from_bytes([0; 32]),
4000 output_index,
4001 note,
4002 Scope::External,
4003 Position::from(u64::from(output_index)),
4004 None,
4005 None,
4006 )
4007 }
4008 match pool {
4009 ShieldedPool::Sapling => {
4010 Self::Sapling(received(id, output_index, fixed_sapling_note(value)))
4011 }
4012 #[cfg(feature = "orchard")]
4013 ShieldedPool::Orchard => Self::Orchard(received(
4014 id,
4015 output_index,
4016 fixed_orchard_note(value, NoteVersion::V2),
4017 )),
4018 #[cfg(feature = "orchard")]
4019 ShieldedPool::Ironwood => Self::Ironwood(received(
4020 id,
4021 output_index,
4022 fixed_orchard_note(value, NoteVersion::V3),
4023 )),
4024 #[cfg(not(feature = "orchard"))]
4025 ShieldedPool::Orchard | ShieldedPool::Ironwood => {
4026 panic!("Orchard and Ironwood notes need the `orchard` feature")
4027 }
4028 }
4029 }
4030
4031 fn pool(&self) -> ShieldedPool {
4032 match self {
4033 Self::Sapling(_) => ShieldedPool::Sapling,
4034 #[cfg(feature = "orchard")]
4035 Self::Orchard(_) => ShieldedPool::Orchard,
4036 #[cfg(feature = "orchard")]
4037 Self::Ironwood(_) => ShieldedPool::Ironwood,
4038 }
4039 }
4040
4041 fn id(&self) -> u32 {
4042 match self {
4043 Self::Sapling(note) => *note.internal_note_id(),
4044 #[cfg(feature = "orchard")]
4045 Self::Orchard(note) | Self::Ironwood(note) => *note.internal_note_id(),
4046 }
4047 }
4048
4049 fn value(&self) -> Zatoshis {
4050 match self {
4051 Self::Sapling(note) => note.note_value(),
4052 #[cfg(feature = "orchard")]
4053 Self::Orchard(note) | Self::Ironwood(note) => note.note_value(),
4054 }
4055 .expect("the note was built from a valid value")
4056 }
4057}
4058
4059pub struct FixedNotes {
4069 notes: Vec<FixedNote>,
4070 sources_handling: FilterHandling,
4071 exclude_handling: FilterHandling,
4072 selection_limit: Option<usize>,
4073 selections: Cell<usize>,
4074}
4075
4076impl FixedNotes {
4077 pub fn new(notes: &[(ShieldedPool, u32, Zatoshis)]) -> Self {
4086 let notes: Vec<FixedNote> = notes
4087 .iter()
4088 .zip(0u16..)
4089 .map(|((pool, id, value), output_index)| {
4090 FixedNote::new(*pool, *id, *value, output_index)
4091 })
4092 .collect();
4093 notes
4094 .iter()
4095 .map(FixedNote::value)
4096 .sum::<Option<Zatoshis>>()
4097 .expect("the notes' total value fits the monetary range");
4098 Self {
4099 notes,
4100 sources_handling: FilterHandling::Honored,
4101 exclude_handling: FilterHandling::Honored,
4102 selection_limit: None,
4103 selections: Cell::new(0),
4104 }
4105 }
4106
4107 pub fn ignoring_sources(self) -> Self {
4111 Self {
4112 sources_handling: FilterHandling::Ignored,
4113 ..self
4114 }
4115 }
4116
4117 pub fn ignoring_exclude(self) -> Self {
4120 Self {
4121 exclude_handling: FilterHandling::Ignored,
4122 ..self
4123 }
4124 }
4125
4126 pub fn with_selection_limit(self, limit: usize) -> Self {
4130 Self {
4131 selection_limit: Some(limit),
4132 ..self
4133 }
4134 }
4135
4136 fn select(
4139 &self,
4140 sources: &[ShieldedPool],
4141 exclude: &[u32],
4142 target: Option<Zatoshis>,
4143 ) -> ReceivedNotes<u32> {
4144 let in_sources = |pool: ShieldedPool| match self.sources_handling {
4145 FilterHandling::Honored => sources.contains(&pool),
4146 FilterHandling::Ignored => true,
4147 };
4148 let excluded = |id: u32| match self.exclude_handling {
4149 FilterHandling::Honored => exclude.contains(&id),
4150 FilterHandling::Ignored => false,
4151 };
4152 let mut selected_value = Zatoshis::ZERO;
4153 let mut sapling = vec![];
4154 #[cfg(feature = "orchard")]
4155 let mut orchard = vec![];
4156 #[cfg(feature = "orchard")]
4157 let mut ironwood = vec![];
4158 for note in &self.notes {
4159 if target.is_some_and(|target| selected_value >= target) {
4160 break;
4161 }
4162 if !in_sources(note.pool()) || excluded(note.id()) {
4163 continue;
4164 }
4165 selected_value = (selected_value + note.value())
4166 .expect("a partial sum of a total in the monetary range is in it too");
4167 match note {
4168 FixedNote::Sapling(note) => sapling.push(note.clone()),
4169 #[cfg(feature = "orchard")]
4170 FixedNote::Orchard(note) => orchard.push(note.clone()),
4171 #[cfg(feature = "orchard")]
4172 FixedNote::Ironwood(note) => ironwood.push(note.clone()),
4173 }
4174 }
4175 ReceivedNotes::new(
4176 sapling,
4177 #[cfg(feature = "orchard")]
4178 orchard,
4179 #[cfg(feature = "orchard")]
4180 ironwood,
4181 )
4182 }
4183}
4184
4185impl InputSource for FixedNotes {
4186 type Error = FixedNotesError;
4187 type AccountId = u32;
4188 type NoteRef = u32;
4189
4190 fn get_spendable_note(
4191 &self,
4192 _txid: &TxId,
4193 _protocol: ShieldedPool,
4194 _index: u32,
4195 _target_height: TargetHeight,
4196 _lock_filter: LockFilter<'_>,
4197 ) -> Result<Option<ReceivedNote<Self::NoteRef, Note>>, Self::Error> {
4198 Err(FixedNotesError::Unsupported)
4199 }
4200
4201 fn anchor_computable(
4202 &self,
4203 _protocol: ShieldedPool,
4204 _height: BlockHeight,
4205 ) -> Result<bool, Self::Error> {
4206 Err(FixedNotesError::Unsupported)
4207 }
4208
4209 fn select_spendable_notes(
4210 &self,
4211 _account: Self::AccountId,
4212 target_value: TargetValue,
4213 sources: &[ShieldedPool],
4214 _target_height: TargetHeight,
4215 _confirmations_policy: ConfirmationsPolicy,
4216 exclude: &[Self::NoteRef],
4217 _lock_filter: LockFilter<'_>,
4218 ) -> Result<ReceivedNotes<Self::NoteRef>, Self::Error> {
4219 let selections = self.selections.get() + 1;
4220 if self.selection_limit.is_some_and(|limit| selections > limit) {
4221 return Err(FixedNotesError::SelectionLimitReached);
4222 }
4223 self.selections.set(selections);
4224 let target = match target_value {
4225 TargetValue::AtLeast(target) => Some(target),
4226 TargetValue::AllFunds(_) => None,
4227 };
4228 Ok(self.select(sources, exclude, target))
4229 }
4230
4231 fn select_unspent_notes(
4232 &self,
4233 _account: Self::AccountId,
4234 sources: &[ShieldedPool],
4235 _target_height: TargetHeight,
4236 exclude: &[Self::NoteRef],
4237 _lock_filter: LockFilter<'_>,
4238 ) -> Result<ReceivedNotes<Self::NoteRef>, Self::Error> {
4239 Ok(self.select(sources, exclude, None))
4240 }
4241
4242 fn get_change_receivers(
4243 &self,
4244 _account: Self::AccountId,
4245 ) -> Result<ChangeReceivers, Self::Error> {
4246 Ok(ChangeReceivers::new(
4247 true,
4248 true,
4249 #[cfg(feature = "transparent-inputs")]
4250 None,
4251 ))
4252 }
4253
4254 fn get_account_metadata(
4255 &self,
4256 _account: Self::AccountId,
4257 _selector: &NoteFilter,
4258 _target_height: TargetHeight,
4259 _exclude: &[Self::NoteRef],
4260 _lock_filter: LockFilter<'_>,
4261 ) -> Result<AccountMeta, Self::Error> {
4262 Err(FixedNotesError::Unsupported)
4263 }
4264}