Skip to main content

zcash_client_backend/data_api/
testing.rs

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