zcash_client_backend/data_api/
testing.rs

1//! Utilities for testing wallets based upon the [`crate::data_api`] traits.
2
3use std::{
4    collections::{BTreeMap, HashMap},
5    convert::Infallible,
6    fmt,
7    hash::Hash,
8    num::NonZeroU32,
9};
10
11use assert_matches::assert_matches;
12use group::ff::Field;
13use incrementalmerkletree::{Marking, Retention};
14use nonempty::NonEmpty;
15use rand::{CryptoRng, Rng, RngCore, SeedableRng};
16use rand_chacha::ChaChaRng;
17use secrecy::{ExposeSecret, Secret, SecretVec};
18use shardtree::{error::ShardTreeError, store::memory::MemoryShardStore, ShardTree};
19use subtle::ConditionallySelectable;
20
21use ::sapling::{
22    note_encryption::{sapling_note_encryption, SaplingDomain},
23    util::generate_random_rseed,
24    zip32::DiversifiableFullViewingKey,
25};
26use zcash_address::ZcashAddress;
27use zcash_keys::{
28    address::{Address, UnifiedAddress},
29    keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey},
30};
31use zcash_note_encryption::Domain;
32use zcash_primitives::{
33    block::BlockHash,
34    transaction::{components::sapling::zip212_enforcement, fees::FeeRule, Transaction, TxId},
35};
36use zcash_proofs::prover::LocalTxProver;
37use zcash_protocol::{
38    consensus::{self, BlockHeight, Network, NetworkUpgrade, Parameters as _},
39    local_consensus::LocalNetwork,
40    memo::{Memo, MemoBytes},
41    value::{ZatBalance, Zatoshis},
42    ShieldedProtocol,
43};
44use zip32::{fingerprint::SeedFingerprint, DiversifierIndex};
45use zip321::Payment;
46
47use super::{
48    chain::{scan_cached_blocks, BlockSource, ChainState, CommitmentTreeRoot, ScanSummary},
49    error::Error,
50    scanning::ScanRange,
51    wallet::{
52        create_proposed_transactions,
53        input_selection::{GreedyInputSelector, InputSelector},
54        propose_standard_transfer_to_address, propose_transfer,
55    },
56    Account, AccountBalance, AccountBirthday, AccountMeta, AccountPurpose, AccountSource,
57    AddressInfo, BlockMetadata, DecryptedTransaction, InputSource, NoteFilter, NullifierQuery,
58    ScannedBlock, SeedRelevance, SentTransaction, SpendableNotes, TransactionDataRequest,
59    TransactionStatus, WalletCommitmentTrees, WalletRead, WalletSummary, WalletTest, WalletWrite,
60    SAPLING_SHARD_HEIGHT,
61};
62use crate::{
63    fees::{
64        standard::{self, SingleOutputChangeStrategy},
65        ChangeStrategy, DustOutputPolicy, StandardFeeRule,
66    },
67    proposal::Proposal,
68    proto::compact_formats::{
69        self, CompactBlock, CompactSaplingOutput, CompactSaplingSpend, CompactTx,
70    },
71    wallet::{Note, NoteId, OvkPolicy, ReceivedNote, WalletTransparentOutput},
72};
73
74#[cfg(feature = "transparent-inputs")]
75use {
76    super::wallet::input_selection::ShieldingSelector,
77    crate::wallet::TransparentAddressMetadata,
78    ::transparent::{address::TransparentAddress, keys::NonHardenedChildIndex},
79    std::ops::Range,
80    transparent::GapLimits,
81};
82
83#[cfg(feature = "orchard")]
84use {
85    super::ORCHARD_SHARD_HEIGHT, crate::proto::compact_formats::CompactOrchardAction,
86    ::orchard::tree::MerkleHashOrchard, group::ff::PrimeField, pasta_curves::pallas,
87};
88
89pub mod pool;
90pub mod sapling;
91
92#[cfg(feature = "orchard")]
93pub mod orchard;
94#[cfg(feature = "transparent-inputs")]
95pub mod transparent;
96
97/// Information about a transaction that the wallet is interested in.
98pub struct TransactionSummary<AccountId> {
99    account_id: AccountId,
100    txid: TxId,
101    expiry_height: Option<BlockHeight>,
102    mined_height: Option<BlockHeight>,
103    account_value_delta: ZatBalance,
104    total_spent: Zatoshis,
105    total_received: Zatoshis,
106    fee_paid: Option<Zatoshis>,
107    spent_note_count: usize,
108    has_change: bool,
109    sent_note_count: usize,
110    received_note_count: usize,
111    memo_count: usize,
112    expired_unmined: bool,
113    is_shielding: bool,
114}
115
116impl<AccountId> TransactionSummary<AccountId> {
117    /// Constructs a `TransactionSummary` from its parts.
118    ///
119    /// See the documentation for each getter method below to determine how each method
120    /// argument should be prepared.
121    #[allow(clippy::too_many_arguments)]
122    pub fn from_parts(
123        account_id: AccountId,
124        txid: TxId,
125        expiry_height: Option<BlockHeight>,
126        mined_height: Option<BlockHeight>,
127        account_value_delta: ZatBalance,
128        total_spent: Zatoshis,
129        total_received: Zatoshis,
130        fee_paid: Option<Zatoshis>,
131        spent_note_count: usize,
132        has_change: bool,
133        sent_note_count: usize,
134        received_note_count: usize,
135        memo_count: usize,
136        expired_unmined: bool,
137        is_shielding: bool,
138    ) -> Self {
139        Self {
140            account_id,
141            txid,
142            expiry_height,
143            mined_height,
144            account_value_delta,
145            total_spent,
146            total_received,
147            fee_paid,
148            spent_note_count,
149            has_change,
150            sent_note_count,
151            received_note_count,
152            memo_count,
153            expired_unmined,
154            is_shielding,
155        }
156    }
157
158    /// Returns the wallet-internal ID for the account that this transaction was received
159    /// by or sent from.
160    pub fn account_id(&self) -> &AccountId {
161        &self.account_id
162    }
163
164    /// Returns the transaction's ID.
165    pub fn txid(&self) -> TxId {
166        self.txid
167    }
168
169    /// Returns the expiry height of the transaction, if known.
170    ///
171    /// - `None` means that the expiry height is unknown.
172    /// - `Some(0)` means that the transaction does not expire.
173    pub fn expiry_height(&self) -> Option<BlockHeight> {
174        self.expiry_height
175    }
176
177    /// Returns the height of the mined block containing this transaction, or `None` if
178    /// the wallet has not yet observed the transaction to be mined.
179    pub fn mined_height(&self) -> Option<BlockHeight> {
180        self.mined_height
181    }
182
183    /// Returns the net change in balance that this transaction caused to the account.
184    ///
185    /// For example, an account-internal transaction (such as a shielding operation) would
186    /// show `-fee_paid` as the account value delta.
187    pub fn account_value_delta(&self) -> ZatBalance {
188        self.account_value_delta
189    }
190
191    /// Returns the total value of notes spent by the account in this transaction.
192    pub fn total_spent(&self) -> Zatoshis {
193        self.total_spent
194    }
195
196    /// Returns the total value of notes received by the account in this transaction.
197    pub fn total_received(&self) -> Zatoshis {
198        self.total_received
199    }
200
201    /// Returns the fee paid by this transaction, if known.
202    pub fn fee_paid(&self) -> Option<Zatoshis> {
203        self.fee_paid
204    }
205
206    /// Returns the number of notes spent by the account in this transaction.
207    pub fn spent_note_count(&self) -> usize {
208        self.spent_note_count
209    }
210
211    /// Returns `true` if the account received a change note as part of this transaction.
212    ///
213    /// This implies that the transaction was (at least in part) sent from the account.
214    pub fn has_change(&self) -> bool {
215        self.has_change
216    }
217
218    /// Returns the number of notes created in this transaction that were sent to a
219    /// wallet-external address.
220    pub fn sent_note_count(&self) -> usize {
221        self.sent_note_count
222    }
223
224    /// Returns the number of notes created in this transaction that were received by the
225    /// account.
226    pub fn received_note_count(&self) -> usize {
227        self.received_note_count
228    }
229
230    /// Returns `true` if, from the wallet's current view of the chain, this transaction
231    /// expired before it was mined.
232    pub fn expired_unmined(&self) -> bool {
233        self.expired_unmined
234    }
235
236    /// Returns the number of non-empty memos viewable by the account in this transaction.
237    pub fn memo_count(&self) -> usize {
238        self.memo_count
239    }
240
241    /// Returns `true` if this is detectably a shielding transaction.
242    ///
243    /// Specifically, `true` means that at a minimum:
244    /// - All of the wallet-spent and wallet-received notes are consistent with a
245    ///   shielding transaction.
246    /// - The transaction contains at least one wallet-spent output.
247    /// - The transaction contains at least one wallet-received note.
248    /// - We do not know about any external outputs of the transaction.
249    ///
250    /// There may be some shielding transactions for which this method returns `false`,
251    /// due to them not being detectable by the wallet as shielding transactions under the
252    /// above metrics.
253    pub fn is_shielding(&self) -> bool {
254        self.is_shielding
255    }
256}
257
258/// Metadata about a block generated by [`TestState`].
259#[derive(Clone, Debug)]
260pub struct CachedBlock {
261    chain_state: ChainState,
262    sapling_end_size: u32,
263    orchard_end_size: u32,
264}
265
266impl CachedBlock {
267    /// Produces metadata for a block "before shielded time", when the Sapling and Orchard
268    /// trees were (by definition) empty.
269    ///
270    /// `block_height` must be a height before Sapling activation (and therefore also
271    /// before NU5 activation).
272    pub fn none(block_height: BlockHeight) -> Self {
273        Self {
274            chain_state: ChainState::empty(block_height, BlockHash([0; 32])),
275            sapling_end_size: 0,
276            orchard_end_size: 0,
277        }
278    }
279
280    /// Produces metadata for a block as of the given chain state.
281    pub fn at(chain_state: ChainState, sapling_end_size: u32, orchard_end_size: u32) -> Self {
282        assert_eq!(
283            chain_state.final_sapling_tree().tree_size() as u32,
284            sapling_end_size
285        );
286        #[cfg(feature = "orchard")]
287        assert_eq!(
288            chain_state.final_orchard_tree().tree_size() as u32,
289            orchard_end_size
290        );
291
292        Self {
293            chain_state,
294            sapling_end_size,
295            orchard_end_size,
296        }
297    }
298
299    fn roll_forward(&self, cb: &CompactBlock) -> Self {
300        assert_eq!(self.chain_state.block_height() + 1, cb.height());
301
302        let sapling_final_tree = cb.vtx.iter().flat_map(|tx| tx.outputs.iter()).fold(
303            self.chain_state.final_sapling_tree().clone(),
304            |mut acc, c_out| {
305                acc.append(::sapling::Node::from_cmu(&c_out.cmu().unwrap()));
306                acc
307            },
308        );
309        let sapling_end_size = sapling_final_tree.tree_size() as u32;
310
311        #[cfg(feature = "orchard")]
312        let orchard_final_tree = cb.vtx.iter().flat_map(|tx| tx.actions.iter()).fold(
313            self.chain_state.final_orchard_tree().clone(),
314            |mut acc, c_act| {
315                acc.append(MerkleHashOrchard::from_cmx(&c_act.cmx().unwrap()));
316                acc
317            },
318        );
319        #[cfg(feature = "orchard")]
320        let orchard_end_size = orchard_final_tree.tree_size() as u32;
321        #[cfg(not(feature = "orchard"))]
322        let orchard_end_size = cb.vtx.iter().fold(self.orchard_end_size, |sz, tx| {
323            sz + (tx.actions.len() as u32)
324        });
325
326        Self {
327            chain_state: ChainState::new(
328                cb.height(),
329                cb.hash(),
330                sapling_final_tree,
331                #[cfg(feature = "orchard")]
332                orchard_final_tree,
333            ),
334            sapling_end_size,
335            orchard_end_size,
336        }
337    }
338
339    /// Returns the height of this block.
340    pub fn height(&self) -> BlockHeight {
341        self.chain_state.block_height()
342    }
343
344    /// Returns the size of the Sapling note commitment tree as of the end of this block.
345    pub fn sapling_end_size(&self) -> u32 {
346        self.sapling_end_size
347    }
348
349    /// Returns the size of the Orchard note commitment tree as of the end of this block.
350    pub fn orchard_end_size(&self) -> u32 {
351        self.orchard_end_size
352    }
353}
354
355/// The test account configured for a [`TestState`].
356///
357/// Create this by calling either [`TestBuilder::with_account_from_sapling_activation`] or
358/// [`TestBuilder::with_account_having_current_birthday`] while setting up a test, and
359/// then access it with [`TestState::test_account`].
360#[derive(Clone)]
361pub struct TestAccount<A> {
362    account: A,
363    usk: UnifiedSpendingKey,
364    birthday: AccountBirthday,
365}
366
367impl<A> TestAccount<A> {
368    /// Returns the underlying wallet account.
369    pub fn account(&self) -> &A {
370        &self.account
371    }
372
373    /// Returns the account's unified spending key.
374    pub fn usk(&self) -> &UnifiedSpendingKey {
375        &self.usk
376    }
377
378    /// Returns the birthday that was configured for the account.
379    pub fn birthday(&self) -> &AccountBirthday {
380        &self.birthday
381    }
382}
383
384impl<A: Account> Account for TestAccount<A> {
385    type AccountId = A::AccountId;
386
387    fn id(&self) -> Self::AccountId {
388        self.account.id()
389    }
390
391    fn name(&self) -> Option<&str> {
392        self.account.name()
393    }
394
395    fn source(&self) -> &AccountSource {
396        self.account.source()
397    }
398
399    fn ufvk(&self) -> Option<&zcash_keys::keys::UnifiedFullViewingKey> {
400        self.account.ufvk()
401    }
402
403    fn uivk(&self) -> zcash_keys::keys::UnifiedIncomingViewingKey {
404        self.account.uivk()
405    }
406}
407
408/// Trait method exposing the ability to reset the wallet within a test.
409// TODO: Does this need to exist separately from DataStoreFactory?
410pub trait Reset: WalletTest + Sized {
411    /// A handle that confers ownership of a specific wallet instance.
412    type Handle;
413
414    /// Replaces the wallet in `st` (via [`TestState::wallet_mut`]) with a new wallet
415    /// database.
416    ///
417    /// This does not recreate accounts. The resulting wallet in `st` has no test account.
418    ///
419    /// Returns the old wallet.
420    fn reset<C>(st: &mut TestState<C, Self, LocalNetwork>) -> Self::Handle;
421}
422
423/// The state for a `zcash_client_backend` test.
424pub struct TestState<Cache, DataStore: WalletTest, Network> {
425    cache: Cache,
426    cached_blocks: BTreeMap<BlockHeight, CachedBlock>,
427    latest_block_height: Option<BlockHeight>,
428    wallet_data: DataStore,
429    network: Network,
430    test_account: Option<(SecretVec<u8>, TestAccount<DataStore::Account>)>,
431    rng: ChaChaRng,
432}
433
434impl<Cache, DataStore: WalletTest, Network> TestState<Cache, DataStore, Network> {
435    /// Exposes an immutable reference to the test's `DataStore`.
436    pub fn wallet(&self) -> &DataStore {
437        &self.wallet_data
438    }
439
440    /// Exposes a mutable reference to the test's `DataStore`.
441    pub fn wallet_mut(&mut self) -> &mut DataStore {
442        &mut self.wallet_data
443    }
444
445    /// Exposes the test framework's source of randomness.
446    pub fn rng_mut(&mut self) -> &mut ChaChaRng {
447        &mut self.rng
448    }
449
450    /// Exposes the network in use.
451    pub fn network(&self) -> &Network {
452        &self.network
453    }
454}
455
456impl<Cache, DataStore: WalletTest, Network: consensus::Parameters>
457    TestState<Cache, DataStore, Network>
458{
459    /// Convenience method for obtaining the Sapling activation height for the network under test.
460    pub fn sapling_activation_height(&self) -> BlockHeight {
461        self.network
462            .activation_height(NetworkUpgrade::Sapling)
463            .expect("Sapling activation height must be known.")
464    }
465
466    /// Convenience method for obtaining the NU5 activation height for the network under test.
467    #[allow(dead_code)]
468    pub fn nu5_activation_height(&self) -> BlockHeight {
469        self.network
470            .activation_height(NetworkUpgrade::Nu5)
471            .expect("NU5 activation height must be known.")
472    }
473
474    /// Exposes the seed for the test wallet.
475    pub fn test_seed(&self) -> Option<&SecretVec<u8>> {
476        self.test_account.as_ref().map(|(seed, _)| seed)
477    }
478
479    /// Returns a reference to the test account, if one was configured.
480    pub fn test_account(&self) -> Option<&TestAccount<<DataStore as WalletRead>::Account>> {
481        self.test_account.as_ref().map(|(_, acct)| acct)
482    }
483
484    /// Returns the test account's Sapling DFVK, if one was configured.
485    pub fn test_account_sapling(&self) -> Option<&DiversifiableFullViewingKey> {
486        let (_, acct) = self.test_account.as_ref()?;
487        let ufvk = acct.ufvk()?;
488        ufvk.sapling()
489    }
490
491    /// Returns the test account's Orchard FVK, if one was configured.
492    #[cfg(feature = "orchard")]
493    pub fn test_account_orchard(&self) -> Option<&::orchard::keys::FullViewingKey> {
494        let (_, acct) = self.test_account.as_ref()?;
495        let ufvk = acct.ufvk()?;
496        ufvk.orchard()
497    }
498}
499
500impl<Cache: TestCache, DataStore, Network> TestState<Cache, DataStore, Network>
501where
502    Network: consensus::Parameters,
503    DataStore: WalletTest + WalletWrite,
504    <Cache::BlockSource as BlockSource>::Error: fmt::Debug,
505{
506    /// Exposes an immutable reference to the test's [`BlockSource`].
507    #[cfg(feature = "unstable")]
508    pub fn cache(&self) -> &Cache::BlockSource {
509        self.cache.block_source()
510    }
511
512    /// Returns the cached chain state corresponding to the latest block generated by this
513    /// `TestState`.
514    pub fn latest_cached_block(&self) -> Option<&CachedBlock> {
515        self.latest_block_height
516            .as_ref()
517            .and_then(|h| self.cached_blocks.get(h))
518    }
519
520    fn latest_cached_block_below_height(&self, height: BlockHeight) -> Option<&CachedBlock> {
521        self.cached_blocks.range(..height).last().map(|(_, b)| b)
522    }
523
524    fn cache_block(
525        &mut self,
526        prev_block: &CachedBlock,
527        compact_block: CompactBlock,
528    ) -> Cache::InsertResult {
529        self.cached_blocks.insert(
530            compact_block.height(),
531            prev_block.roll_forward(&compact_block),
532        );
533        self.cache.insert(&compact_block)
534    }
535
536    /// Creates a fake block at the expected next height containing a single output of the
537    /// given value, and inserts it into the cache.
538    pub fn generate_next_block<Fvk: TestFvk>(
539        &mut self,
540        fvk: &Fvk,
541        address_type: AddressType,
542        value: Zatoshis,
543    ) -> (BlockHeight, Cache::InsertResult, Fvk::Nullifier) {
544        let pre_activation_block = CachedBlock::none(self.sapling_activation_height() - 1);
545        let prior_cached_block = self.latest_cached_block().unwrap_or(&pre_activation_block);
546        let height = prior_cached_block.height() + 1;
547
548        let (res, nfs) = self.generate_block_at(
549            height,
550            prior_cached_block.chain_state.block_hash(),
551            &[FakeCompactOutput::new(fvk, address_type, value)],
552            prior_cached_block.sapling_end_size,
553            prior_cached_block.orchard_end_size,
554            false,
555        );
556
557        (height, res, nfs[0])
558    }
559
560    /// Creates a fake block at the expected next height containing multiple outputs
561    /// and inserts it into the cache.
562    #[allow(dead_code)]
563    pub fn generate_next_block_multi<Fvk: TestFvk>(
564        &mut self,
565        outputs: &[FakeCompactOutput<Fvk>],
566    ) -> (BlockHeight, Cache::InsertResult, Vec<Fvk::Nullifier>) {
567        let pre_activation_block = CachedBlock::none(self.sapling_activation_height() - 1);
568        let prior_cached_block = self.latest_cached_block().unwrap_or(&pre_activation_block);
569        let height = prior_cached_block.height() + 1;
570
571        let (res, nfs) = self.generate_block_at(
572            height,
573            prior_cached_block.chain_state.block_hash(),
574            outputs,
575            prior_cached_block.sapling_end_size,
576            prior_cached_block.orchard_end_size,
577            false,
578        );
579
580        (height, res, nfs)
581    }
582
583    /// Adds an empty block to the cache, advancing the simulated chain height.
584    #[allow(dead_code)] // used only for tests that are flagged off by default
585    pub fn generate_empty_block(&mut self) -> (BlockHeight, Cache::InsertResult) {
586        let new_hash = {
587            let mut hash = vec![0; 32];
588            self.rng.fill_bytes(&mut hash);
589            hash
590        };
591
592        let pre_activation_block = CachedBlock::none(self.sapling_activation_height() - 1);
593        let prior_cached_block = self
594            .latest_cached_block()
595            .unwrap_or(&pre_activation_block)
596            .clone();
597        let new_height = prior_cached_block.height() + 1;
598
599        let mut cb = CompactBlock {
600            hash: new_hash,
601            height: new_height.into(),
602            ..Default::default()
603        };
604        cb.prev_hash
605            .extend_from_slice(&prior_cached_block.chain_state.block_hash().0);
606
607        cb.chain_metadata = Some(compact_formats::ChainMetadata {
608            sapling_commitment_tree_size: prior_cached_block.sapling_end_size,
609            orchard_commitment_tree_size: prior_cached_block.orchard_end_size,
610        });
611
612        let res = self.cache_block(&prior_cached_block, cb);
613        self.latest_block_height = Some(new_height);
614
615        (new_height, res)
616    }
617
618    /// Creates a fake block with the given height and hash containing the requested outputs, and
619    /// inserts it into the cache.
620    ///
621    /// This generated block will be treated as the latest block, and subsequent calls to
622    /// [`Self::generate_next_block`] will build on it.
623    #[allow(clippy::too_many_arguments)]
624    pub fn generate_block_at<Fvk: TestFvk>(
625        &mut self,
626        height: BlockHeight,
627        prev_hash: BlockHash,
628        outputs: &[FakeCompactOutput<Fvk>],
629        initial_sapling_tree_size: u32,
630        initial_orchard_tree_size: u32,
631        allow_broken_hash_chain: bool,
632    ) -> (Cache::InsertResult, Vec<Fvk::Nullifier>) {
633        let mut prior_cached_block = self
634            .latest_cached_block_below_height(height)
635            .cloned()
636            .unwrap_or_else(|| CachedBlock::none(self.sapling_activation_height() - 1));
637        assert!(prior_cached_block.chain_state.block_height() < height);
638        assert!(prior_cached_block.sapling_end_size <= initial_sapling_tree_size);
639        assert!(prior_cached_block.orchard_end_size <= initial_orchard_tree_size);
640
641        // If the block height has increased or the Sapling and/or Orchard tree sizes have changed,
642        // we need to generate a new prior cached block that the block to be generated can
643        // successfully chain from, with the provided tree sizes.
644        if prior_cached_block.chain_state.block_height() == height - 1 {
645            if !allow_broken_hash_chain {
646                assert_eq!(prev_hash, prior_cached_block.chain_state.block_hash());
647            }
648        } else {
649            let final_sapling_tree =
650                (prior_cached_block.sapling_end_size..initial_sapling_tree_size).fold(
651                    prior_cached_block.chain_state.final_sapling_tree().clone(),
652                    |mut acc, _| {
653                        acc.append(::sapling::Node::from_scalar(bls12_381::Scalar::random(
654                            &mut self.rng,
655                        )));
656                        acc
657                    },
658                );
659
660            #[cfg(feature = "orchard")]
661            let final_orchard_tree =
662                (prior_cached_block.orchard_end_size..initial_orchard_tree_size).fold(
663                    prior_cached_block.chain_state.final_orchard_tree().clone(),
664                    |mut acc, _| {
665                        acc.append(MerkleHashOrchard::random(&mut self.rng));
666                        acc
667                    },
668                );
669
670            prior_cached_block = CachedBlock::at(
671                ChainState::new(
672                    height - 1,
673                    prev_hash,
674                    final_sapling_tree,
675                    #[cfg(feature = "orchard")]
676                    final_orchard_tree,
677                ),
678                initial_sapling_tree_size,
679                initial_orchard_tree_size,
680            );
681
682            self.cached_blocks
683                .insert(height - 1, prior_cached_block.clone());
684        }
685
686        let (cb, nfs) = fake_compact_block(
687            &self.network,
688            height,
689            prev_hash,
690            outputs,
691            initial_sapling_tree_size,
692            initial_orchard_tree_size,
693            &mut self.rng,
694        );
695        assert_eq!(cb.height(), height);
696
697        let res = self.cache_block(&prior_cached_block, cb);
698        self.latest_block_height = Some(height);
699
700        (res, nfs)
701    }
702
703    /// Creates a fake block at the expected next height spending the given note, and
704    /// inserts it into the cache.
705    pub fn generate_next_block_spending<Fvk: TestFvk>(
706        &mut self,
707        fvk: &Fvk,
708        note: (Fvk::Nullifier, Zatoshis),
709        to: impl Into<Address>,
710        value: Zatoshis,
711    ) -> (BlockHeight, Cache::InsertResult) {
712        let prior_cached_block = self
713            .latest_cached_block()
714            .cloned()
715            .unwrap_or_else(|| CachedBlock::none(self.sapling_activation_height() - 1));
716        let height = prior_cached_block.height() + 1;
717
718        let cb = fake_compact_block_spending(
719            &self.network,
720            height,
721            prior_cached_block.chain_state.block_hash(),
722            note,
723            fvk,
724            to.into(),
725            value,
726            prior_cached_block.sapling_end_size,
727            prior_cached_block.orchard_end_size,
728            &mut self.rng,
729        );
730        assert_eq!(cb.height(), height);
731
732        let res = self.cache_block(&prior_cached_block, cb);
733        self.latest_block_height = Some(height);
734
735        (height, res)
736    }
737
738    /// Creates a fake block at the expected next height containing only the wallet
739    /// transaction with the given txid, and inserts it into the cache.
740    ///
741    /// This generated block will be treated as the latest block, and subsequent calls to
742    /// [`Self::generate_next_block`] (or similar) will build on it.
743    pub fn generate_next_block_including(
744        &mut self,
745        txid: TxId,
746    ) -> (BlockHeight, Cache::InsertResult) {
747        let tx = self
748            .wallet()
749            .get_transaction(txid)
750            .unwrap()
751            .expect("TxId should exist in the wallet");
752
753        // Index 0 is by definition a coinbase transaction, and the wallet doesn't
754        // construct coinbase transactions. So we pretend here that the block has a
755        // coinbase transaction that does not have shielded coinbase outputs.
756        self.generate_next_block_from_tx(1, &tx)
757    }
758
759    /// Creates a fake block at the expected next height containing only the given
760    /// transaction, and inserts it into the cache.
761    ///
762    /// This generated block will be treated as the latest block, and subsequent calls to
763    /// [`Self::generate_next_block`] will build on it.
764    pub fn generate_next_block_from_tx(
765        &mut self,
766        tx_index: usize,
767        tx: &Transaction,
768    ) -> (BlockHeight, Cache::InsertResult) {
769        let prior_cached_block = self
770            .latest_cached_block()
771            .cloned()
772            .unwrap_or_else(|| CachedBlock::none(self.sapling_activation_height() - 1));
773        let height = prior_cached_block.height() + 1;
774
775        let cb = fake_compact_block_from_tx(
776            height,
777            prior_cached_block.chain_state.block_hash(),
778            tx_index,
779            tx,
780            prior_cached_block.sapling_end_size,
781            prior_cached_block.orchard_end_size,
782            &mut self.rng,
783        );
784        assert_eq!(cb.height(), height);
785
786        let res = self.cache_block(&prior_cached_block, cb);
787        self.latest_block_height = Some(height);
788
789        (height, res)
790    }
791
792    /// Truncates the test wallet and block cache to the specified height, discarding all data from
793    /// blocks at heights greater than the specified height, excluding transaction data that may
794    /// not be recoverable from the chain.
795    pub fn truncate_to_height(&mut self, height: BlockHeight) {
796        self.wallet_mut().truncate_to_height(height).unwrap();
797        self.cache.truncate_to_height(height);
798        self.cached_blocks.split_off(&(height + 1));
799        self.latest_block_height = Some(height);
800    }
801
802    /// Truncates the test wallet to the specified height, and resets the cache's latest block
803    /// height but does not truncate the block cache. This is useful for circumstances when you
804    /// want to re-scan a set of cached blocks.
805    pub fn truncate_to_height_retaining_cache(&mut self, height: BlockHeight) {
806        self.wallet_mut().truncate_to_height(height).unwrap();
807        self.latest_block_height = Some(height);
808    }
809}
810
811impl<Cache, DbT, ParamsT> TestState<Cache, DbT, ParamsT>
812where
813    Cache: TestCache,
814    <Cache::BlockSource as BlockSource>::Error: fmt::Debug,
815    ParamsT: consensus::Parameters + Send + 'static,
816    DbT: InputSource + WalletTest + WalletWrite + WalletCommitmentTrees,
817    <DbT as WalletRead>::AccountId:
818        std::fmt::Debug + ConditionallySelectable + Default + Send + 'static,
819{
820    /// Invokes [`scan_cached_blocks`] with the given arguments, expecting success.
821    pub fn scan_cached_blocks(&mut self, from_height: BlockHeight, limit: usize) -> ScanSummary {
822        let result = self.try_scan_cached_blocks(from_height, limit);
823        assert_matches!(result, Ok(_));
824        result.unwrap()
825    }
826
827    /// Invokes [`scan_cached_blocks`] with the given arguments.
828    pub fn try_scan_cached_blocks(
829        &mut self,
830        from_height: BlockHeight,
831        limit: usize,
832    ) -> Result<
833        ScanSummary,
834        super::chain::error::Error<
835            <DbT as WalletRead>::Error,
836            <Cache::BlockSource as BlockSource>::Error,
837        >,
838    > {
839        let prior_cached_block = self
840            .latest_cached_block_below_height(from_height)
841            .cloned()
842            .unwrap_or_else(|| CachedBlock::none(from_height - 1));
843
844        let result = scan_cached_blocks(
845            &self.network,
846            self.cache.block_source(),
847            &mut self.wallet_data,
848            from_height,
849            &prior_cached_block.chain_state,
850            limit,
851        );
852        result
853    }
854
855    /// Insert shard roots for both trees.
856    pub fn put_subtree_roots(
857        &mut self,
858        sapling_start_index: u64,
859        sapling_roots: &[CommitmentTreeRoot<::sapling::Node>],
860        #[cfg(feature = "orchard")] orchard_start_index: u64,
861        #[cfg(feature = "orchard")] orchard_roots: &[CommitmentTreeRoot<MerkleHashOrchard>],
862    ) -> Result<(), ShardTreeError<<DbT as WalletCommitmentTrees>::Error>> {
863        self.wallet_mut()
864            .put_sapling_subtree_roots(sapling_start_index, sapling_roots)?;
865
866        #[cfg(feature = "orchard")]
867        self.wallet_mut()
868            .put_orchard_subtree_roots(orchard_start_index, orchard_roots)?;
869
870        Ok(())
871    }
872}
873
874impl<Cache, DbT, ParamsT, AccountIdT, ErrT> TestState<Cache, DbT, ParamsT>
875where
876    ParamsT: consensus::Parameters + Send + 'static,
877    AccountIdT: std::fmt::Debug + std::cmp::Eq + std::hash::Hash,
878    ErrT: std::fmt::Debug,
879    DbT: InputSource<AccountId = AccountIdT, Error = ErrT>
880        + WalletTest
881        + WalletWrite<AccountId = AccountIdT, Error = ErrT>
882        + WalletCommitmentTrees,
883    <DbT as WalletRead>::AccountId: ConditionallySelectable + Default + Send + 'static,
884{
885    // Creates a transaction that sends the specified value from the given account to
886    // the provided recipient address, using a greedy input selector and the default
887    // mutli-output change strategy.
888    pub fn create_standard_transaction(
889        &mut self,
890        from_account: &TestAccount<DbT::Account>,
891        to: ZcashAddress,
892        value: Zatoshis,
893    ) -> Result<
894        NonEmpty<TxId>,
895        super::wallet::TransferErrT<
896            DbT,
897            GreedyInputSelector<DbT>,
898            standard::MultiOutputChangeStrategy<DbT>,
899        >,
900    > {
901        let input_selector = GreedyInputSelector::new();
902
903        #[cfg(not(feature = "orchard"))]
904        let fallback_change_pool = ShieldedProtocol::Sapling;
905        #[cfg(feature = "orchard")]
906        let fallback_change_pool = ShieldedProtocol::Orchard;
907
908        let change_strategy = standard::SingleOutputChangeStrategy::new(
909            StandardFeeRule::Zip317,
910            None,
911            fallback_change_pool,
912            DustOutputPolicy::default(),
913        );
914
915        let request =
916            zip321::TransactionRequest::new(vec![Payment::without_memo(to, value)]).unwrap();
917
918        self.spend(
919            &input_selector,
920            &change_strategy,
921            from_account.usk(),
922            request,
923            OvkPolicy::Sender,
924            NonZeroU32::MIN,
925        )
926    }
927
928    /// Prepares and executes the given [`zip321::TransactionRequest`] in a single step.
929    #[allow(clippy::type_complexity)]
930    pub fn spend<InputsT, ChangeT>(
931        &mut self,
932        input_selector: &InputsT,
933        change_strategy: &ChangeT,
934        usk: &UnifiedSpendingKey,
935        request: zip321::TransactionRequest,
936        ovk_policy: OvkPolicy,
937        min_confirmations: NonZeroU32,
938    ) -> Result<NonEmpty<TxId>, super::wallet::TransferErrT<DbT, InputsT, ChangeT>>
939    where
940        InputsT: InputSelector<InputSource = DbT>,
941        ChangeT: ChangeStrategy<MetaSource = DbT>,
942    {
943        let prover = LocalTxProver::bundled();
944        let network = self.network().clone();
945
946        let account = self
947            .wallet()
948            .get_account_for_ufvk(&usk.to_unified_full_viewing_key())
949            .map_err(Error::DataSource)?
950            .ok_or(Error::KeyNotRecognized)?;
951
952        let proposal = propose_transfer(
953            self.wallet_mut(),
954            &network,
955            account.id(),
956            input_selector,
957            change_strategy,
958            request,
959            min_confirmations,
960        )?;
961
962        create_proposed_transactions(
963            self.wallet_mut(),
964            &network,
965            &prover,
966            &prover,
967            usk,
968            ovk_policy,
969            &proposal,
970        )
971    }
972
973    /// Invokes [`propose_transfer`] with the given arguments.
974    #[allow(clippy::type_complexity)]
975    pub fn propose_transfer<InputsT, ChangeT>(
976        &mut self,
977        spend_from_account: <DbT as InputSource>::AccountId,
978        input_selector: &InputsT,
979        change_strategy: &ChangeT,
980        request: zip321::TransactionRequest,
981        min_confirmations: NonZeroU32,
982    ) -> Result<
983        Proposal<ChangeT::FeeRule, <DbT as InputSource>::NoteRef>,
984        super::wallet::ProposeTransferErrT<DbT, Infallible, InputsT, ChangeT>,
985    >
986    where
987        InputsT: InputSelector<InputSource = DbT>,
988        ChangeT: ChangeStrategy<MetaSource = DbT>,
989    {
990        let network = self.network().clone();
991        propose_transfer::<_, _, _, _, Infallible>(
992            self.wallet_mut(),
993            &network,
994            spend_from_account,
995            input_selector,
996            change_strategy,
997            request,
998            min_confirmations,
999        )
1000    }
1001
1002    /// Invokes [`propose_standard_transfer_to_address`] with the given arguments.
1003    #[allow(clippy::type_complexity)]
1004    #[allow(clippy::too_many_arguments)]
1005    pub fn propose_standard_transfer<CommitmentTreeErrT>(
1006        &mut self,
1007        spend_from_account: <DbT as InputSource>::AccountId,
1008        fee_rule: StandardFeeRule,
1009        min_confirmations: NonZeroU32,
1010        to: &Address,
1011        amount: Zatoshis,
1012        memo: Option<MemoBytes>,
1013        change_memo: Option<MemoBytes>,
1014        fallback_change_pool: ShieldedProtocol,
1015    ) -> Result<
1016        Proposal<StandardFeeRule, <DbT as InputSource>::NoteRef>,
1017        super::wallet::ProposeTransferErrT<
1018            DbT,
1019            CommitmentTreeErrT,
1020            GreedyInputSelector<DbT>,
1021            SingleOutputChangeStrategy<DbT>,
1022        >,
1023    > {
1024        let network = self.network().clone();
1025        let result = propose_standard_transfer_to_address::<_, _, CommitmentTreeErrT>(
1026            self.wallet_mut(),
1027            &network,
1028            fee_rule,
1029            spend_from_account,
1030            min_confirmations,
1031            to,
1032            amount,
1033            memo,
1034            change_memo,
1035            fallback_change_pool,
1036        );
1037
1038        if let Ok(proposal) = &result {
1039            check_proposal_serialization_roundtrip(self.wallet(), proposal);
1040        }
1041
1042        result
1043    }
1044
1045    /// Invokes [`propose_shielding`] with the given arguments.
1046    ///
1047    /// [`propose_shielding`]: crate::data_api::wallet::propose_shielding
1048    #[cfg(feature = "transparent-inputs")]
1049    #[allow(clippy::type_complexity)]
1050    #[allow(dead_code)]
1051    pub fn propose_shielding<InputsT, ChangeT>(
1052        &mut self,
1053        input_selector: &InputsT,
1054        change_strategy: &ChangeT,
1055        shielding_threshold: Zatoshis,
1056        from_addrs: &[TransparentAddress],
1057        to_account: <InputsT::InputSource as InputSource>::AccountId,
1058        min_confirmations: u32,
1059    ) -> Result<
1060        Proposal<ChangeT::FeeRule, Infallible>,
1061        super::wallet::ProposeShieldingErrT<DbT, Infallible, InputsT, ChangeT>,
1062    >
1063    where
1064        InputsT: ShieldingSelector<InputSource = DbT>,
1065        ChangeT: ChangeStrategy<MetaSource = DbT>,
1066    {
1067        use super::wallet::propose_shielding;
1068
1069        let network = self.network().clone();
1070        propose_shielding::<_, _, _, _, Infallible>(
1071            self.wallet_mut(),
1072            &network,
1073            input_selector,
1074            change_strategy,
1075            shielding_threshold,
1076            from_addrs,
1077            to_account,
1078            min_confirmations,
1079        )
1080    }
1081
1082    /// Invokes [`create_proposed_transactions`] with the given arguments.
1083    #[allow(clippy::type_complexity)]
1084    pub fn create_proposed_transactions<InputsErrT, FeeRuleT, ChangeErrT>(
1085        &mut self,
1086        usk: &UnifiedSpendingKey,
1087        ovk_policy: OvkPolicy,
1088        proposal: &Proposal<FeeRuleT, <DbT as InputSource>::NoteRef>,
1089    ) -> Result<
1090        NonEmpty<TxId>,
1091        super::wallet::CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, DbT::NoteRef>,
1092    >
1093    where
1094        FeeRuleT: FeeRule,
1095    {
1096        let prover = LocalTxProver::bundled();
1097        let network = self.network().clone();
1098        create_proposed_transactions(
1099            self.wallet_mut(),
1100            &network,
1101            &prover,
1102            &prover,
1103            usk,
1104            ovk_policy,
1105            proposal,
1106        )
1107    }
1108
1109    /// Invokes [`create_pczt_from_proposal`] with the given arguments.
1110    ///
1111    /// [`create_pczt_from_proposal`]: super::wallet::create_pczt_from_proposal
1112    #[cfg(feature = "pczt")]
1113    #[allow(clippy::type_complexity)]
1114    pub fn create_pczt_from_proposal<InputsErrT, FeeRuleT, ChangeErrT>(
1115        &mut self,
1116        spend_from_account: <DbT as InputSource>::AccountId,
1117        ovk_policy: OvkPolicy,
1118        proposal: &Proposal<FeeRuleT, <DbT as InputSource>::NoteRef>,
1119    ) -> Result<
1120        pczt::Pczt,
1121        super::wallet::CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, DbT::NoteRef>,
1122    >
1123    where
1124        <DbT as WalletRead>::AccountId: serde::Serialize,
1125        FeeRuleT: FeeRule,
1126    {
1127        use super::wallet::create_pczt_from_proposal;
1128
1129        let network = self.network().clone();
1130
1131        create_pczt_from_proposal(
1132            self.wallet_mut(),
1133            &network,
1134            spend_from_account,
1135            ovk_policy,
1136            proposal,
1137        )
1138    }
1139
1140    /// Invokes [`extract_and_store_transaction_from_pczt`] with the given arguments.
1141    ///
1142    /// [`extract_and_store_transaction_from_pczt`]: super::wallet::extract_and_store_transaction_from_pczt
1143    #[cfg(feature = "pczt")]
1144    #[allow(clippy::type_complexity)]
1145    pub fn extract_and_store_transaction_from_pczt(
1146        &mut self,
1147        pczt: pczt::Pczt,
1148    ) -> Result<TxId, super::wallet::ExtractErrT<DbT, DbT::NoteRef>>
1149    where
1150        <DbT as WalletRead>::AccountId: serde::de::DeserializeOwned,
1151    {
1152        use super::wallet::extract_and_store_transaction_from_pczt;
1153
1154        let prover = LocalTxProver::bundled();
1155        let (spend_vk, output_vk) = prover.verifying_keys();
1156
1157        extract_and_store_transaction_from_pczt(
1158            self.wallet_mut(),
1159            pczt,
1160            Some((&spend_vk, &output_vk)),
1161            None,
1162        )
1163    }
1164
1165    /// Invokes [`shield_transparent_funds`] with the given arguments.
1166    ///
1167    /// [`shield_transparent_funds`]: crate::data_api::wallet::shield_transparent_funds
1168    #[cfg(feature = "transparent-inputs")]
1169    #[allow(clippy::type_complexity)]
1170    #[allow(clippy::too_many_arguments)]
1171    pub fn shield_transparent_funds<InputsT, ChangeT>(
1172        &mut self,
1173        input_selector: &InputsT,
1174        change_strategy: &ChangeT,
1175        shielding_threshold: Zatoshis,
1176        usk: &UnifiedSpendingKey,
1177        from_addrs: &[TransparentAddress],
1178        to_account: <DbT as InputSource>::AccountId,
1179        min_confirmations: u32,
1180    ) -> Result<NonEmpty<TxId>, super::wallet::ShieldErrT<DbT, InputsT, ChangeT>>
1181    where
1182        InputsT: ShieldingSelector<InputSource = DbT>,
1183        ChangeT: ChangeStrategy<MetaSource = DbT>,
1184    {
1185        use crate::data_api::wallet::shield_transparent_funds;
1186
1187        let prover = LocalTxProver::bundled();
1188        let network = self.network().clone();
1189        shield_transparent_funds(
1190            self.wallet_mut(),
1191            &network,
1192            &prover,
1193            &prover,
1194            input_selector,
1195            change_strategy,
1196            shielding_threshold,
1197            usk,
1198            from_addrs,
1199            to_account,
1200            min_confirmations,
1201        )
1202    }
1203
1204    fn with_account_balance<T, F: FnOnce(&AccountBalance) -> T>(
1205        &self,
1206        account: AccountIdT,
1207        min_confirmations: u32,
1208        f: F,
1209    ) -> T {
1210        let binding = self
1211            .wallet()
1212            .get_wallet_summary(min_confirmations)
1213            .unwrap()
1214            .unwrap();
1215        f(binding.account_balances().get(&account).unwrap())
1216    }
1217
1218    /// Returns the total balance in the given account at this point in the test.
1219    pub fn get_total_balance(&self, account: AccountIdT) -> Zatoshis {
1220        self.with_account_balance(account, 0, |balance| balance.total())
1221    }
1222
1223    /// Returns the balance in the given account that is spendable with the given number
1224    /// of confirmations at this point in the test.
1225    pub fn get_spendable_balance(&self, account: AccountIdT, min_confirmations: u32) -> Zatoshis {
1226        self.with_account_balance(account, min_confirmations, |balance| {
1227            balance.spendable_value()
1228        })
1229    }
1230
1231    /// Returns the balance in the given account that is detected but not yet spendable
1232    /// with the given number of confirmations at this point in the test.
1233    pub fn get_pending_shielded_balance(
1234        &self,
1235        account: AccountIdT,
1236        min_confirmations: u32,
1237    ) -> Zatoshis {
1238        self.with_account_balance(account, min_confirmations, |balance| {
1239            balance.value_pending_spendability() + balance.change_pending_confirmation()
1240        })
1241        .unwrap()
1242    }
1243
1244    /// Returns the amount of change in the given account that is not yet spendable with
1245    /// the given number of confirmations at this point in the test.
1246    #[allow(dead_code)]
1247    pub fn get_pending_change(&self, account: AccountIdT, min_confirmations: u32) -> Zatoshis {
1248        self.with_account_balance(account, min_confirmations, |balance| {
1249            balance.change_pending_confirmation()
1250        })
1251    }
1252
1253    /// Returns a summary of the wallet at this point in the test.
1254    pub fn get_wallet_summary(&self, min_confirmations: u32) -> Option<WalletSummary<AccountIdT>> {
1255        self.wallet().get_wallet_summary(min_confirmations).unwrap()
1256    }
1257}
1258
1259impl<Cache, DbT, ParamsT, AccountIdT, ErrT> TestState<Cache, DbT, ParamsT>
1260where
1261    ParamsT: consensus::Parameters + Send + 'static,
1262    AccountIdT: std::cmp::Eq + std::hash::Hash,
1263    ErrT: std::fmt::Debug,
1264    DbT: InputSource<AccountId = AccountIdT, Error = ErrT>
1265        + WalletTest
1266        + WalletWrite<AccountId = AccountIdT, Error = ErrT>
1267        + WalletCommitmentTrees,
1268    <DbT as WalletRead>::AccountId: ConditionallySelectable + Default + Send + 'static,
1269{
1270    /// Returns a transaction from the history.
1271    #[allow(dead_code)]
1272    pub fn get_tx_from_history(
1273        &self,
1274        txid: TxId,
1275    ) -> Result<Option<TransactionSummary<AccountIdT>>, ErrT> {
1276        let history = self.wallet().get_tx_history()?;
1277        Ok(history.into_iter().find(|tx| tx.txid() == txid))
1278    }
1279}
1280
1281impl<Cache, DbT: WalletRead + Reset> TestState<Cache, DbT, LocalNetwork> {
1282    /// Resets the wallet using a new wallet database but with the same cache of blocks,
1283    /// and returns the old wallet database file.
1284    ///
1285    /// This does not recreate accounts, nor does it rescan the cached blocks.
1286    /// The resulting wallet has no test account.
1287    /// Before using any `generate_*` method on the reset state, call `reset_latest_cached_block()`.
1288    pub fn reset(&mut self) -> DbT::Handle {
1289        self.latest_block_height = None;
1290        self.test_account = None;
1291        DbT::reset(self)
1292    }
1293
1294    //    /// Reset the latest cached block to the most recent one in the cache database.
1295    //    #[allow(dead_code)]
1296    //    pub fn reset_latest_cached_block(&mut self) {
1297    //        self.cache
1298    //            .block_source()
1299    //            .with_blocks::<_, Infallible>(None, None, |block: CompactBlock| {
1300    //                let chain_metadata = block.chain_metadata.unwrap();
1301    //                self.latest_cached_block = Some(CachedBlock::at(
1302    //                    BlockHash::from_slice(block.hash.as_slice()),
1303    //                    BlockHeight::from_u32(block.height.try_into().unwrap()),
1304    //                    chain_metadata.sapling_commitment_tree_size,
1305    //                    chain_metadata.orchard_commitment_tree_size,
1306    //                ));
1307    //                Ok(())
1308    //            })
1309    //            .unwrap();
1310    //    }
1311}
1312
1313pub fn single_output_change_strategy<DbT: InputSource>(
1314    fee_rule: StandardFeeRule,
1315    change_memo: Option<&str>,
1316    fallback_change_pool: ShieldedProtocol,
1317) -> standard::SingleOutputChangeStrategy<DbT> {
1318    let change_memo = change_memo.map(|m| MemoBytes::from(m.parse::<Memo>().unwrap()));
1319    standard::SingleOutputChangeStrategy::new(
1320        fee_rule,
1321        change_memo,
1322        fallback_change_pool,
1323        DustOutputPolicy::default(),
1324    )
1325}
1326
1327// Checks that a protobuf proposal serialized from the provided proposal value correctly parses to
1328// the same proposal value.
1329fn check_proposal_serialization_roundtrip<DbT: InputSource>(
1330    wallet_data: &DbT,
1331    proposal: &Proposal<StandardFeeRule, DbT::NoteRef>,
1332) {
1333    let proposal_proto = crate::proto::proposal::Proposal::from_standard_proposal(proposal);
1334    let deserialized_proposal = proposal_proto.try_into_standard_proposal(wallet_data);
1335    assert_matches!(deserialized_proposal, Ok(r) if &r == proposal);
1336}
1337
1338/// The initial chain state for a test.
1339///
1340/// This is returned from the closure passed to [`TestBuilder::with_initial_chain_state`]
1341/// to configure the test state with a starting chain position, to which subsequent test
1342/// activity is applied.
1343pub struct InitialChainState {
1344    /// Information about the chain's state as of the chain tip.
1345    pub chain_state: ChainState,
1346    /// Roots of the completed Sapling subtrees as of this chain state.
1347    pub prior_sapling_roots: Vec<CommitmentTreeRoot<::sapling::Node>>,
1348    /// Roots of the completed Orchard subtrees as of this chain state.
1349    #[cfg(feature = "orchard")]
1350    pub prior_orchard_roots: Vec<CommitmentTreeRoot<MerkleHashOrchard>>,
1351}
1352
1353/// Trait representing the ability to construct a new data store for use in a test.
1354pub trait DataStoreFactory {
1355    type Error: core::fmt::Debug;
1356    type AccountId: std::fmt::Debug + ConditionallySelectable + Default + Hash + Eq + Send + 'static;
1357    type Account: Account<AccountId = Self::AccountId> + Clone;
1358    type DsError: core::fmt::Debug;
1359    type DataStore: InputSource<AccountId = Self::AccountId, Error = Self::DsError>
1360        + WalletRead<AccountId = Self::AccountId, Account = Self::Account, Error = Self::DsError>
1361        + WalletTest
1362        + WalletWrite
1363        + WalletCommitmentTrees;
1364
1365    /// Constructs a new data store.
1366    fn new_data_store(
1367        &self,
1368        network: LocalNetwork,
1369        #[cfg(feature = "transparent-inputs")] gap_limits: GapLimits,
1370    ) -> Result<Self::DataStore, Self::Error>;
1371}
1372
1373/// A [`TestState`] builder, that configures the environment for a test.
1374pub struct TestBuilder<Cache, DataStoreFactory> {
1375    rng: ChaChaRng,
1376    network: LocalNetwork,
1377    cache: Cache,
1378    ds_factory: DataStoreFactory,
1379    initial_chain_state: Option<InitialChainState>,
1380    account_birthday: Option<AccountBirthday>,
1381    account_index: Option<zip32::AccountId>,
1382    #[cfg(feature = "transparent-inputs")]
1383    gap_limits: GapLimits,
1384}
1385
1386impl TestBuilder<(), ()> {
1387    /// The default network used by [`TestBuilder::new`].
1388    ///
1389    /// This is a fake network where Sapling through NU5 activate at the same height. We
1390    /// pick height 100,000 to be large enough to handle any hard-coded test offsets.
1391    pub const DEFAULT_NETWORK: LocalNetwork = LocalNetwork {
1392        overwinter: Some(BlockHeight::from_u32(1)),
1393        sapling: Some(BlockHeight::from_u32(100_000)),
1394        blossom: Some(BlockHeight::from_u32(100_000)),
1395        heartwood: Some(BlockHeight::from_u32(100_000)),
1396        canopy: Some(BlockHeight::from_u32(100_000)),
1397        nu5: Some(BlockHeight::from_u32(100_000)),
1398        nu6: None,
1399        #[cfg(zcash_unstable = "nu7")]
1400        nu7: None,
1401        #[cfg(zcash_unstable = "zfuture")]
1402        z_future: None,
1403    };
1404
1405    /// Constructs a new test environment builder.
1406    pub fn new() -> Self {
1407        TestBuilder {
1408            rng: ChaChaRng::seed_from_u64(0),
1409            network: Self::DEFAULT_NETWORK,
1410            cache: (),
1411            ds_factory: (),
1412            initial_chain_state: None,
1413            account_birthday: None,
1414            account_index: None,
1415            #[cfg(feature = "transparent-inputs")]
1416            gap_limits: GapLimits::new(10, 5, 5),
1417        }
1418    }
1419}
1420
1421impl Default for TestBuilder<(), ()> {
1422    fn default() -> Self {
1423        Self::new()
1424    }
1425}
1426
1427impl<A> TestBuilder<(), A> {
1428    /// Adds a block cache to the test environment.
1429    pub fn with_block_cache<C: TestCache>(self, cache: C) -> TestBuilder<C, A> {
1430        TestBuilder {
1431            rng: self.rng,
1432            network: self.network,
1433            cache,
1434            ds_factory: self.ds_factory,
1435            initial_chain_state: self.initial_chain_state,
1436            account_birthday: self.account_birthday,
1437            account_index: self.account_index,
1438            #[cfg(feature = "transparent-inputs")]
1439            gap_limits: self.gap_limits,
1440        }
1441    }
1442}
1443
1444impl<A> TestBuilder<A, ()> {
1445    /// Adds a wallet data store to the test environment.
1446    pub fn with_data_store_factory<DsFactory>(
1447        self,
1448        ds_factory: DsFactory,
1449    ) -> TestBuilder<A, DsFactory> {
1450        TestBuilder {
1451            rng: self.rng,
1452            network: self.network,
1453            cache: self.cache,
1454            ds_factory,
1455            initial_chain_state: self.initial_chain_state,
1456            account_birthday: self.account_birthday,
1457            account_index: self.account_index,
1458            #[cfg(feature = "transparent-inputs")]
1459            gap_limits: self.gap_limits,
1460        }
1461    }
1462}
1463
1464impl<A, B> TestBuilder<A, B> {
1465    #[cfg(feature = "transparent-inputs")]
1466    pub fn with_gap_limits(self, gap_limits: GapLimits) -> TestBuilder<A, B> {
1467        TestBuilder {
1468            rng: self.rng,
1469            network: self.network,
1470            cache: self.cache,
1471            ds_factory: self.ds_factory,
1472            initial_chain_state: self.initial_chain_state,
1473            account_birthday: self.account_birthday,
1474            account_index: self.account_index,
1475            gap_limits,
1476        }
1477    }
1478}
1479
1480impl<Cache, DsFactory> TestBuilder<Cache, DsFactory> {
1481    /// Configures the test to start with the given initial chain state.
1482    ///
1483    /// # Panics
1484    ///
1485    /// - Must not be called twice.
1486    /// - Must be called before [`Self::with_account_from_sapling_activation`] or
1487    ///   [`Self::with_account_having_current_birthday`].
1488    ///
1489    /// # Examples
1490    ///
1491    /// ```
1492    /// use std::num::NonZeroU8;
1493    ///
1494    /// use incrementalmerkletree::frontier::Frontier;
1495    /// use zcash_primitives::{block::BlockHash, consensus::Parameters};
1496    /// use zcash_protocol::consensus::NetworkUpgrade;
1497    /// use zcash_client_backend::data_api::{
1498    ///     chain::{ChainState, CommitmentTreeRoot},
1499    ///     testing::{InitialChainState, TestBuilder},
1500    /// };
1501    ///
1502    /// // For this test, we'll start inserting leaf notes 5 notes after the end of the
1503    /// // third subtree, with a gap of 10 blocks. After `scan_cached_blocks`, the scan
1504    /// // queue should have a requested scan range of 300..310 with `FoundNote` priority,
1505    /// // 310..320 with `Scanned` priority. We set both Sapling and Orchard to the same
1506    /// // initial tree size for simplicity.
1507    /// let prior_block_hash = BlockHash([0; 32]);
1508    /// let initial_sapling_tree_size: u32 = (0x1 << 16) * 3 + 5;
1509    /// let initial_orchard_tree_size: u32 = (0x1 << 16) * 3 + 5;
1510    /// let initial_height_offset = 310;
1511    ///
1512    /// let mut st = TestBuilder::new()
1513    ///     .with_initial_chain_state(|rng, network| {
1514    ///         // For simplicity, assume Sapling and NU5 activated at the same height.
1515    ///         let sapling_activation_height =
1516    ///             network.activation_height(NetworkUpgrade::Sapling).unwrap();
1517    ///
1518    ///         // Construct a fake chain state for the end of block 300
1519    ///         let (prior_sapling_roots, sapling_initial_tree) =
1520    ///             Frontier::random_with_prior_subtree_roots(
1521    ///                 rng,
1522    ///                 initial_sapling_tree_size.into(),
1523    ///                 NonZeroU8::new(16).unwrap(),
1524    ///             );
1525    ///         let prior_sapling_roots = prior_sapling_roots
1526    ///             .into_iter()
1527    ///             .zip(1u32..)
1528    ///             .map(|(root, i)| {
1529    ///                 CommitmentTreeRoot::from_parts(sapling_activation_height + (100 * i), root)
1530    ///             })
1531    ///             .collect::<Vec<_>>();
1532    ///
1533    ///         #[cfg(feature = "orchard")]
1534    ///         let (prior_orchard_roots, orchard_initial_tree) =
1535    ///             Frontier::random_with_prior_subtree_roots(
1536    ///                 rng,
1537    ///                 initial_orchard_tree_size.into(),
1538    ///                 NonZeroU8::new(16).unwrap(),
1539    ///             );
1540    ///         #[cfg(feature = "orchard")]
1541    ///         let prior_orchard_roots = prior_orchard_roots
1542    ///             .into_iter()
1543    ///             .zip(1u32..)
1544    ///             .map(|(root, i)| {
1545    ///                 CommitmentTreeRoot::from_parts(sapling_activation_height + (100 * i), root)
1546    ///             })
1547    ///             .collect::<Vec<_>>();
1548    ///
1549    ///         InitialChainState {
1550    ///             chain_state: ChainState::new(
1551    ///                 sapling_activation_height + initial_height_offset - 1,
1552    ///                 prior_block_hash,
1553    ///                 sapling_initial_tree,
1554    ///                 #[cfg(feature = "orchard")]
1555    ///                 orchard_initial_tree,
1556    ///             ),
1557    ///             prior_sapling_roots,
1558    ///             #[cfg(feature = "orchard")]
1559    ///             prior_orchard_roots,
1560    ///         }
1561    ///     });
1562    /// ```
1563    pub fn with_initial_chain_state(
1564        mut self,
1565        chain_state: impl FnOnce(&mut ChaChaRng, &LocalNetwork) -> InitialChainState,
1566    ) -> Self {
1567        assert!(self.initial_chain_state.is_none());
1568        assert!(self.account_birthday.is_none());
1569        self.initial_chain_state = Some(chain_state(&mut self.rng, &self.network));
1570        self
1571    }
1572
1573    /// Configures the environment with a [`TestAccount`] that has a birthday at Sapling
1574    /// activation.
1575    ///
1576    /// # Panics
1577    ///
1578    /// - Must not be called twice.
1579    /// - Do not call both [`Self::with_account_having_current_birthday`] and this method.
1580    pub fn with_account_from_sapling_activation(mut self, prev_hash: BlockHash) -> Self {
1581        assert!(self.account_birthday.is_none());
1582        self.account_birthday = Some(AccountBirthday::from_parts(
1583            ChainState::empty(
1584                self.network
1585                    .activation_height(NetworkUpgrade::Sapling)
1586                    .unwrap()
1587                    - 1,
1588                prev_hash,
1589            ),
1590            None,
1591        ));
1592        self
1593    }
1594
1595    /// Configures the environment with a [`TestAccount`] that has a birthday one block
1596    /// after the initial chain state.
1597    ///
1598    /// # Panics
1599    ///
1600    /// - Must not be called twice.
1601    /// - Must call [`Self::with_initial_chain_state`] before calling this method.
1602    /// - Do not call both [`Self::with_account_from_sapling_activation`] and this method.
1603    pub fn with_account_having_current_birthday(mut self) -> Self {
1604        assert!(self.account_birthday.is_none());
1605        assert!(self.initial_chain_state.is_some());
1606        self.account_birthday = Some(AccountBirthday::from_parts(
1607            self.initial_chain_state
1608                .as_ref()
1609                .unwrap()
1610                .chain_state
1611                .clone(),
1612            None,
1613        ));
1614        self
1615    }
1616
1617    /// Sets the account index for the test account.
1618    ///
1619    /// Does nothing unless either [`Self::with_account_from_sapling_activation`] or
1620    /// [`Self::with_account_having_current_birthday`] is also called.
1621    ///
1622    /// # Panics
1623    ///
1624    /// - Must not be called twice.
1625    pub fn set_account_index(mut self, index: zip32::AccountId) -> Self {
1626        assert!(self.account_index.is_none());
1627        self.account_index = Some(index);
1628        self
1629    }
1630}
1631
1632impl<Cache, DsFactory: DataStoreFactory> TestBuilder<Cache, DsFactory> {
1633    /// Builds the state for this test.
1634    pub fn build(self) -> TestState<Cache, DsFactory::DataStore, LocalNetwork> {
1635        let mut cached_blocks = BTreeMap::new();
1636        let mut wallet_data = self
1637            .ds_factory
1638            .new_data_store(
1639                self.network,
1640                #[cfg(feature = "transparent-inputs")]
1641                self.gap_limits,
1642            )
1643            .unwrap();
1644
1645        if let Some(initial_state) = &self.initial_chain_state {
1646            wallet_data
1647                .put_sapling_subtree_roots(0, &initial_state.prior_sapling_roots)
1648                .unwrap();
1649            wallet_data
1650                .with_sapling_tree_mut(|t| {
1651                    t.insert_frontier(
1652                        initial_state.chain_state.final_sapling_tree().clone(),
1653                        Retention::Checkpoint {
1654                            id: initial_state.chain_state.block_height(),
1655                            marking: Marking::Reference,
1656                        },
1657                    )
1658                })
1659                .unwrap();
1660
1661            #[cfg(feature = "orchard")]
1662            {
1663                wallet_data
1664                    .put_orchard_subtree_roots(0, &initial_state.prior_orchard_roots)
1665                    .unwrap();
1666                wallet_data
1667                    .with_orchard_tree_mut(|t| {
1668                        t.insert_frontier(
1669                            initial_state.chain_state.final_orchard_tree().clone(),
1670                            Retention::Checkpoint {
1671                                id: initial_state.chain_state.block_height(),
1672                                marking: Marking::Reference,
1673                            },
1674                        )
1675                    })
1676                    .unwrap();
1677            }
1678
1679            let final_sapling_tree_size =
1680                initial_state.chain_state.final_sapling_tree().tree_size() as u32;
1681            let _final_orchard_tree_size = 0;
1682            #[cfg(feature = "orchard")]
1683            let _final_orchard_tree_size =
1684                initial_state.chain_state.final_orchard_tree().tree_size() as u32;
1685
1686            cached_blocks.insert(
1687                initial_state.chain_state.block_height(),
1688                CachedBlock {
1689                    chain_state: initial_state.chain_state.clone(),
1690                    sapling_end_size: final_sapling_tree_size,
1691                    orchard_end_size: _final_orchard_tree_size,
1692                },
1693            );
1694        };
1695
1696        let test_account = self.account_birthday.map(|birthday| {
1697            let seed = Secret::new(vec![0u8; 32]);
1698            let (account, usk) = match self.account_index {
1699                Some(index) => wallet_data
1700                    .import_account_hd("", &seed, index, &birthday, None)
1701                    .unwrap(),
1702                None => {
1703                    let result = wallet_data
1704                        .create_account("", &seed, &birthday, None)
1705                        .unwrap();
1706                    (
1707                        wallet_data.get_account(result.0).unwrap().unwrap(),
1708                        result.1,
1709                    )
1710                }
1711            };
1712            (
1713                seed,
1714                TestAccount {
1715                    account,
1716                    usk,
1717                    birthday,
1718                },
1719            )
1720        });
1721
1722        TestState {
1723            cache: self.cache,
1724            cached_blocks,
1725            latest_block_height: self
1726                .initial_chain_state
1727                .map(|s| s.chain_state.block_height()),
1728            wallet_data,
1729            network: self.network,
1730            test_account,
1731            rng: self.rng,
1732        }
1733    }
1734}
1735
1736/// Trait used by tests that require a full viewing key.
1737pub trait TestFvk: Clone {
1738    /// The type of nullifier corresponding to the kind of note that this full viewing key
1739    /// can detect (and that its corresponding spending key can spend).
1740    type Nullifier: Copy;
1741
1742    /// Returns the Sapling outgoing viewing key corresponding to this full viewing key,
1743    /// if any.
1744    fn sapling_ovk(&self) -> Option<::sapling::keys::OutgoingViewingKey>;
1745
1746    /// Returns the Orchard outgoing viewing key corresponding to this full viewing key,
1747    /// if any.
1748    #[cfg(feature = "orchard")]
1749    fn orchard_ovk(&self, scope: zip32::Scope) -> Option<::orchard::keys::OutgoingViewingKey>;
1750
1751    /// Adds a single spend to the given [`CompactTx`] of a note previously received by
1752    /// this full viewing key.
1753    fn add_spend<R: RngCore + CryptoRng>(
1754        &self,
1755        ctx: &mut CompactTx,
1756        nf: Self::Nullifier,
1757        rng: &mut R,
1758    );
1759
1760    /// Adds a single output to the given [`CompactTx`] that will be received by this full
1761    /// viewing key.
1762    ///
1763    /// `req` allows configuring how the full viewing key will detect the output.
1764    #[allow(clippy::too_many_arguments)]
1765    fn add_output<P: consensus::Parameters, R: RngCore + CryptoRng>(
1766        &self,
1767        ctx: &mut CompactTx,
1768        params: &P,
1769        height: BlockHeight,
1770        req: AddressType,
1771        value: Zatoshis,
1772        initial_sapling_tree_size: u32,
1773        // we don't require an initial Orchard tree size because we don't need it to compute
1774        // the nullifier.
1775        rng: &mut R,
1776    ) -> Self::Nullifier;
1777
1778    /// Adds both a spend and an output to the given [`CompactTx`].
1779    ///
1780    /// - If this is a Sapling full viewing key, the transaction will gain both a Spend
1781    ///   and an Output.
1782    /// - If this is an Orchard full viewing key, the transaction will gain an Action.
1783    ///
1784    /// `req` allows configuring how the full viewing key will detect the output.
1785    #[allow(clippy::too_many_arguments)]
1786    fn add_logical_action<P: consensus::Parameters, R: RngCore + CryptoRng>(
1787        &self,
1788        ctx: &mut CompactTx,
1789        params: &P,
1790        height: BlockHeight,
1791        nf: Self::Nullifier,
1792        req: AddressType,
1793        value: Zatoshis,
1794        initial_sapling_tree_size: u32,
1795        // we don't require an initial Orchard tree size because we don't need it to compute
1796        // the nullifier.
1797        rng: &mut R,
1798    ) -> Self::Nullifier;
1799}
1800
1801impl<A: TestFvk> TestFvk for &A {
1802    type Nullifier = A::Nullifier;
1803
1804    fn sapling_ovk(&self) -> Option<::sapling::keys::OutgoingViewingKey> {
1805        (*self).sapling_ovk()
1806    }
1807
1808    #[cfg(feature = "orchard")]
1809    fn orchard_ovk(&self, scope: zip32::Scope) -> Option<::orchard::keys::OutgoingViewingKey> {
1810        (*self).orchard_ovk(scope)
1811    }
1812
1813    fn add_spend<R: RngCore + CryptoRng>(
1814        &self,
1815        ctx: &mut CompactTx,
1816        nf: Self::Nullifier,
1817        rng: &mut R,
1818    ) {
1819        (*self).add_spend(ctx, nf, rng)
1820    }
1821
1822    fn add_output<P: consensus::Parameters, R: RngCore + CryptoRng>(
1823        &self,
1824        ctx: &mut CompactTx,
1825        params: &P,
1826        height: BlockHeight,
1827        req: AddressType,
1828        value: Zatoshis,
1829        initial_sapling_tree_size: u32,
1830        // we don't require an initial Orchard tree size because we don't need it to compute
1831        // the nullifier.
1832        rng: &mut R,
1833    ) -> Self::Nullifier {
1834        (*self).add_output(
1835            ctx,
1836            params,
1837            height,
1838            req,
1839            value,
1840            initial_sapling_tree_size,
1841            rng,
1842        )
1843    }
1844
1845    fn add_logical_action<P: consensus::Parameters, R: RngCore + CryptoRng>(
1846        &self,
1847        ctx: &mut CompactTx,
1848        params: &P,
1849        height: BlockHeight,
1850        nf: Self::Nullifier,
1851        req: AddressType,
1852        value: Zatoshis,
1853        initial_sapling_tree_size: u32,
1854        // we don't require an initial Orchard tree size because we don't need it to compute
1855        // the nullifier.
1856        rng: &mut R,
1857    ) -> Self::Nullifier {
1858        (*self).add_logical_action(
1859            ctx,
1860            params,
1861            height,
1862            nf,
1863            req,
1864            value,
1865            initial_sapling_tree_size,
1866            rng,
1867        )
1868    }
1869}
1870
1871impl TestFvk for DiversifiableFullViewingKey {
1872    type Nullifier = ::sapling::Nullifier;
1873
1874    fn sapling_ovk(&self) -> Option<::sapling::keys::OutgoingViewingKey> {
1875        Some(self.fvk().ovk)
1876    }
1877
1878    #[cfg(feature = "orchard")]
1879    fn orchard_ovk(&self, _: zip32::Scope) -> Option<::orchard::keys::OutgoingViewingKey> {
1880        None
1881    }
1882
1883    fn add_spend<R: RngCore + CryptoRng>(
1884        &self,
1885        ctx: &mut CompactTx,
1886        nf: Self::Nullifier,
1887        _: &mut R,
1888    ) {
1889        let cspend = CompactSaplingSpend { nf: nf.to_vec() };
1890        ctx.spends.push(cspend);
1891    }
1892
1893    fn add_output<P: consensus::Parameters, R: RngCore + CryptoRng>(
1894        &self,
1895        ctx: &mut CompactTx,
1896        params: &P,
1897        height: BlockHeight,
1898        req: AddressType,
1899        value: Zatoshis,
1900        initial_sapling_tree_size: u32,
1901        rng: &mut R,
1902    ) -> Self::Nullifier {
1903        let recipient = match req {
1904            AddressType::DefaultExternal => self.default_address().1,
1905            AddressType::DiversifiedExternal(idx) => self.find_address(idx).unwrap().1,
1906            AddressType::Internal => self.change_address().1,
1907        };
1908
1909        let position = initial_sapling_tree_size + ctx.outputs.len() as u32;
1910
1911        let (cout, note) =
1912            compact_sapling_output(params, height, recipient, value, self.sapling_ovk(), rng);
1913        ctx.outputs.push(cout);
1914
1915        note.nf(&self.fvk().vk.nk, position as u64)
1916    }
1917
1918    #[allow(clippy::too_many_arguments)]
1919    fn add_logical_action<P: consensus::Parameters, R: RngCore + CryptoRng>(
1920        &self,
1921        ctx: &mut CompactTx,
1922        params: &P,
1923        height: BlockHeight,
1924        nf: Self::Nullifier,
1925        req: AddressType,
1926        value: Zatoshis,
1927        initial_sapling_tree_size: u32,
1928        rng: &mut R,
1929    ) -> Self::Nullifier {
1930        self.add_spend(ctx, nf, rng);
1931        self.add_output(
1932            ctx,
1933            params,
1934            height,
1935            req,
1936            value,
1937            initial_sapling_tree_size,
1938            rng,
1939        )
1940    }
1941}
1942
1943#[cfg(feature = "orchard")]
1944impl TestFvk for ::orchard::keys::FullViewingKey {
1945    type Nullifier = ::orchard::note::Nullifier;
1946
1947    fn sapling_ovk(&self) -> Option<::sapling::keys::OutgoingViewingKey> {
1948        None
1949    }
1950
1951    fn orchard_ovk(&self, scope: zip32::Scope) -> Option<::orchard::keys::OutgoingViewingKey> {
1952        Some(self.to_ovk(scope))
1953    }
1954
1955    fn add_spend<R: RngCore + CryptoRng>(
1956        &self,
1957        ctx: &mut CompactTx,
1958        revealed_spent_note_nullifier: Self::Nullifier,
1959        rng: &mut R,
1960    ) {
1961        // Generate a dummy recipient.
1962        let recipient = loop {
1963            let mut bytes = [0; 32];
1964            rng.fill_bytes(&mut bytes);
1965            let sk = ::orchard::keys::SpendingKey::from_bytes(bytes);
1966            if sk.is_some().into() {
1967                break ::orchard::keys::FullViewingKey::from(&sk.unwrap())
1968                    .address_at(0u32, zip32::Scope::External);
1969            }
1970        };
1971
1972        let (cact, _) = compact_orchard_action(
1973            revealed_spent_note_nullifier,
1974            recipient,
1975            Zatoshis::ZERO,
1976            self.orchard_ovk(zip32::Scope::Internal),
1977            rng,
1978        );
1979        ctx.actions.push(cact);
1980    }
1981
1982    fn add_output<P: consensus::Parameters, R: RngCore + CryptoRng>(
1983        &self,
1984        ctx: &mut CompactTx,
1985        _: &P,
1986        _: BlockHeight,
1987        req: AddressType,
1988        value: Zatoshis,
1989        _: u32, // the position is not required for computing the Orchard nullifier
1990        mut rng: &mut R,
1991    ) -> Self::Nullifier {
1992        // Generate a dummy nullifier for the spend
1993        let revealed_spent_note_nullifier =
1994            ::orchard::note::Nullifier::from_bytes(&pallas::Base::random(&mut rng).to_repr())
1995                .unwrap();
1996
1997        let (j, scope) = match req {
1998            AddressType::DefaultExternal => (0u32.into(), zip32::Scope::External),
1999            AddressType::DiversifiedExternal(idx) => (idx, zip32::Scope::External),
2000            AddressType::Internal => (0u32.into(), zip32::Scope::Internal),
2001        };
2002
2003        let (cact, note) = compact_orchard_action(
2004            revealed_spent_note_nullifier,
2005            self.address_at(j, scope),
2006            value,
2007            self.orchard_ovk(scope),
2008            rng,
2009        );
2010        ctx.actions.push(cact);
2011
2012        note.nullifier(self)
2013    }
2014
2015    // Override so we can merge the spend and output into a single action.
2016    fn add_logical_action<P: consensus::Parameters, R: RngCore + CryptoRng>(
2017        &self,
2018        ctx: &mut CompactTx,
2019        _: &P,
2020        _: BlockHeight,
2021        revealed_spent_note_nullifier: Self::Nullifier,
2022        address_type: AddressType,
2023        value: Zatoshis,
2024        _: u32, // the position is not required for computing the Orchard nullifier
2025        rng: &mut R,
2026    ) -> Self::Nullifier {
2027        let (j, scope) = match address_type {
2028            AddressType::DefaultExternal => (0u32.into(), zip32::Scope::External),
2029            AddressType::DiversifiedExternal(idx) => (idx, zip32::Scope::External),
2030            AddressType::Internal => (0u32.into(), zip32::Scope::Internal),
2031        };
2032
2033        let (cact, note) = compact_orchard_action(
2034            revealed_spent_note_nullifier,
2035            self.address_at(j, scope),
2036            value,
2037            self.orchard_ovk(scope),
2038            rng,
2039        );
2040        ctx.actions.push(cact);
2041
2042        // Return the nullifier of the newly created output note
2043        note.nullifier(self)
2044    }
2045}
2046
2047/// Configures how a [`TestFvk`] receives a particular output.
2048///
2049/// Used with [`TestFvk::add_output`] and [`TestFvk::add_logical_action`].
2050#[derive(Clone, Copy)]
2051pub enum AddressType {
2052    /// The output will be sent to the default address of the full viewing key.
2053    DefaultExternal,
2054    /// The output will be sent to the specified diversified address of the full viewing
2055    /// key.
2056    #[allow(dead_code)]
2057    DiversifiedExternal(DiversifierIndex),
2058    /// The output will be sent to the internal receiver of the full viewing key.
2059    ///
2060    /// Such outputs are treated as "wallet-internal". A "recipient address" is **NEVER**
2061    /// exposed to users.
2062    Internal,
2063}
2064
2065/// Creates a `CompactSaplingOutput` at the given height paying the given recipient.
2066///
2067/// Returns the `CompactSaplingOutput` and the new note.
2068fn compact_sapling_output<P: consensus::Parameters, R: RngCore + CryptoRng>(
2069    params: &P,
2070    height: BlockHeight,
2071    recipient: ::sapling::PaymentAddress,
2072    value: Zatoshis,
2073    ovk: Option<::sapling::keys::OutgoingViewingKey>,
2074    rng: &mut R,
2075) -> (CompactSaplingOutput, ::sapling::Note) {
2076    let rseed = generate_random_rseed(zip212_enforcement(params, height), rng);
2077    let note = ::sapling::Note::from_parts(
2078        recipient,
2079        ::sapling::value::NoteValue::from_raw(value.into_u64()),
2080        rseed,
2081    );
2082    let encryptor =
2083        sapling_note_encryption(ovk, note.clone(), MemoBytes::empty().into_bytes(), rng);
2084    let cmu = note.cmu().to_bytes().to_vec();
2085    let ephemeral_key = SaplingDomain::epk_bytes(encryptor.epk()).0.to_vec();
2086    let enc_ciphertext = encryptor.encrypt_note_plaintext();
2087
2088    (
2089        CompactSaplingOutput {
2090            cmu,
2091            ephemeral_key,
2092            ciphertext: enc_ciphertext[..52].to_vec(),
2093        },
2094        note,
2095    )
2096}
2097
2098/// Creates a `CompactOrchardAction` at the given height paying the given recipient.
2099///
2100/// Returns the `CompactOrchardAction` and the new note.
2101#[cfg(feature = "orchard")]
2102fn compact_orchard_action<R: RngCore + CryptoRng>(
2103    nf_old: ::orchard::note::Nullifier,
2104    recipient: ::orchard::Address,
2105    value: Zatoshis,
2106    ovk: Option<::orchard::keys::OutgoingViewingKey>,
2107    rng: &mut R,
2108) -> (CompactOrchardAction, ::orchard::Note) {
2109    use zcash_note_encryption::ShieldedOutput;
2110
2111    let (compact_action, note) = ::orchard::note_encryption::testing::fake_compact_action(
2112        rng,
2113        nf_old,
2114        recipient,
2115        ::orchard::value::NoteValue::from_raw(value.into_u64()),
2116        ovk,
2117    );
2118
2119    (
2120        CompactOrchardAction {
2121            nullifier: compact_action.nullifier().to_bytes().to_vec(),
2122            cmx: compact_action.cmx().to_bytes().to_vec(),
2123            ephemeral_key: compact_action.ephemeral_key().0.to_vec(),
2124            ciphertext: compact_action.enc_ciphertext()[..52].to_vec(),
2125        },
2126        note,
2127    )
2128}
2129
2130/// Creates a fake `CompactTx` with a random transaction ID and no spends or outputs.
2131fn fake_compact_tx<R: RngCore + CryptoRng>(rng: &mut R) -> CompactTx {
2132    let mut ctx = CompactTx::default();
2133    let mut txid = vec![0; 32];
2134    rng.fill_bytes(&mut txid);
2135    ctx.hash = txid;
2136
2137    ctx
2138}
2139
2140/// A fake output of a [`CompactTx`].
2141///
2142/// Used with the following block generators:
2143/// - [`TestState::generate_next_block_multi`]
2144/// - [`TestState::generate_block_at`]
2145#[derive(Clone)]
2146pub struct FakeCompactOutput<Fvk> {
2147    fvk: Fvk,
2148    address_type: AddressType,
2149    value: Zatoshis,
2150}
2151
2152impl<Fvk> FakeCompactOutput<Fvk> {
2153    /// Constructs a new fake output with the given properties.
2154    pub fn new(fvk: Fvk, address_type: AddressType, value: Zatoshis) -> Self {
2155        Self {
2156            fvk,
2157            address_type,
2158            value,
2159        }
2160    }
2161
2162    /// Constructs a new random fake external output to the given FVK with a value in the range
2163    /// 10000..1000000 ZAT.
2164    pub fn random<R: RngCore>(rng: &mut R, fvk: Fvk) -> Self {
2165        Self {
2166            fvk,
2167            address_type: AddressType::DefaultExternal,
2168            value: Zatoshis::const_from_u64(rng.gen_range(10000..1000000)),
2169        }
2170    }
2171}
2172
2173/// Create a fake CompactBlock at the given height, containing the specified fake compact outputs.
2174///
2175/// Returns the newly created compact block, along with the nullifier for each note created in that
2176/// block.
2177#[allow(clippy::too_many_arguments)]
2178fn fake_compact_block<P: consensus::Parameters, Fvk: TestFvk>(
2179    params: &P,
2180    height: BlockHeight,
2181    prev_hash: BlockHash,
2182    outputs: &[FakeCompactOutput<Fvk>],
2183    initial_sapling_tree_size: u32,
2184    initial_orchard_tree_size: u32,
2185    mut rng: impl RngCore + CryptoRng,
2186) -> (CompactBlock, Vec<Fvk::Nullifier>) {
2187    // Create a fake CompactBlock containing the note
2188    let mut ctx = fake_compact_tx(&mut rng);
2189    let mut nfs = vec![];
2190    for output in outputs {
2191        let nf = output.fvk.add_output(
2192            &mut ctx,
2193            params,
2194            height,
2195            output.address_type,
2196            output.value,
2197            initial_sapling_tree_size,
2198            &mut rng,
2199        );
2200        nfs.push(nf);
2201    }
2202
2203    let cb = fake_compact_block_from_compact_tx(
2204        ctx,
2205        height,
2206        prev_hash,
2207        initial_sapling_tree_size,
2208        initial_orchard_tree_size,
2209        rng,
2210    );
2211    (cb, nfs)
2212}
2213
2214/// Create a fake CompactBlock at the given height containing only the given transaction.
2215fn fake_compact_block_from_tx(
2216    height: BlockHeight,
2217    prev_hash: BlockHash,
2218    tx_index: usize,
2219    tx: &Transaction,
2220    initial_sapling_tree_size: u32,
2221    initial_orchard_tree_size: u32,
2222    rng: impl RngCore,
2223) -> CompactBlock {
2224    // Create a fake CompactTx containing the transaction.
2225    let mut ctx = CompactTx {
2226        index: tx_index as u64,
2227        hash: tx.txid().as_ref().to_vec(),
2228        ..Default::default()
2229    };
2230
2231    if let Some(bundle) = tx.sapling_bundle() {
2232        for spend in bundle.shielded_spends() {
2233            ctx.spends.push(spend.into());
2234        }
2235        for output in bundle.shielded_outputs() {
2236            ctx.outputs.push(output.into());
2237        }
2238    }
2239
2240    #[cfg(feature = "orchard")]
2241    if let Some(bundle) = tx.orchard_bundle() {
2242        for action in bundle.actions() {
2243            ctx.actions.push(action.into());
2244        }
2245    }
2246
2247    fake_compact_block_from_compact_tx(
2248        ctx,
2249        height,
2250        prev_hash,
2251        initial_sapling_tree_size,
2252        initial_orchard_tree_size,
2253        rng,
2254    )
2255}
2256
2257/// Create a fake CompactBlock at the given height, spending a single note from the
2258/// given address.
2259#[allow(clippy::too_many_arguments)]
2260fn fake_compact_block_spending<P: consensus::Parameters, Fvk: TestFvk>(
2261    params: &P,
2262    height: BlockHeight,
2263    prev_hash: BlockHash,
2264    (nf, in_value): (Fvk::Nullifier, Zatoshis),
2265    fvk: &Fvk,
2266    to: Address,
2267    value: Zatoshis,
2268    initial_sapling_tree_size: u32,
2269    initial_orchard_tree_size: u32,
2270    mut rng: impl RngCore + CryptoRng,
2271) -> CompactBlock {
2272    let mut ctx = fake_compact_tx(&mut rng);
2273
2274    // Create a fake spend and a fake Note for the change
2275    fvk.add_logical_action(
2276        &mut ctx,
2277        params,
2278        height,
2279        nf,
2280        AddressType::Internal,
2281        (in_value - value).unwrap(),
2282        initial_sapling_tree_size,
2283        &mut rng,
2284    );
2285
2286    // Create a fake Note for the payment
2287    match to {
2288        Address::Sapling(recipient) => ctx.outputs.push(
2289            compact_sapling_output(
2290                params,
2291                height,
2292                recipient,
2293                value,
2294                fvk.sapling_ovk(),
2295                &mut rng,
2296            )
2297            .0,
2298        ),
2299        Address::Transparent(_) | Address::Tex(_) => {
2300            panic!("transparent addresses not supported in compact blocks")
2301        }
2302        Address::Unified(ua) => {
2303            // This is annoying to implement, because the protocol-aware UA type has no
2304            // concept of ZIP 316 preference order.
2305            let mut done = false;
2306
2307            #[cfg(feature = "orchard")]
2308            if let Some(recipient) = ua.orchard() {
2309                // Generate a dummy nullifier
2310                let nullifier = ::orchard::note::Nullifier::from_bytes(
2311                    &pallas::Base::random(&mut rng).to_repr(),
2312                )
2313                .unwrap();
2314
2315                ctx.actions.push(
2316                    compact_orchard_action(
2317                        nullifier,
2318                        *recipient,
2319                        value,
2320                        fvk.orchard_ovk(zip32::Scope::External),
2321                        &mut rng,
2322                    )
2323                    .0,
2324                );
2325                done = true;
2326            }
2327
2328            if !done {
2329                if let Some(recipient) = ua.sapling() {
2330                    ctx.outputs.push(
2331                        compact_sapling_output(
2332                            params,
2333                            height,
2334                            *recipient,
2335                            value,
2336                            fvk.sapling_ovk(),
2337                            &mut rng,
2338                        )
2339                        .0,
2340                    );
2341                    done = true;
2342                }
2343            }
2344            if !done {
2345                panic!("No supported shielded receiver to send funds to");
2346            }
2347        }
2348    }
2349
2350    fake_compact_block_from_compact_tx(
2351        ctx,
2352        height,
2353        prev_hash,
2354        initial_sapling_tree_size,
2355        initial_orchard_tree_size,
2356        rng,
2357    )
2358}
2359
2360fn fake_compact_block_from_compact_tx(
2361    ctx: CompactTx,
2362    height: BlockHeight,
2363    prev_hash: BlockHash,
2364    initial_sapling_tree_size: u32,
2365    initial_orchard_tree_size: u32,
2366    mut rng: impl RngCore,
2367) -> CompactBlock {
2368    let mut cb = CompactBlock {
2369        hash: {
2370            let mut hash = vec![0; 32];
2371            rng.fill_bytes(&mut hash);
2372            hash
2373        },
2374        height: height.into(),
2375        ..Default::default()
2376    };
2377    cb.prev_hash.extend_from_slice(&prev_hash.0);
2378    cb.vtx.push(ctx);
2379    cb.chain_metadata = Some(compact_formats::ChainMetadata {
2380        sapling_commitment_tree_size: initial_sapling_tree_size
2381            + cb.vtx.iter().map(|tx| tx.outputs.len() as u32).sum::<u32>(),
2382        orchard_commitment_tree_size: initial_orchard_tree_size
2383            + cb.vtx.iter().map(|tx| tx.actions.len() as u32).sum::<u32>(),
2384    });
2385    cb
2386}
2387
2388/// Trait used by tests that require a block cache.
2389pub trait TestCache {
2390    type BsError: core::fmt::Debug;
2391    type BlockSource: BlockSource<Error = Self::BsError>;
2392    type InsertResult;
2393
2394    /// Exposes the block cache as a [`BlockSource`].
2395    fn block_source(&self) -> &Self::BlockSource;
2396
2397    /// Inserts a CompactBlock into the cache DB.
2398    fn insert(&mut self, cb: &CompactBlock) -> Self::InsertResult;
2399
2400    /// Deletes block data from the cache, retaining blocks at heights less than or equal to the
2401    /// specified height.
2402    fn truncate_to_height(&mut self, height: BlockHeight);
2403}
2404
2405/// A convenience type for the note commitments contained within a [`CompactBlock`].
2406///
2407/// Indended for use as (part of) the [`TestCache::InsertResult`] associated type.
2408pub struct NoteCommitments {
2409    sapling: Vec<::sapling::Node>,
2410    #[cfg(feature = "orchard")]
2411    orchard: Vec<MerkleHashOrchard>,
2412}
2413
2414impl NoteCommitments {
2415    /// Extracts the note commitments from the given compact block.
2416    pub fn from_compact_block(cb: &CompactBlock) -> Self {
2417        NoteCommitments {
2418            sapling: cb
2419                .vtx
2420                .iter()
2421                .flat_map(|tx| {
2422                    tx.outputs
2423                        .iter()
2424                        .map(|out| ::sapling::Node::from_cmu(&out.cmu().unwrap()))
2425                })
2426                .collect(),
2427            #[cfg(feature = "orchard")]
2428            orchard: cb
2429                .vtx
2430                .iter()
2431                .flat_map(|tx| {
2432                    tx.actions
2433                        .iter()
2434                        .map(|act| MerkleHashOrchard::from_cmx(&act.cmx().unwrap()))
2435                })
2436                .collect(),
2437        }
2438    }
2439
2440    /// Returns the Sapling note commitments.
2441    #[allow(dead_code)]
2442    pub fn sapling(&self) -> &[::sapling::Node] {
2443        self.sapling.as_ref()
2444    }
2445
2446    /// Returns the Orchard note commitments.
2447    #[cfg(feature = "orchard")]
2448    pub fn orchard(&self) -> &[MerkleHashOrchard] {
2449        self.orchard.as_ref()
2450    }
2451}
2452
2453/// A mock wallet data source that implements the bare minimum necessary to function.
2454pub struct MockWalletDb {
2455    pub network: Network,
2456    pub sapling_tree: ShardTree<
2457        MemoryShardStore<::sapling::Node, BlockHeight>,
2458        { SAPLING_SHARD_HEIGHT * 2 },
2459        SAPLING_SHARD_HEIGHT,
2460    >,
2461    #[cfg(feature = "orchard")]
2462    pub orchard_tree: ShardTree<
2463        MemoryShardStore<::orchard::tree::MerkleHashOrchard, BlockHeight>,
2464        { ORCHARD_SHARD_HEIGHT * 2 },
2465        ORCHARD_SHARD_HEIGHT,
2466    >,
2467}
2468
2469impl MockWalletDb {
2470    /// Constructs a new mock wallet data source.
2471    pub fn new(network: Network) -> Self {
2472        Self {
2473            network,
2474            sapling_tree: ShardTree::new(MemoryShardStore::empty(), 100),
2475            #[cfg(feature = "orchard")]
2476            orchard_tree: ShardTree::new(MemoryShardStore::empty(), 100),
2477        }
2478    }
2479}
2480
2481impl InputSource for MockWalletDb {
2482    type Error = ();
2483    type NoteRef = u32;
2484    type AccountId = u32;
2485
2486    fn get_spendable_note(
2487        &self,
2488        _txid: &TxId,
2489        _protocol: ShieldedProtocol,
2490        _index: u32,
2491    ) -> Result<Option<ReceivedNote<Self::NoteRef, Note>>, Self::Error> {
2492        Ok(None)
2493    }
2494
2495    fn select_spendable_notes(
2496        &self,
2497        _account: Self::AccountId,
2498        _target_value: Zatoshis,
2499        _sources: &[ShieldedProtocol],
2500        _anchor_height: BlockHeight,
2501        _exclude: &[Self::NoteRef],
2502    ) -> Result<SpendableNotes<Self::NoteRef>, Self::Error> {
2503        Ok(SpendableNotes::empty())
2504    }
2505
2506    fn get_account_metadata(
2507        &self,
2508        _account: Self::AccountId,
2509        _selector: &NoteFilter,
2510        _exclude: &[Self::NoteRef],
2511    ) -> Result<AccountMeta, Self::Error> {
2512        Err(())
2513    }
2514}
2515
2516impl WalletRead for MockWalletDb {
2517    type Error = ();
2518    type AccountId = u32;
2519    type Account = (Self::AccountId, UnifiedFullViewingKey);
2520
2521    fn get_account_ids(&self) -> Result<Vec<Self::AccountId>, Self::Error> {
2522        Ok(Vec::new())
2523    }
2524
2525    fn get_account(
2526        &self,
2527        _account_id: Self::AccountId,
2528    ) -> Result<Option<Self::Account>, Self::Error> {
2529        Ok(None)
2530    }
2531
2532    fn get_derived_account(
2533        &self,
2534        _seed: &SeedFingerprint,
2535        _account_id: zip32::AccountId,
2536    ) -> Result<Option<Self::Account>, Self::Error> {
2537        Ok(None)
2538    }
2539
2540    fn validate_seed(
2541        &self,
2542        _account_id: Self::AccountId,
2543        _seed: &SecretVec<u8>,
2544    ) -> Result<bool, Self::Error> {
2545        Ok(false)
2546    }
2547
2548    fn seed_relevance_to_derived_accounts(
2549        &self,
2550        _seed: &SecretVec<u8>,
2551    ) -> Result<SeedRelevance<Self::AccountId>, Self::Error> {
2552        Ok(SeedRelevance::NoAccounts)
2553    }
2554
2555    fn get_account_for_ufvk(
2556        &self,
2557        _ufvk: &UnifiedFullViewingKey,
2558    ) -> Result<Option<Self::Account>, Self::Error> {
2559        Ok(None)
2560    }
2561
2562    fn list_addresses(&self, _account: Self::AccountId) -> Result<Vec<AddressInfo>, Self::Error> {
2563        Ok(vec![])
2564    }
2565
2566    fn get_last_generated_address_matching(
2567        &self,
2568        _account: Self::AccountId,
2569        _request: UnifiedAddressRequest,
2570    ) -> Result<Option<UnifiedAddress>, Self::Error> {
2571        Ok(None)
2572    }
2573
2574    fn get_account_birthday(&self, _account: Self::AccountId) -> Result<BlockHeight, Self::Error> {
2575        Err(())
2576    }
2577
2578    fn get_wallet_birthday(&self) -> Result<Option<BlockHeight>, Self::Error> {
2579        Ok(None)
2580    }
2581
2582    fn get_wallet_summary(
2583        &self,
2584        _min_confirmations: u32,
2585    ) -> Result<Option<WalletSummary<Self::AccountId>>, Self::Error> {
2586        Ok(None)
2587    }
2588
2589    fn chain_height(&self) -> Result<Option<BlockHeight>, Self::Error> {
2590        Ok(None)
2591    }
2592
2593    fn get_block_hash(&self, _block_height: BlockHeight) -> Result<Option<BlockHash>, Self::Error> {
2594        Ok(None)
2595    }
2596
2597    fn block_metadata(&self, _height: BlockHeight) -> Result<Option<BlockMetadata>, Self::Error> {
2598        Ok(None)
2599    }
2600
2601    fn block_fully_scanned(&self) -> Result<Option<BlockMetadata>, Self::Error> {
2602        Ok(None)
2603    }
2604
2605    fn get_max_height_hash(&self) -> Result<Option<(BlockHeight, BlockHash)>, Self::Error> {
2606        Ok(None)
2607    }
2608
2609    fn block_max_scanned(&self) -> Result<Option<BlockMetadata>, Self::Error> {
2610        Ok(None)
2611    }
2612
2613    fn suggest_scan_ranges(&self) -> Result<Vec<ScanRange>, Self::Error> {
2614        Ok(vec![])
2615    }
2616
2617    fn get_target_and_anchor_heights(
2618        &self,
2619        _min_confirmations: NonZeroU32,
2620    ) -> Result<Option<(BlockHeight, BlockHeight)>, Self::Error> {
2621        Ok(None)
2622    }
2623
2624    fn get_tx_height(&self, _txid: TxId) -> Result<Option<BlockHeight>, Self::Error> {
2625        Ok(None)
2626    }
2627
2628    fn get_unified_full_viewing_keys(
2629        &self,
2630    ) -> Result<HashMap<Self::AccountId, UnifiedFullViewingKey>, Self::Error> {
2631        Ok(HashMap::new())
2632    }
2633
2634    fn get_memo(&self, _id_note: NoteId) -> Result<Option<Memo>, Self::Error> {
2635        Ok(None)
2636    }
2637
2638    fn get_transaction(&self, _txid: TxId) -> Result<Option<Transaction>, Self::Error> {
2639        Ok(None)
2640    }
2641
2642    fn get_sapling_nullifiers(
2643        &self,
2644        _query: NullifierQuery,
2645    ) -> Result<Vec<(Self::AccountId, ::sapling::Nullifier)>, Self::Error> {
2646        Ok(Vec::new())
2647    }
2648
2649    #[cfg(feature = "orchard")]
2650    fn get_orchard_nullifiers(
2651        &self,
2652        _query: NullifierQuery,
2653    ) -> Result<Vec<(Self::AccountId, ::orchard::note::Nullifier)>, Self::Error> {
2654        Ok(Vec::new())
2655    }
2656
2657    #[cfg(feature = "transparent-inputs")]
2658    fn get_transparent_receivers(
2659        &self,
2660        _account: Self::AccountId,
2661        _include_change: bool,
2662    ) -> Result<HashMap<TransparentAddress, Option<TransparentAddressMetadata>>, Self::Error> {
2663        Ok(HashMap::new())
2664    }
2665
2666    #[cfg(feature = "transparent-inputs")]
2667    fn get_transparent_balances(
2668        &self,
2669        _account: Self::AccountId,
2670        _max_height: BlockHeight,
2671    ) -> Result<HashMap<TransparentAddress, Zatoshis>, Self::Error> {
2672        Ok(HashMap::new())
2673    }
2674
2675    #[cfg(feature = "transparent-inputs")]
2676    fn get_transparent_address_metadata(
2677        &self,
2678        _account: Self::AccountId,
2679        _address: &TransparentAddress,
2680    ) -> Result<Option<TransparentAddressMetadata>, Self::Error> {
2681        Ok(None)
2682    }
2683
2684    #[cfg(feature = "transparent-inputs")]
2685    fn get_known_ephemeral_addresses(
2686        &self,
2687        _account: Self::AccountId,
2688        _index_range: Option<Range<NonHardenedChildIndex>>,
2689    ) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, Self::Error> {
2690        Ok(vec![])
2691    }
2692
2693    #[cfg(feature = "transparent-inputs")]
2694    fn utxo_query_height(&self, _account: Self::AccountId) -> Result<BlockHeight, Self::Error> {
2695        Ok(BlockHeight::from(0u32))
2696    }
2697
2698    #[cfg(feature = "transparent-inputs")]
2699    fn find_account_for_ephemeral_address(
2700        &self,
2701        _address: &TransparentAddress,
2702    ) -> Result<Option<Self::AccountId>, Self::Error> {
2703        Ok(None)
2704    }
2705
2706    fn transaction_data_requests(&self) -> Result<Vec<TransactionDataRequest>, Self::Error> {
2707        Ok(vec![])
2708    }
2709}
2710
2711impl WalletWrite for MockWalletDb {
2712    type UtxoRef = u32;
2713
2714    fn create_account(
2715        &mut self,
2716        _account_name: &str,
2717        seed: &SecretVec<u8>,
2718        _birthday: &AccountBirthday,
2719        _key_source: Option<&str>,
2720    ) -> Result<(Self::AccountId, UnifiedSpendingKey), Self::Error> {
2721        let account = zip32::AccountId::ZERO;
2722        UnifiedSpendingKey::from_seed(&self.network, seed.expose_secret(), account)
2723            .map(|k| (u32::from(account), k))
2724            .map_err(|_| ())
2725    }
2726
2727    fn import_account_hd(
2728        &mut self,
2729        _account_name: &str,
2730        _seed: &SecretVec<u8>,
2731        _account_index: zip32::AccountId,
2732        _birthday: &AccountBirthday,
2733        _key_source: Option<&str>,
2734    ) -> Result<(Self::Account, UnifiedSpendingKey), Self::Error> {
2735        todo!()
2736    }
2737
2738    fn import_account_ufvk(
2739        &mut self,
2740        _account_name: &str,
2741        _unified_key: &UnifiedFullViewingKey,
2742        _birthday: &AccountBirthday,
2743        _purpose: AccountPurpose,
2744        _key_source: Option<&str>,
2745    ) -> Result<Self::Account, Self::Error> {
2746        todo!()
2747    }
2748
2749    fn get_next_available_address(
2750        &mut self,
2751        _account: Self::AccountId,
2752        _request: UnifiedAddressRequest,
2753    ) -> Result<Option<(UnifiedAddress, DiversifierIndex)>, Self::Error> {
2754        Ok(None)
2755    }
2756
2757    fn get_address_for_index(
2758        &mut self,
2759        _account: Self::AccountId,
2760        _diversifier_index: DiversifierIndex,
2761        _request: UnifiedAddressRequest,
2762    ) -> Result<Option<UnifiedAddress>, Self::Error> {
2763        Ok(None)
2764    }
2765
2766    #[allow(clippy::type_complexity)]
2767    fn put_blocks(
2768        &mut self,
2769        _from_state: &ChainState,
2770        _blocks: Vec<ScannedBlock<Self::AccountId>>,
2771    ) -> Result<(), Self::Error> {
2772        Ok(())
2773    }
2774
2775    fn update_chain_tip(&mut self, _tip_height: BlockHeight) -> Result<(), Self::Error> {
2776        Ok(())
2777    }
2778
2779    fn store_decrypted_tx(
2780        &mut self,
2781        _received_tx: DecryptedTransaction<Self::AccountId>,
2782    ) -> Result<(), Self::Error> {
2783        Ok(())
2784    }
2785
2786    fn store_transactions_to_be_sent(
2787        &mut self,
2788        _transactions: &[SentTransaction<Self::AccountId>],
2789    ) -> Result<(), Self::Error> {
2790        Ok(())
2791    }
2792
2793    fn truncate_to_height(
2794        &mut self,
2795        _block_height: BlockHeight,
2796    ) -> Result<BlockHeight, Self::Error> {
2797        Err(())
2798    }
2799
2800    /// Adds a transparent UTXO received by the wallet to the data store.
2801    fn put_received_transparent_utxo(
2802        &mut self,
2803        _output: &WalletTransparentOutput,
2804    ) -> Result<Self::UtxoRef, Self::Error> {
2805        Ok(0)
2806    }
2807
2808    #[cfg(feature = "transparent-inputs")]
2809    fn reserve_next_n_ephemeral_addresses(
2810        &mut self,
2811        _account_id: Self::AccountId,
2812        _n: usize,
2813    ) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, Self::Error> {
2814        Err(())
2815    }
2816
2817    fn set_transaction_status(
2818        &mut self,
2819        _txid: TxId,
2820        _status: TransactionStatus,
2821    ) -> Result<(), Self::Error> {
2822        Ok(())
2823    }
2824}
2825
2826impl WalletCommitmentTrees for MockWalletDb {
2827    type Error = Infallible;
2828    type SaplingShardStore<'a> = MemoryShardStore<::sapling::Node, BlockHeight>;
2829
2830    fn with_sapling_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
2831    where
2832        for<'a> F: FnMut(
2833            &'a mut ShardTree<
2834                Self::SaplingShardStore<'a>,
2835                { ::sapling::NOTE_COMMITMENT_TREE_DEPTH },
2836                SAPLING_SHARD_HEIGHT,
2837            >,
2838        ) -> Result<A, E>,
2839        E: From<ShardTreeError<Infallible>>,
2840    {
2841        callback(&mut self.sapling_tree)
2842    }
2843
2844    fn put_sapling_subtree_roots(
2845        &mut self,
2846        start_index: u64,
2847        roots: &[CommitmentTreeRoot<::sapling::Node>],
2848    ) -> Result<(), ShardTreeError<Self::Error>> {
2849        self.with_sapling_tree_mut(|t| {
2850            for (root, i) in roots.iter().zip(0u64..) {
2851                let root_addr = incrementalmerkletree::Address::from_parts(
2852                    SAPLING_SHARD_HEIGHT.into(),
2853                    start_index + i,
2854                );
2855                t.insert(root_addr, *root.root_hash())?;
2856            }
2857            Ok::<_, ShardTreeError<Self::Error>>(())
2858        })?;
2859
2860        Ok(())
2861    }
2862
2863    #[cfg(feature = "orchard")]
2864    type OrchardShardStore<'a> = MemoryShardStore<::orchard::tree::MerkleHashOrchard, BlockHeight>;
2865
2866    #[cfg(feature = "orchard")]
2867    fn with_orchard_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
2868    where
2869        for<'a> F: FnMut(
2870            &'a mut ShardTree<
2871                Self::OrchardShardStore<'a>,
2872                { ORCHARD_SHARD_HEIGHT * 2 },
2873                ORCHARD_SHARD_HEIGHT,
2874            >,
2875        ) -> Result<A, E>,
2876        E: From<ShardTreeError<Self::Error>>,
2877    {
2878        callback(&mut self.orchard_tree)
2879    }
2880
2881    /// Adds a sequence of note commitment tree subtree roots to the data store.
2882    #[cfg(feature = "orchard")]
2883    fn put_orchard_subtree_roots(
2884        &mut self,
2885        start_index: u64,
2886        roots: &[CommitmentTreeRoot<::orchard::tree::MerkleHashOrchard>],
2887    ) -> Result<(), ShardTreeError<Self::Error>> {
2888        self.with_orchard_tree_mut(|t| {
2889            for (root, i) in roots.iter().zip(0u64..) {
2890                let root_addr = incrementalmerkletree::Address::from_parts(
2891                    ORCHARD_SHARD_HEIGHT.into(),
2892                    start_index + i,
2893                );
2894                t.insert(root_addr, *root.root_hash())?;
2895            }
2896            Ok::<_, ShardTreeError<Self::Error>>(())
2897        })?;
2898
2899        Ok(())
2900    }
2901}