Skip to main content

zcash_client_backend/data_api/testing/
transparent.rs

1use std::{
2    collections::{BTreeMap, HashSet},
3    convert::Infallible,
4};
5
6use assert_matches::assert_matches;
7
8use nonempty::NonEmpty;
9use sapling::zip32::ExtendedSpendingKey;
10use subtle::ConditionallySelectable;
11use transparent::{
12    address::{Script, TransparentAddress},
13    bundle::{Authorized, Bundle, OutPoint, TxIn, TxOut},
14    keys::{NonHardenedChildIndex, TransparentKeyScope},
15};
16use zcash_keys::{
17    address::Address,
18    keys::{UnifiedAddressRequest, transparent::gap_limits::GapLimits},
19};
20use zcash_primitives::{
21    block::{Block, BlockHash, BlockHeaderData},
22    transaction::{
23        Transaction, TransactionData, TxVersion, builder::NU7_DEFAULT_TX_EXPIRY_DELTA, fees::zip317,
24    },
25};
26use zcash_protocol::{
27    consensus::{BlockHeight, BranchId, COINBASE_MATURITY_BLOCKS},
28    local_consensus::LocalNetwork,
29    value::Zatoshis,
30};
31use zip321::{Payment, TransactionRequest};
32
33#[cfg(feature = "transparent-key-import")]
34use {
35    crate::{
36        data_api::{
37            AccountBirthday,
38            wallet::{self, SpendingKeys},
39        },
40        wallet::TransparentAddressSource,
41    },
42    rand::{rand_core::UnwrapErr, rngs::SysRng},
43    secp256k1::SecretKey,
44    secrecy::Secret,
45    std::collections::HashMap,
46    zcash_protocol::consensus::{NetworkUpgrade, Parameters},
47    zcash_script::{descriptor::sh, pattern::check_multisig, script},
48};
49
50use super::TestAccount;
51use crate::{
52    data_api::{
53        Account as _, AccountBalance, Balance, BlockMetadata, CoinbaseFilter, InputSource as _,
54        MaxSpendMode, ScannedBlock, TargetValue, WalletRead, WalletTest as _, WalletWrite,
55        chain::ChainState,
56        testing::{
57            AddressType, DataStoreFactory, ShieldedPool, TestBuilder, TestCache, TestState,
58            single_output_change_strategy,
59        },
60        wallet::{
61            ConfirmationsPolicy, TargetHeight, decrypt_and_store_transaction,
62            input_selection::{
63                GreedyInputSelector, LockFilter, LockedInputPolicy, SpendPolicy,
64                TransparentSpendPolicy,
65            },
66        },
67    },
68    fees::{ChangeValue, StandardFeeRule, TransparentChangePolicy},
69    scanning::{
70        ScanningKeys, SpendIdentifiers,
71        full::{decrypt_block, scan_block},
72    },
73    wallet::{Exposure, OvkPolicy, WalletTransparentOutput},
74};
75
76pub use super::pool::locking::transparent_note_locking;
77
78/// Checks whether the transparent balance of the given test `account` is as `expected`
79/// considering the `confirmations_policy`.
80fn check_balance<DSF>(
81    st: &TestState<impl TestCache, <DSF as DataStoreFactory>::DataStore, LocalNetwork>,
82    account: &TestAccount<<DSF as DataStoreFactory>::Account>,
83    taddr: &TransparentAddress,
84    confirmations_policy: ConfirmationsPolicy,
85    expected: &Balance,
86) where
87    DSF: DataStoreFactory,
88{
89    // Check the wallet summary returns the expected transparent balance.
90    let summary = st
91        .wallet()
92        .get_wallet_summary(confirmations_policy)
93        .unwrap()
94        .unwrap();
95    let balance = summary.account_balances().get(&account.id()).unwrap();
96
97    #[allow(deprecated)]
98    let old_unshielded_value = balance.unshielded();
99    assert_eq!(old_unshielded_value, expected.total());
100    assert_eq!(balance.unshielded_regular_balance(), expected);
101    assert_eq!(balance.unshielded_coinbase_balance(), &Balance::ZERO);
102    assert_eq!(balance.unshielded_balance(), *expected);
103
104    // Check the older APIs for consistency.
105    let target_height = TargetHeight::from(st.wallet().chain_height().unwrap().unwrap() + 1);
106    assert_eq!(
107        st.wallet()
108            .get_transparent_balances(account.id(), target_height, confirmations_policy)
109            .unwrap()
110            .get(taddr)
111            .cloned()
112            .map_or(Zatoshis::ZERO, |(_, b)| b.spendable_value()),
113        expected.total(),
114    );
115    assert_eq!(
116        st.wallet()
117            .get_spendable_transparent_outputs(
118                taddr,
119                target_height,
120                confirmations_policy,
121                CoinbaseFilter::AllTransparentOutputs,
122                LockFilter::Policy(&LockedInputPolicy::Exclude),
123            )
124            .unwrap()
125            .into_iter()
126            .map(|utxo| utxo.value())
127            .sum::<Option<Zatoshis>>(),
128        Some(expected.spendable_value()),
129    );
130}
131
132pub fn put_received_transparent_utxo<DSF>(dsf: DSF)
133where
134    DSF: DataStoreFactory,
135    <<DSF as DataStoreFactory>::DataStore as WalletWrite>::UtxoRef: std::fmt::Debug + PartialEq,
136{
137    let mut st = TestBuilder::new()
138        .with_data_store_factory(dsf)
139        .with_account_from_sapling_activation(BlockHash([0; 32]))
140        .build();
141
142    let birthday = st.test_account().unwrap().birthday().height();
143    let account_id = st.test_account().unwrap().id();
144    let uaddr = st
145        .wallet()
146        .get_last_generated_address_matching(account_id, UnifiedAddressRequest::AllAvailableKeys)
147        .unwrap()
148        .unwrap();
149    let taddr = uaddr.transparent().unwrap();
150
151    let height_1 = birthday + 12345;
152    st.wallet_mut().update_chain_tip(height_1).unwrap();
153
154    let bal_absent = st
155        .wallet()
156        .get_transparent_balances(
157            account_id,
158            TargetHeight::from(height_1 + 1),
159            ConfirmationsPolicy::MIN,
160        )
161        .unwrap();
162    assert!(bal_absent.is_empty());
163
164    // Create a fake transparent output.
165    let value = Zatoshis::const_from_u64(100000);
166    let outpoint = OutPoint::fake();
167    let txout = TxOut::new(value, taddr.script().into());
168
169    // Pretend the output's transaction was mined at `height_1`.
170    let utxo = WalletTransparentOutput::from_parts(
171        outpoint.clone(),
172        txout.clone(),
173        Some(height_1),
174        Some(account_id),
175        Some(TransparentKeyScope::EXTERNAL),
176        None,
177    )
178    .unwrap();
179    let res0 = st.wallet_mut().put_received_transparent_utxo(&utxo);
180    assert_matches!(res0, Ok(_));
181
182    let target_height = TargetHeight::from(height_1 + 1);
183    // Confirm that we see the output unspent as of `height_1`.
184    assert_matches!(
185        st.wallet().get_spendable_transparent_outputs(
186            taddr,
187            target_height,
188            ConfirmationsPolicy::MIN,
189            CoinbaseFilter::AllTransparentOutputs,
190            LockFilter::Policy(&LockedInputPolicy::Exclude),
191        ).as_deref(),
192        Ok([ret])
193        if (ret.outpoint(), ret.txout(), ret.mined_height()) == (utxo.outpoint(), utxo.txout(), Some(height_1))
194    );
195    assert_matches!(
196        st.wallet()
197            .get_unspent_transparent_output(utxo.outpoint(), target_height),
198        Ok(Some(ret))
199        if (ret.outpoint(), ret.txout(), ret.mined_height()) == (utxo.outpoint(), utxo.txout(), Some(height_1))
200    );
201
202    // Change the mined height of the UTXO and upsert; we should get back
203    // the same `UtxoId`.
204    let height_2 = birthday + 34567;
205    st.wallet_mut().update_chain_tip(height_2).unwrap();
206    let utxo2 = WalletTransparentOutput::from_parts(
207        outpoint,
208        txout,
209        Some(height_2),
210        Some(account_id),
211        Some(TransparentKeyScope::EXTERNAL),
212        None,
213    )
214    .unwrap();
215    let res1 = st.wallet_mut().put_received_transparent_utxo(&utxo2);
216    assert_matches!(res1, Ok(id) if id == res0.unwrap());
217
218    // Confirm that we no longer see any unspent outputs as of `height_1`.
219    assert_matches!(
220        st.wallet()
221            .get_spendable_transparent_outputs(
222                taddr,
223                target_height,
224                ConfirmationsPolicy::MIN,
225                CoinbaseFilter::AllTransparentOutputs,
226                LockFilter::Policy(&LockedInputPolicy::Exclude)
227            )
228            .as_deref(),
229        Ok(&[])
230    );
231
232    // We can still look up the specific output, and it has the expected height.
233    assert_matches!(
234        st.wallet()
235            .get_unspent_transparent_output(utxo2.outpoint(), target_height),
236        Ok(Some(ret))
237        if (ret.outpoint(), ret.txout(), ret.mined_height()) == (utxo2.outpoint(), utxo2.txout(), Some(height_2))
238    );
239
240    // If we include `height_2` then the output is returned.
241    assert_matches!(
242        st.wallet()
243            .get_spendable_transparent_outputs(taddr, TargetHeight::from(height_2 + 1), ConfirmationsPolicy::MIN, CoinbaseFilter::AllTransparentOutputs, LockFilter::Policy(&LockedInputPolicy::Exclude))
244            .as_deref(),
245        Ok([ret]) if (ret.outpoint(), ret.txout(), ret.mined_height()) == (utxo.outpoint(), utxo.txout(), Some(height_2))
246    );
247
248    assert_matches!(
249        st.wallet().get_transparent_balances(
250            account_id,
251            TargetHeight::from(height_2 + 1),
252            ConfirmationsPolicy::MIN
253        ),
254        Ok(h) if h.get(taddr).map(|(_, b)| b.spendable_value()) == Some(value)
255    );
256}
257
258/// Tests that an unmined UTXO received when the chain tip is one block below NU7 activation is
259/// assumed to expire after the default expiry delta for the mempool height, which is the NU7
260/// activation height.
261pub fn put_received_unmined_utxo_uses_mempool_height_expiry_delta<DSF>(dsf: DSF)
262where
263    DSF: DataStoreFactory,
264{
265    /// The number of blocks between Sapling activation and NU7 activation on the test network.
266    const NU7_ACTIVATION_OFFSET: u32 = 10;
267
268    let sapling_activation = TestBuilder::<(), ()>::DEFAULT_NETWORK
269        .sapling
270        .expect("the default test network activates Sapling");
271    let nu7_activation = sapling_activation + NU7_ACTIVATION_OFFSET;
272    let mut st = TestBuilder::new()
273        .with_network(LocalNetwork {
274            nu6: Some(sapling_activation),
275            nu6_1: Some(sapling_activation),
276            nu6_2: Some(sapling_activation),
277            nu6_3: Some(sapling_activation),
278            nu7: Some(nu7_activation),
279            ..TestBuilder::<(), ()>::DEFAULT_NETWORK
280        })
281        .with_data_store_factory(dsf)
282        .with_account_from_sapling_activation(BlockHash([0; 32]))
283        .build();
284
285    let account_id = st.test_account().unwrap().id();
286    let uaddr = st
287        .wallet()
288        .get_last_generated_address_matching(account_id, UnifiedAddressRequest::AllAvailableKeys)
289        .unwrap()
290        .unwrap();
291    let taddr = uaddr.transparent().unwrap();
292
293    st.wallet_mut()
294        .update_chain_tip(nu7_activation - 1)
295        .unwrap();
296
297    let outpoint = OutPoint::fake();
298    let utxo = WalletTransparentOutput::from_parts(
299        outpoint.clone(),
300        TxOut::new(Zatoshis::const_from_u64(100000), taddr.script().into()),
301        None,
302        Some(account_id),
303        Some(TransparentKeyScope::EXTERNAL),
304        None,
305    )
306    .unwrap();
307    st.wallet_mut()
308        .put_received_transparent_utxo(&utxo)
309        .unwrap();
310
311    // The output is unexpired at every target height up to the NU7 activation height plus the
312    // NU7 default expiry delta, and expired above it.
313    let last_unexpired_height = nu7_activation + NU7_DEFAULT_TX_EXPIRY_DELTA;
314    assert_matches!(
315        st.wallet()
316            .get_transparent_output(&outpoint, Some(TargetHeight::from(last_unexpired_height))),
317        Ok(Some(_))
318    );
319    assert_matches!(
320        st.wallet().get_transparent_output(
321            &outpoint,
322            Some(TargetHeight::from(last_unexpired_height + 1))
323        ),
324        Ok(None)
325    );
326}
327
328pub fn transparent_balance_across_shielding<DSF>(dsf: DSF, cache: impl TestCache)
329where
330    DSF: DataStoreFactory,
331{
332    let mut st = TestBuilder::new()
333        .with_data_store_factory(dsf)
334        .with_block_cache(cache)
335        .with_account_from_sapling_activation(BlockHash([0; 32]))
336        .build();
337
338    let account = st.test_account().cloned().unwrap();
339    let uaddr = st
340        .wallet()
341        .get_last_generated_address_matching(account.id(), UnifiedAddressRequest::AllAvailableKeys)
342        .unwrap()
343        .unwrap();
344    let taddr = uaddr.transparent().unwrap();
345
346    // Initialize the wallet with chain data that has no shielded notes for us.
347    let not_our_key = ExtendedSpendingKey::master(&[])
348        .expect("the derivation path yields a valid key")
349        .to_diversifiable_full_viewing_key();
350    let not_our_value = Zatoshis::const_from_u64(10000);
351    let (start_height, _, _) =
352        st.generate_next_block(&not_our_key, AddressType::DefaultExternal, not_our_value);
353    for _ in 1..10 {
354        st.generate_next_block(&not_our_key, AddressType::DefaultExternal, not_our_value);
355    }
356    st.scan_cached_blocks(start_height, 10);
357
358    // The wallet starts out with zero balance.
359    check_balance::<DSF>(
360        &st,
361        &account,
362        taddr,
363        ConfirmationsPolicy::MIN,
364        &Balance::ZERO,
365    );
366
367    // Create a fake transparent output.
368    let value = Zatoshis::from_u64(100000).unwrap();
369    let txout = TxOut::new(value, taddr.script().into());
370
371    // Pretend the output was received in the chain tip.
372    let height = st.wallet().chain_height().unwrap().unwrap();
373    let utxo = WalletTransparentOutput::from_parts(
374        OutPoint::fake(),
375        txout,
376        Some(height),
377        Some(account.id()),
378        Some(TransparentKeyScope::EXTERNAL),
379        None,
380    )
381    .unwrap();
382    st.wallet_mut()
383        .put_received_transparent_utxo(&utxo)
384        .unwrap();
385
386    // The wallet should detect the balance as available
387    let mut zero_or_one_conf_value = Balance::ZERO;
388
389    // add the spendable value to the expected balance
390    zero_or_one_conf_value.add_spendable_value(value).unwrap();
391
392    check_balance::<DSF>(
393        &st,
394        &account,
395        taddr,
396        ConfirmationsPolicy::MIN,
397        &zero_or_one_conf_value,
398    );
399
400    // Shield the output.
401    let input_selector = GreedyInputSelector::new();
402    let change_strategy =
403        single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Sapling);
404    let txid = st
405        .shield_transparent_funds(
406            &input_selector,
407            &change_strategy,
408            value,
409            account.usk(),
410            &[*taddr],
411            account.id(),
412            ConfirmationsPolicy::MIN,
413        )
414        .unwrap()[0];
415
416    // The wallet should have zero transparent balance, because the shielding
417    // transaction can be mined.
418    check_balance::<DSF>(
419        &st,
420        &account,
421        taddr,
422        ConfirmationsPolicy::MIN,
423        &Balance::ZERO,
424    );
425
426    // Mine the shielding transaction.
427    let (mined_height, _) = st.generate_next_block_including(txid);
428    st.scan_cached_blocks(mined_height, 1);
429
430    // The wallet should still have zero transparent balance.
431    check_balance::<DSF>(
432        &st,
433        &account,
434        taddr,
435        ConfirmationsPolicy::MIN,
436        &Balance::ZERO,
437    );
438
439    // Unmine the shielding transaction via a reorg.
440    st.wallet_mut()
441        .truncate_to_height(mined_height - 1)
442        .unwrap();
443    assert_eq!(st.wallet().chain_height().unwrap(), Some(mined_height - 1));
444
445    // The wallet should still have zero transparent balance.
446    check_balance::<DSF>(
447        &st,
448        &account,
449        taddr,
450        ConfirmationsPolicy::MIN,
451        &Balance::ZERO,
452    );
453
454    // Expire the shielding transaction.
455    let expiry_height = st
456        .wallet()
457        .get_transaction(txid)
458        .unwrap()
459        .expect("Transaction exists in the wallet.")
460        .expiry_height();
461    st.wallet_mut().update_chain_tip(expiry_height).unwrap();
462
463    check_balance::<DSF>(
464        &st,
465        &account,
466        taddr,
467        ConfirmationsPolicy::MIN,
468        &zero_or_one_conf_value,
469    );
470}
471
472/// Regression test for [PRO-291]: shielding a transparent balance composed of many P2PKH
473/// UTXOs must not fail with `ChangeRequired` due to fee disagreement between the proposal
474/// and builder layers.
475///
476/// At 150 P2PKH inputs the proposal-time fee computation (which uses
477/// `STANDARD_P2PKH = 150` bytes per input) starts to diverge from the builder-time
478/// fee computation (which historically used the actual serialized size, 149 bytes per
479/// input) due to the `ceildiv(t_in_total_size, 150)` term in the ZIP 317 fee formula.
480/// The discrepancy grows by one logical action (5000 zats) for every additional 150
481/// inputs.
482///
483/// [PRO-291]: https://linear.app/zodl/issue/PRO-291
484pub fn shielding_many_transparent_utxos<DSF>(dsf: DSF, cache: impl TestCache)
485where
486    DSF: DataStoreFactory,
487{
488    // Choose enough UTXOs to cross the first ceildiv(_, 150) boundary.
489    const NUM_UTXOS: usize = 160;
490    // Per-UTXO value comfortably above the marginal fee so none are treated as dust.
491    const PER_UTXO: u64 = 100_000;
492
493    let mut st = TestBuilder::new()
494        .with_data_store_factory(dsf)
495        .with_block_cache(cache)
496        .with_account_from_sapling_activation(BlockHash([0; 32]))
497        .build();
498
499    let account = st.test_account().cloned().unwrap();
500    let uaddr = st
501        .wallet()
502        .get_last_generated_address_matching(account.id(), UnifiedAddressRequest::AllAvailableKeys)
503        .unwrap()
504        .unwrap();
505    let taddr = uaddr.transparent().unwrap();
506
507    // Initialize the wallet with chain data that has no shielded notes for us.
508    let not_our_key = ExtendedSpendingKey::master(&[])
509        .expect("the derivation path yields a valid key")
510        .to_diversifiable_full_viewing_key();
511    let not_our_value = Zatoshis::const_from_u64(10_000);
512    let (start_height, _, _) =
513        st.generate_next_block(&not_our_key, AddressType::DefaultExternal, not_our_value);
514    for _ in 1..10 {
515        st.generate_next_block(&not_our_key, AddressType::DefaultExternal, not_our_value);
516    }
517    st.scan_cached_blocks(start_height, 10);
518
519    // Add many distinct P2PKH UTXOs to the wallet, all at the same transparent address.
520    let value = Zatoshis::const_from_u64(PER_UTXO);
521    let txout = TxOut::new(value, taddr.script().into());
522    let height = st.wallet().chain_height().unwrap().unwrap();
523    for i in 0..NUM_UTXOS {
524        let mut hash = [0u8; 32];
525        hash[..4].copy_from_slice(&(i as u32).to_le_bytes());
526        let outpoint = OutPoint::new(hash, 0);
527        let utxo = WalletTransparentOutput::from_parts(
528            outpoint,
529            txout.clone(),
530            Some(height),
531            Some(account.id()),
532            Some(TransparentKeyScope::EXTERNAL),
533            None,
534        )
535        .unwrap();
536        st.wallet_mut()
537            .put_received_transparent_utxo(&utxo)
538            .unwrap();
539    }
540
541    // Shield the transparent balance.
542    let input_selector = GreedyInputSelector::new();
543    let change_strategy =
544        single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Sapling);
545    let txids = st
546        .shield_transparent_funds(
547            &input_selector,
548            &change_strategy,
549            value,
550            account.usk(),
551            &[*taddr],
552            account.id(),
553            ConfirmationsPolicy::MIN,
554        )
555        .expect("shielding many P2PKH UTXOs should succeed");
556    assert_eq!(txids.len(), 1);
557
558    // After shielding, the transparent balance should be zero.
559    check_balance::<DSF>(
560        &st,
561        &account,
562        taddr,
563        ConfirmationsPolicy::MIN,
564        &Balance::ZERO,
565    );
566}
567
568/// Verifies that `InputSource::get_spendable_transparent_outputs_for_addresses` returns the
569/// spendable outputs for a *set* of addresses in a single call, equivalent to the union of
570/// per-address queries, and honours subset and empty requests.
571pub fn get_spendable_transparent_outputs_for_addresses<DSF>(dsf: DSF)
572where
573    DSF: DataStoreFactory,
574{
575    let mut st = TestBuilder::new()
576        .with_data_store_factory(dsf)
577        .with_account_from_sapling_activation(BlockHash([0; 32]))
578        .build();
579
580    let account_id = st.test_account().unwrap().id();
581    let birthday = st.test_account().unwrap().birthday().height();
582
583    let height_1 = birthday + 12345;
584    st.wallet_mut().update_chain_tip(height_1).unwrap();
585
586    // Obtain three distinct transparent receivers for the account.
587    let mut taddrs = Vec::new();
588    while taddrs.len() < 3 {
589        let (ua, _) = st
590            .wallet_mut()
591            .get_next_available_address(account_id, UnifiedAddressRequest::AllAvailableKeys)
592            .unwrap()
593            .expect("an address should be available within the gap limit");
594        if let Some(taddr) = ua.transparent() {
595            taddrs.push(*taddr);
596        }
597    }
598
599    // Place one distinct UTXO at each address.
600    let value = Zatoshis::const_from_u64(100_000);
601    for (i, taddr) in taddrs.iter().enumerate() {
602        let mut hash = [0u8; 32];
603        hash[..4].copy_from_slice(&(i as u32).to_le_bytes());
604        let utxo = WalletTransparentOutput::from_parts(
605            OutPoint::new(hash, 0),
606            TxOut::new(value, taddr.script().into()),
607            Some(height_1),
608            Some(account_id),
609            Some(TransparentKeyScope::EXTERNAL),
610            None,
611        )
612        .unwrap();
613        st.wallet_mut()
614            .put_received_transparent_utxo(&utxo)
615            .unwrap();
616    }
617
618    let target_height = TargetHeight::from(height_1 + 1);
619    let sorted = |mut v: Vec<TransparentAddress>| {
620        v.sort();
621        v
622    };
623
624    // The batched query over all three addresses returns one output per address.
625    let all = st
626        .wallet()
627        .get_spendable_transparent_outputs_for_addresses(
628            &taddrs,
629            target_height,
630            ConfirmationsPolicy::MIN,
631            CoinbaseFilter::AllTransparentOutputs,
632            LockFilter::Policy(&LockedInputPolicy::Exclude),
633        )
634        .unwrap();
635    assert_eq!(all.len(), 3);
636    assert_eq!(
637        sorted(all.iter().map(|u| *u.recipient_address()).collect()),
638        sorted(taddrs.clone()),
639    );
640
641    // It is equivalent to the union of per-address queries.
642    let mut per_address = Vec::new();
643    for taddr in &taddrs {
644        per_address.extend(
645            st.wallet()
646                .get_spendable_transparent_outputs(
647                    taddr,
648                    target_height,
649                    ConfirmationsPolicy::MIN,
650                    CoinbaseFilter::AllTransparentOutputs,
651                    LockFilter::Policy(&LockedInputPolicy::Exclude),
652                )
653                .unwrap(),
654        );
655    }
656    assert_eq!(
657        sorted(all.iter().map(|u| *u.recipient_address()).collect()),
658        sorted(per_address.iter().map(|u| *u.recipient_address()).collect()),
659    );
660
661    // A subset request returns only the requested address's output.
662    let subset = st
663        .wallet()
664        .get_spendable_transparent_outputs_for_addresses(
665            &taddrs[..1],
666            target_height,
667            ConfirmationsPolicy::MIN,
668            CoinbaseFilter::AllTransparentOutputs,
669            LockFilter::Policy(&LockedInputPolicy::Exclude),
670        )
671        .unwrap();
672    assert_eq!(subset.len(), 1);
673    assert_eq!(subset[0].recipient_address(), &taddrs[0]);
674
675    // An empty request returns no outputs.
676    assert!(
677        st.wallet()
678            .get_spendable_transparent_outputs_for_addresses(
679                &[],
680                target_height,
681                ConfirmationsPolicy::MIN,
682                CoinbaseFilter::AllTransparentOutputs,
683                LockFilter::Policy(&LockedInputPolicy::Exclude),
684            )
685            .unwrap()
686            .is_empty()
687    );
688}
689
690/// Verifies that a shielding proposal caps the number of transparent inputs in a single
691/// transaction to the selector's configured fraction of block space, selecting the highest-value
692/// UTXOs first and leaving the remainder unspent.
693pub fn shielding_transparent_input_cap<DSF>(dsf: DSF, cache: impl TestCache)
694where
695    DSF: DataStoreFactory,
696{
697    // At 1% of block space the cap is (2_000_000 * 1 / 100) / 150 = 133 inputs.
698    const BLOCK_SPACE_PERCENT: u32 = 1;
699    const CAP: usize = 133;
700    const NUM_UTXOS: usize = CAP + 7; // 140; the 7 smallest must be dropped.
701    const BASE: u64 = 100_000;
702    const STEP: u64 = 1_000;
703
704    let mut st = TestBuilder::new()
705        .with_data_store_factory(dsf)
706        .with_block_cache(cache)
707        .with_account_from_sapling_activation(BlockHash([0; 32]))
708        .build();
709
710    let account = st.test_account().cloned().unwrap();
711    let uaddr = st
712        .wallet()
713        .get_last_generated_address_matching(account.id(), UnifiedAddressRequest::AllAvailableKeys)
714        .unwrap()
715        .unwrap();
716    let taddr = uaddr.transparent().unwrap();
717
718    // Initialize the wallet with chain data that has no shielded notes for us.
719    let not_our_key = ExtendedSpendingKey::master(&[])
720        .expect("the derivation path yields a valid key")
721        .to_diversifiable_full_viewing_key();
722    let not_our_value = Zatoshis::const_from_u64(10_000);
723    let (start_height, _, _) =
724        st.generate_next_block(&not_our_key, AddressType::DefaultExternal, not_our_value);
725    for _ in 1..10 {
726        st.generate_next_block(&not_our_key, AddressType::DefaultExternal, not_our_value);
727    }
728    st.scan_cached_blocks(start_height, 10);
729
730    // Add `NUM_UTXOS` distinct-value P2PKH UTXOs at the same transparent address, so that
731    // largest-first selection is unambiguous.
732    let height = st.wallet().chain_height().unwrap().unwrap();
733    for i in 0..NUM_UTXOS {
734        let value = Zatoshis::const_from_u64(BASE + (i as u64) * STEP);
735        let mut hash = [0u8; 32];
736        hash[..4].copy_from_slice(&(i as u32).to_le_bytes());
737        let utxo = WalletTransparentOutput::from_parts(
738            OutPoint::new(hash, 0),
739            TxOut::new(value, taddr.script().into()),
740            Some(height),
741            Some(account.id()),
742            Some(TransparentKeyScope::EXTERNAL),
743            None,
744        )
745        .unwrap();
746        st.wallet_mut()
747            .put_received_transparent_utxo(&utxo)
748            .unwrap();
749    }
750
751    // Propose shielding with a 1%-of-block-space input cap.
752    let input_selector =
753        GreedyInputSelector::new().with_shielding_block_space_percent(BLOCK_SPACE_PERCENT);
754    let change_strategy =
755        single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Sapling);
756    let proposal = st
757        .propose_shielding(
758            &input_selector,
759            &change_strategy,
760            Zatoshis::const_from_u64(BASE),
761            &[*taddr],
762            account.id(),
763            ConfirmationsPolicy::MIN,
764            CoinbaseFilter::AllTransparentOutputs,
765        )
766        .expect("shielding proposal should succeed");
767
768    let inputs = proposal.steps().first().transparent_inputs();
769    assert_eq!(
770        inputs.len(),
771        CAP,
772        "the number of transparent inputs should be capped",
773    );
774
775    // The selected inputs must be the `CAP` highest-value UTXOs: the `NUM_UTXOS - CAP` smallest
776    // are dropped, so the smallest selected value is `BASE + (NUM_UTXOS - CAP) * STEP`.
777    let min_selected = inputs.iter().map(|u| u.value()).min().unwrap();
778    assert_eq!(
779        min_selected,
780        Zatoshis::const_from_u64(BASE + ((NUM_UTXOS - CAP) as u64) * STEP),
781        "the lowest-value UTXOs should be the ones left unspent",
782    );
783}
784
785/// This test attempts to verify that transparent funds spendability is
786/// accounted for properly given the different minimum confirmations values
787/// that can be set when querying for balances.
788pub fn transparent_balance_spendability<DSF>(dsf: DSF, cache: impl TestCache)
789where
790    DSF: DataStoreFactory,
791{
792    let mut st = TestBuilder::new()
793        .with_data_store_factory(dsf)
794        .with_block_cache(cache)
795        .with_account_from_sapling_activation(BlockHash([0; 32]))
796        .build();
797
798    let account = st.test_account().cloned().unwrap();
799    let uaddr = st
800        .wallet()
801        .get_last_generated_address_matching(account.id(), UnifiedAddressRequest::AllAvailableKeys)
802        .unwrap()
803        .unwrap();
804    let taddr = uaddr.transparent().unwrap();
805
806    // Initialize the wallet with chain data that has no shielded notes for us.
807    let not_our_key = ExtendedSpendingKey::master(&[])
808        .expect("the derivation path yields a valid key")
809        .to_diversifiable_full_viewing_key();
810    let not_our_value = Zatoshis::const_from_u64(10000);
811    let (start_height, _, _) =
812        st.generate_next_block(&not_our_key, AddressType::DefaultExternal, not_our_value);
813    for _ in 1..10 {
814        st.generate_next_block(&not_our_key, AddressType::DefaultExternal, not_our_value);
815    }
816    st.scan_cached_blocks(start_height, 10);
817
818    // The wallet starts out with zero balance.
819    check_balance::<DSF>(
820        &st as &TestState<_, DSF::DataStore, _>,
821        &account,
822        taddr,
823        ConfirmationsPolicy::MIN,
824        &Balance::ZERO,
825    );
826
827    // Create a fake transparent output.
828    let value = Zatoshis::from_u64(100000).unwrap();
829    let txout = TxOut::new(value, taddr.script().into());
830
831    // Pretend the output was received in the chain tip.
832    let height = st.wallet().chain_height().unwrap().unwrap();
833    let utxo = WalletTransparentOutput::from_parts(
834        OutPoint::fake(),
835        txout,
836        Some(height),
837        Some(account.id()),
838        Some(TransparentKeyScope::EXTERNAL),
839        None,
840    )
841    .unwrap();
842    st.wallet_mut()
843        .put_received_transparent_utxo(&utxo)
844        .unwrap();
845
846    // The wallet should detect the balance as available
847    let mut zero_or_one_conf_value = Balance::ZERO;
848
849    // add the spendable value to the expected balance
850    zero_or_one_conf_value.add_spendable_value(value).unwrap();
851
852    check_balance::<DSF>(
853        &st,
854        &account,
855        taddr,
856        ConfirmationsPolicy::MIN,
857        &zero_or_one_conf_value,
858    );
859
860    // now if we increase the number of confirmations our spendable balance should
861    // be zero and the total balance equal to `value`
862    let mut not_confirmed_yet_value = Balance::ZERO;
863
864    not_confirmed_yet_value
865        .add_pending_spendable_value(value)
866        .unwrap();
867
868    check_balance::<DSF>(
869        &st,
870        &account,
871        taddr,
872        ConfirmationsPolicy::new_symmetrical_unchecked(2, false),
873        &not_confirmed_yet_value,
874    );
875
876    // Add one extra block
877    st.generate_empty_block();
878
879    // Scan that block
880    st.scan_cached_blocks(height, 1);
881
882    // now we generate one more block and the balance should be the same as when the
883    // check_balance function was called with zero or one confirmation.
884    st.generate_empty_block();
885    st.scan_cached_blocks(height + 1, 1);
886
887    check_balance::<DSF>(
888        &st,
889        &account,
890        taddr,
891        ConfirmationsPolicy::new_symmetrical_unchecked(2, true),
892        &zero_or_one_conf_value,
893    );
894}
895
896/// Constructs a fake transparent-only coinbase transaction paying `value` to `taddr`.
897///
898/// The result is a structurally valid coinbase transaction (a single input spending the null
899/// outpoint), which causes the receiving wallet to classify it as coinbase when it is stored
900/// via [`decrypt_and_store_transaction`]. The `lock_time` parameter has no consensus meaning
901/// here; distinct values may be used to give otherwise-identical coinbase transactions
902/// distinct txids.
903fn fake_transparent_coinbase_tx(
904    lock_time: u32,
905    value: Zatoshis,
906    taddr: &TransparentAddress,
907) -> Transaction {
908    let coinbase_bundle = Bundle {
909        vin: vec![TxIn::from_parts(
910            OutPoint::NULL,
911            Script::default(),
912            u32::MAX,
913        )],
914        vout: vec![TxOut::new(value, taddr.script().into())],
915        authorization: Authorized,
916    };
917
918    TransactionData::<zcash_primitives::transaction::Authorized>::from_parts(
919        TxVersion::V5,
920        BranchId::Nu5,
921        lock_time,
922        // Coinbase transactions do not expire.
923        BlockHeight::from(0),
924        // Coinbase transactions burn nothing.
925        Some(coinbase_bundle),
926        None,
927        None,
928        None,
929    )
930    .freeze()
931    .unwrap()
932}
933
934/// Retrieves the [`AccountBalance`] for the given test account from the wallet summary.
935fn get_account_balance<DSF>(
936    st: &TestState<impl TestCache, <DSF as DataStoreFactory>::DataStore, LocalNetwork>,
937    account: &TestAccount<<DSF as DataStoreFactory>::Account>,
938    confirmations_policy: ConfirmationsPolicy,
939) -> AccountBalance
940where
941    DSF: DataStoreFactory,
942{
943    let summary = st
944        .wallet()
945        .get_wallet_summary(confirmations_policy)
946        .unwrap()
947        .unwrap();
948    *summary.account_balances().get(&account.id()).unwrap()
949}
950
951/// Verifies that transparent funds are reported in the correct `AccountBalance` bucket
952/// (regular vs. coinbase), that immature coinbase value is reported as pending rather than
953/// spendable, and that it becomes spendable upon reaching coinbase maturity.
954pub fn transparent_coinbase_balance_split<DSF>(ds_factory: DSF, cache: impl TestCache)
955where
956    DSF: DataStoreFactory,
957{
958    let mut st = TestBuilder::new()
959        .with_data_store_factory(ds_factory)
960        .with_block_cache(cache)
961        .with_account_from_sapling_activation(BlockHash([0; 32]))
962        .build();
963
964    let account = st.test_account().cloned().unwrap();
965    let uaddr = st
966        .wallet()
967        .get_last_generated_address_matching(account.id(), UnifiedAddressRequest::AllAvailableKeys)
968        .unwrap()
969        .unwrap();
970    let taddr = uaddr.transparent().unwrap();
971
972    // Mine a coinbase output paying the wallet's transparent address at tx index 0.
973    let coinbase_value = Zatoshis::const_from_u64(625_000_000);
974    let coinbase_tx = fake_transparent_coinbase_tx(0, coinbase_value, taddr);
975    let (h, _) = st.generate_next_block_from_tx(0, &coinbase_tx);
976    st.scan_cached_blocks(h, 1);
977    let params = *st.network();
978    decrypt_and_store_transaction(&params, st.wallet_mut(), &coinbase_tx, Some(h)).unwrap();
979
980    // Immature coinbase: the value is pending spendability in the coinbase bucket, not
981    // spendable; the regular bucket is untouched.
982    let balance = get_account_balance::<DSF>(&st, &account, ConfirmationsPolicy::MIN);
983    assert_eq!(
984        balance.unshielded_coinbase_balance().spendable_value(),
985        Zatoshis::ZERO
986    );
987    assert_eq!(
988        balance
989            .unshielded_coinbase_balance()
990            .value_pending_spendability(),
991        coinbase_value
992    );
993    assert_eq!(balance.unshielded_regular_balance(), &Balance::ZERO);
994
995    // The same holds when the confirmations policy itself is not yet satisfied (the coinbase
996    // output has only one confirmation here), which exercises the pending-balance query.
997    let balance = get_account_balance::<DSF>(
998        &st,
999        &account,
1000        ConfirmationsPolicy::new_symmetrical_unchecked(2, false),
1001    );
1002    assert_eq!(
1003        balance.unshielded_coinbase_balance().spendable_value(),
1004        Zatoshis::ZERO
1005    );
1006    assert_eq!(
1007        balance
1008            .unshielded_coinbase_balance()
1009            .value_pending_spendability(),
1010        coinbase_value
1011    );
1012    assert_eq!(balance.unshielded_regular_balance(), &Balance::ZERO);
1013
1014    // Receive a regular (non-coinbase) UTXO. This output's transaction has no known tx_index,
1015    // so it must be classified as regular (non-coinbase) funds.
1016    let regular_value = Zatoshis::const_from_u64(100_000);
1017    let utxo = WalletTransparentOutput::from_parts(
1018        OutPoint::fake(),
1019        TxOut::new(regular_value, taddr.script().into()),
1020        Some(h),
1021        Some(account.id()),
1022        Some(TransparentKeyScope::EXTERNAL),
1023        None,
1024    )
1025    .unwrap();
1026    st.wallet_mut()
1027        .put_received_transparent_utxo(&utxo)
1028        .unwrap();
1029
1030    // Mixed state: the regular value is spendable, the coinbase value remains pending, and the
1031    // combined accessors report the sums of the two buckets.
1032    let balance = get_account_balance::<DSF>(&st, &account, ConfirmationsPolicy::MIN);
1033    assert_eq!(
1034        balance.unshielded_regular_balance().spendable_value(),
1035        regular_value
1036    );
1037    assert_eq!(
1038        balance
1039            .unshielded_coinbase_balance()
1040            .value_pending_spendability(),
1041        coinbase_value
1042    );
1043    assert_eq!(
1044        balance.unshielded_balance(),
1045        (*balance.unshielded_regular_balance() + *balance.unshielded_coinbase_balance()).unwrap()
1046    );
1047    #[allow(deprecated)]
1048    let unshielded = balance.unshielded();
1049    assert_eq!(unshielded, (regular_value + coinbase_value).unwrap());
1050    assert_eq!(balance.total(), (regular_value + coinbase_value).unwrap());
1051
1052    // Once the coinbase output reaches maturity, its value moves from pending to spendable.
1053    for _ in 0..COINBASE_MATURITY_BLOCKS {
1054        st.generate_empty_block();
1055    }
1056    st.scan_cached_blocks(h + 1, COINBASE_MATURITY_BLOCKS as usize);
1057
1058    let balance = get_account_balance::<DSF>(&st, &account, ConfirmationsPolicy::MIN);
1059    assert_eq!(
1060        balance.unshielded_coinbase_balance().spendable_value(),
1061        coinbase_value
1062    );
1063    assert_eq!(
1064        balance
1065            .unshielded_coinbase_balance()
1066            .value_pending_spendability(),
1067        Zatoshis::ZERO
1068    );
1069    assert_eq!(
1070        balance.unshielded_regular_balance().spendable_value(),
1071        regular_value
1072    );
1073    assert_eq!(balance.total(), (regular_value + coinbase_value).unwrap());
1074}
1075
1076/// Verifies that dust-valued (uneconomic) transparent outputs are reported in the
1077/// `uneconomic_value` field of the correct `AccountBalance` bucket (regular vs. coinbase).
1078pub fn transparent_coinbase_balance_dust<DSF>(ds_factory: DSF, cache: impl TestCache)
1079where
1080    DSF: DataStoreFactory,
1081{
1082    let dust_value = Zatoshis::const_from_u64(1000);
1083    assert!(dust_value <= zip317::MARGINAL_FEE);
1084
1085    let mut st = TestBuilder::new()
1086        .with_data_store_factory(ds_factory)
1087        .with_block_cache(cache)
1088        .with_account_from_sapling_activation(BlockHash([0; 32]))
1089        .build();
1090
1091    let account = st.test_account().cloned().unwrap();
1092    let uaddr = st
1093        .wallet()
1094        .get_last_generated_address_matching(account.id(), UnifiedAddressRequest::AllAvailableKeys)
1095        .unwrap()
1096        .unwrap();
1097    let taddr = uaddr.transparent().unwrap();
1098
1099    // Mine a dust coinbase output paying the wallet's transparent address at tx index 0.
1100    let coinbase_tx = fake_transparent_coinbase_tx(0, dust_value, taddr);
1101    let (h, _) = st.generate_next_block_from_tx(0, &coinbase_tx);
1102    st.scan_cached_blocks(h, 1);
1103    let params = *st.network();
1104    decrypt_and_store_transaction(&params, st.wallet_mut(), &coinbase_tx, Some(h)).unwrap();
1105
1106    // Receive a dust regular (non-coinbase) UTXO.
1107    let utxo = WalletTransparentOutput::from_parts(
1108        OutPoint::fake(),
1109        TxOut::new(dust_value, taddr.script().into()),
1110        Some(h),
1111        Some(account.id()),
1112        Some(TransparentKeyScope::EXTERNAL),
1113        None,
1114    )
1115    .unwrap();
1116    st.wallet_mut()
1117        .put_received_transparent_utxo(&utxo)
1118        .unwrap();
1119
1120    // Each dust output lands in the uneconomic value of its own bucket, and contributes to
1121    // neither spendable nor pending value.
1122    let balance = get_account_balance::<DSF>(&st, &account, ConfirmationsPolicy::MIN);
1123    assert_eq!(
1124        balance.unshielded_regular_balance().uneconomic_value(),
1125        dust_value
1126    );
1127    assert_eq!(
1128        balance.unshielded_coinbase_balance().uneconomic_value(),
1129        dust_value
1130    );
1131    assert_eq!(
1132        balance.uneconomic_value(),
1133        (dust_value + dust_value).unwrap()
1134    );
1135    assert_eq!(
1136        balance.unshielded_balance().spendable_value(),
1137        Zatoshis::ZERO
1138    );
1139    assert_eq!(
1140        balance.unshielded_balance().value_pending_spendability(),
1141        Zatoshis::ZERO
1142    );
1143    assert_eq!(balance.total(), Zatoshis::ZERO);
1144
1145    // Dust classification takes precedence over coinbase maturity: after the coinbase output
1146    // matures, its value remains uneconomic rather than becoming spendable.
1147    for _ in 0..COINBASE_MATURITY_BLOCKS {
1148        st.generate_empty_block();
1149    }
1150    st.scan_cached_blocks(h + 1, COINBASE_MATURITY_BLOCKS as usize);
1151
1152    let balance = get_account_balance::<DSF>(&st, &account, ConfirmationsPolicy::MIN);
1153    assert_eq!(
1154        balance.unshielded_coinbase_balance().uneconomic_value(),
1155        dust_value
1156    );
1157    assert_eq!(
1158        balance.unshielded_coinbase_balance().spendable_value(),
1159        Zatoshis::ZERO
1160    );
1161}
1162
1163pub fn gap_limits<DSF>(ds_factory: DSF, cache: impl TestCache, gap_limits: GapLimits)
1164where
1165    DSF: DataStoreFactory,
1166    <DSF as DataStoreFactory>::AccountId: std::fmt::Debug,
1167{
1168    let mut st = TestBuilder::new()
1169        .with_data_store_factory(ds_factory)
1170        .with_block_cache(cache)
1171        .with_gap_limits(gap_limits)
1172        .with_account_from_sapling_activation(BlockHash([0; 32]))
1173        .build();
1174
1175    let test_account = st.test_account().cloned().unwrap();
1176    let account_uuid = test_account.account().id();
1177    let ufvk = test_account.account().ufvk().unwrap().clone();
1178
1179    let external_taddrs = st
1180        .wallet()
1181        .get_transparent_receivers(account_uuid, false, true)
1182        .unwrap();
1183    assert_eq!(
1184        u32::try_from(external_taddrs.len()).unwrap(),
1185        gap_limits.external()
1186    );
1187    let internal_taddrs = st
1188        .wallet()
1189        .get_transparent_receivers(account_uuid, true, false)
1190        .unwrap();
1191    assert_eq!(
1192        u32::try_from(internal_taddrs.len()).unwrap(),
1193        gap_limits.external() + gap_limits.internal()
1194    );
1195    let ephemeral_taddrs = st
1196        .wallet()
1197        .get_known_ephemeral_addresses(account_uuid, None)
1198        .unwrap();
1199    assert_eq!(
1200        u32::try_from(ephemeral_taddrs.len()).unwrap(),
1201        gap_limits.ephemeral()
1202    );
1203
1204    // Add some funds to the wallet
1205    let (h0, _, _) = st.generate_next_block(
1206        &ufvk.sapling().unwrap(),
1207        AddressType::DefaultExternal,
1208        Zatoshis::const_from_u64(1000000),
1209    );
1210    st.scan_cached_blocks(h0, 1);
1211
1212    // The previous operation was shielded-only, but unified address usage within the
1213    // valid transparent child index range still count towards the gap limit, so this
1214    // updates the gap limit by the index of the default Sapling receiver
1215    let external_taddrs = st
1216        .wallet()
1217        .get_transparent_receivers(account_uuid, false, true)
1218        .unwrap();
1219    assert_eq!(
1220        u32::try_from(external_taddrs.len()).unwrap(),
1221        gap_limits.external()
1222            + (u32::try_from(ufvk.sapling().unwrap().default_address().0).unwrap() + 1)
1223    );
1224
1225    // Pick an address half way through the set of external taddrs
1226    let external_taddrs_sorted = external_taddrs
1227        .into_iter()
1228        .filter_map(|(addr, meta)| meta.address_index().map(|i| (i, addr)))
1229        .collect::<BTreeMap<_, _>>();
1230    let to = Address::from(
1231        *external_taddrs_sorted
1232            .get(&transparent::keys::NonHardenedChildIndex::from_index(4).unwrap())
1233            .expect("An address exists at index 4."),
1234    )
1235    .to_zcash_address(st.network());
1236
1237    // Create a transaction & scan the block. Since the txid corresponds to one our wallet
1238    // generated, this should cause the gap limit to be bumped (generating addresses with index
1239    // 10..15)
1240    let txids = st
1241        .create_standard_transaction(&test_account, to, Zatoshis::const_from_u64(20000))
1242        .unwrap();
1243    let (h1, _) = st.generate_next_block_including(txids.head);
1244
1245    // At this point, the transaction has been created, but since it has not been mined it does
1246    // not cause an update to the gap limit; we have to wait for the transaction to actually be
1247    // mined or we could bump the gap limit too soon and start generating addresses that will
1248    // never be inspected on wallet recovery.
1249    let external_taddrs = st
1250        .wallet()
1251        .get_transparent_receivers(account_uuid, false, true)
1252        .unwrap();
1253    assert_eq!(
1254        u32::try_from(external_taddrs.len()).unwrap(),
1255        gap_limits.external()
1256            + (u32::try_from(ufvk.sapling().unwrap().default_address().0).unwrap() + 1)
1257    );
1258
1259    // Mine the block, then use `decrypt_and_store_transaction` to ensure that the wallet sees
1260    // the transaction as mined (since transparent handling doesn't get this from
1261    // `scan_cached_blocks`)
1262    st.scan_cached_blocks(h1, 1);
1263    let tx = st.wallet().get_transaction(txids.head).unwrap().unwrap();
1264    decrypt_and_store_transaction(&st.network().clone(), st.wallet_mut(), &tx, Some(h1)).unwrap();
1265
1266    // Now that the transaction has been mined, the gap limit should have increased.
1267    let external_taddrs = st
1268        .wallet()
1269        .get_transparent_receivers(account_uuid, false, true)
1270        .unwrap();
1271    assert_eq!(
1272        u32::try_from(external_taddrs.len()).unwrap(),
1273        gap_limits.external() + 5
1274    );
1275
1276    // The utxo query height should be equal to the minimum mined height among transactions
1277    // sent to any of the set of {addresses in the gap limit range | address prior to the gap}.
1278    let query_height = st.wallet().utxo_query_height(account_uuid).unwrap();
1279    assert_eq!(query_height, h0);
1280}
1281
1282/// Builds a test 1-of-1 multisig redeem script from a single keypair.
1283#[cfg(feature = "transparent-key-import")]
1284fn build_test_redeem_script() -> (script::Redeem, secp256k1::SecretKey) {
1285    let secret_key = SecretKey::from_secret_bytes([1u8; 32]).expect("valid secret key");
1286    let pubkey = secret_key.public_key();
1287    let redeem_script = script::Component(
1288        check_multisig(1, &[&pubkey.serialize()], false)
1289            .unwrap()
1290            .into_iter()
1291            .collect(),
1292    );
1293    (redeem_script, secret_key)
1294}
1295
1296/// Tests that importing standalone transparent addresses without key material succeeds and
1297/// the addresses appear in `get_transparent_receivers` with the
1298/// [`TransparentAddressSource::StandaloneAddress`] source.
1299#[cfg(feature = "transparent-key-import")]
1300pub fn import_standalone_transparent_address<DSF>(dsf: DSF)
1301where
1302    DSF: DataStoreFactory,
1303{
1304    let mut st = TestBuilder::new()
1305        .with_data_store_factory(dsf)
1306        .with_account_from_sapling_activation(BlockHash([0; 32]))
1307        .build();
1308
1309    let account_id = st.test_account().unwrap().id();
1310
1311    // A P2PKH address, imported without its pubkey.
1312    let secret_key = SecretKey::from_secret_bytes([1u8; 32]).expect("valid secret key");
1313    let p2pkh_addr = TransparentAddress::from_pubkey(&secret_key.public_key());
1314    assert_matches!(
1315        st.wallet_mut()
1316            .import_standalone_transparent_address(account_id, p2pkh_addr),
1317        Ok(_)
1318    );
1319
1320    // A P2SH address, imported without its redeem script.
1321    let (redeem_script, _) = build_test_redeem_script();
1322    let p2sh_addr =
1323        TransparentAddress::from_script_pubkey(&sh(&redeem_script)).expect("valid P2SH address");
1324    assert_matches!(
1325        st.wallet_mut()
1326            .import_standalone_transparent_address(account_id, p2sh_addr),
1327        Ok(_)
1328    );
1329
1330    let receivers = st
1331        .wallet()
1332        .get_transparent_receivers(account_id, false, true)
1333        .unwrap();
1334    for addr in [p2pkh_addr, p2sh_addr] {
1335        let metadata = receivers.get(&addr).expect("address should be present");
1336        assert!(matches!(
1337            metadata.source(),
1338            TransparentAddressSource::StandaloneAddress
1339        ));
1340    }
1341}
1342
1343/// Tests that importing the same standalone address twice to the same account is idempotent.
1344#[cfg(feature = "transparent-key-import")]
1345pub fn import_standalone_transparent_address_idempotent<DSF>(dsf: DSF)
1346where
1347    DSF: DataStoreFactory,
1348{
1349    let mut st = TestBuilder::new()
1350        .with_data_store_factory(dsf)
1351        .with_account_from_sapling_activation(BlockHash([0; 32]))
1352        .build();
1353
1354    let account_id = st.test_account().unwrap().id();
1355
1356    let secret_key = SecretKey::from_secret_bytes([1u8; 32]).expect("valid secret key");
1357    let taddr = TransparentAddress::from_pubkey(&secret_key.public_key());
1358
1359    assert_matches!(
1360        st.wallet_mut()
1361            .import_standalone_transparent_address(account_id, taddr),
1362        Ok(_)
1363    );
1364
1365    let receivers_before = st
1366        .wallet()
1367        .get_transparent_receivers(account_id, false, true)
1368        .unwrap();
1369
1370    // Second import to the same account should also succeed (idempotent).
1371    assert_matches!(
1372        st.wallet_mut()
1373            .import_standalone_transparent_address(account_id, taddr),
1374        Ok(_)
1375    );
1376
1377    let receivers_after = st
1378        .wallet()
1379        .get_transparent_receivers(account_id, false, true)
1380        .unwrap();
1381    assert_eq!(receivers_before.len(), receivers_after.len());
1382}
1383
1384/// Tests that importing the same standalone address to a different account fails.
1385#[cfg(feature = "transparent-key-import")]
1386pub fn import_standalone_transparent_address_conflict<DSF>(dsf: DSF)
1387where
1388    DSF: DataStoreFactory,
1389{
1390    let mut st = TestBuilder::new()
1391        .with_data_store_factory(dsf)
1392        .with_account_from_sapling_activation(BlockHash([0; 32]))
1393        .build();
1394
1395    let account1_id = st.test_account().unwrap().id();
1396
1397    let secret_key = SecretKey::from_secret_bytes([1u8; 32]).expect("valid secret key");
1398    let taddr = TransparentAddress::from_pubkey(&secret_key.public_key());
1399
1400    assert_matches!(
1401        st.wallet_mut()
1402            .import_standalone_transparent_address(account1_id, taddr),
1403        Ok(_)
1404    );
1405
1406    // Create a second account
1407    let birthday = AccountBirthday::from_parts(
1408        ChainState::empty(
1409            st.network()
1410                .activation_height(NetworkUpgrade::Sapling)
1411                .unwrap()
1412                - 1,
1413            BlockHash([0; 32]),
1414        ),
1415        None,
1416    );
1417    let seed2 = Secret::new(vec![42u8; 32]);
1418    let (account2_id, _) = st
1419        .wallet_mut()
1420        .create_account("account2", &seed2, &birthday, None)
1421        .unwrap();
1422
1423    // Import of the same address to the second account should fail
1424    assert_matches!(
1425        st.wallet_mut()
1426            .import_standalone_transparent_address(account2_id, taddr),
1427        Err(_)
1428    );
1429}
1430
1431/// Tests that a UTXO received at an address imported without key material is reflected in the
1432/// wallet balance, but is not offered as a spendable output.
1433#[cfg(feature = "transparent-key-import")]
1434pub fn import_standalone_transparent_address_balance<DSF>(dsf: DSF)
1435where
1436    DSF: DataStoreFactory,
1437    <<DSF as DataStoreFactory>::DataStore as WalletWrite>::UtxoRef: std::fmt::Debug,
1438{
1439    let mut st = TestBuilder::new()
1440        .with_data_store_factory(dsf)
1441        .with_account_from_sapling_activation(BlockHash([0; 32]))
1442        .build();
1443
1444    let account_id = st.test_account().unwrap().id();
1445    let birthday = st.test_account().unwrap().birthday().height();
1446
1447    let secret_key = SecretKey::from_secret_bytes([1u8; 32]).expect("valid secret key");
1448    let taddr = TransparentAddress::from_pubkey(&secret_key.public_key());
1449
1450    // Import the address without its pubkey.
1451    st.wallet_mut()
1452        .import_standalone_transparent_address(account_id, taddr)
1453        .unwrap();
1454
1455    let height = birthday + 1000;
1456    st.wallet_mut().update_chain_tip(height).unwrap();
1457
1458    // Create a fake UTXO at the address.
1459    let value = Zatoshis::const_from_u64(50_000);
1460    let outpoint = OutPoint::fake();
1461    let txout = TxOut::new(value, taddr.script().into());
1462    let utxo = WalletTransparentOutput::from_parts(
1463        outpoint,
1464        txout,
1465        Some(height),
1466        Some(account_id),
1467        None,
1468        None,
1469    )
1470    .unwrap();
1471    st.wallet_mut()
1472        .put_received_transparent_utxo(&utxo)
1473        .unwrap();
1474
1475    // Verify the balance is reflected via get_transparent_balances.
1476    let target_height = TargetHeight::from(height + 1);
1477    let balances = st
1478        .wallet()
1479        .get_transparent_balances(account_id, target_height, ConfirmationsPolicy::MIN)
1480        .unwrap();
1481    assert_eq!(balances.get(&taddr).map(|(_, b)| b.total()), Some(value),);
1482
1483    // The output must not be offered for spending: the wallet holds no key material with
1484    // which a spend could be constructed.
1485    let utxos = st
1486        .wallet()
1487        .get_spendable_transparent_outputs(
1488            &taddr,
1489            target_height,
1490            ConfirmationsPolicy::MIN,
1491            CoinbaseFilter::AllTransparentOutputs,
1492            LockFilter::Policy(&LockedInputPolicy::Exclude),
1493        )
1494        .unwrap();
1495    assert_eq!(utxos, vec![]);
1496}
1497
1498/// Tests that importing key material for a previously address-only import upgrades the
1499/// address in place: the pubkey (P2PKH) or redeem script (P2SH) becomes the address's
1500/// source, without a duplicate receiver appearing.
1501#[cfg(feature = "transparent-key-import")]
1502pub fn import_standalone_transparent_address_upgrade<DSF>(dsf: DSF)
1503where
1504    DSF: DataStoreFactory,
1505{
1506    let mut st = TestBuilder::new()
1507        .with_data_store_factory(dsf)
1508        .with_account_from_sapling_activation(BlockHash([0; 32]))
1509        .build();
1510
1511    let account_id = st.test_account().unwrap().id();
1512
1513    let secret_key = SecretKey::from_secret_bytes([1u8; 32]).expect("valid secret key");
1514    let pubkey = secret_key.public_key();
1515    let p2pkh_addr = TransparentAddress::from_pubkey(&pubkey);
1516
1517    let (redeem_script, _) = build_test_redeem_script();
1518    let p2sh_addr =
1519        TransparentAddress::from_script_pubkey(&sh(&redeem_script)).expect("valid P2SH address");
1520
1521    // Import both addresses without key material.
1522    st.wallet_mut()
1523        .import_standalone_transparent_address(account_id, p2pkh_addr)
1524        .unwrap();
1525    st.wallet_mut()
1526        .import_standalone_transparent_address(account_id, p2sh_addr)
1527        .unwrap();
1528
1529    let receivers_before = st
1530        .wallet()
1531        .get_transparent_receivers(account_id, false, true)
1532        .unwrap();
1533
1534    // Import the key material for each address.
1535    st.wallet_mut()
1536        .import_standalone_transparent_pubkey(account_id, pubkey)
1537        .unwrap();
1538    st.wallet_mut()
1539        .import_standalone_transparent_script(account_id, redeem_script)
1540        .unwrap();
1541
1542    // The addresses were upgraded in place: no new receivers, and each address's source
1543    // now reflects the imported key material.
1544    let receivers_after = st
1545        .wallet()
1546        .get_transparent_receivers(account_id, false, true)
1547        .unwrap();
1548    assert_eq!(receivers_before.len(), receivers_after.len());
1549
1550    let p2pkh_metadata = receivers_after
1551        .get(&p2pkh_addr)
1552        .expect("address should be present");
1553    assert!(matches!(
1554        p2pkh_metadata.source(),
1555        TransparentAddressSource::StandalonePubkey(_)
1556    ));
1557
1558    let p2sh_metadata = receivers_after
1559        .get(&p2sh_addr)
1560        .expect("address should be present");
1561    assert!(matches!(
1562        p2sh_metadata.source(),
1563        TransparentAddressSource::StandaloneScript(_)
1564    ));
1565}
1566
1567/// Tests that importing a standalone transparent public key succeeds.
1568#[cfg(feature = "transparent-key-import")]
1569pub fn import_standalone_transparent_pubkey<DSF>(dsf: DSF)
1570where
1571    DSF: DataStoreFactory,
1572{
1573    let mut st = TestBuilder::new()
1574        .with_data_store_factory(dsf)
1575        .with_account_from_sapling_activation(BlockHash([0; 32]))
1576        .build();
1577
1578    let account_id = st.test_account().unwrap().id();
1579
1580    let secret_key = SecretKey::from_secret_bytes([1u8; 32]).expect("valid secret key");
1581    let pubkey = secret_key.public_key();
1582    assert_matches!(
1583        st.wallet_mut()
1584            .import_standalone_transparent_pubkey(account_id, pubkey),
1585        Ok(_)
1586    );
1587}
1588
1589/// Tests that importing the same pubkey twice to the same account is idempotent.
1590#[cfg(feature = "transparent-key-import")]
1591pub fn import_standalone_transparent_pubkey_idempotent<DSF>(dsf: DSF)
1592where
1593    DSF: DataStoreFactory,
1594{
1595    let mut st = TestBuilder::new()
1596        .with_data_store_factory(dsf)
1597        .with_account_from_sapling_activation(BlockHash([0; 32]))
1598        .build();
1599
1600    let account_id = st.test_account().unwrap().id();
1601
1602    let secret_key = SecretKey::from_secret_bytes([1u8; 32]).expect("valid secret key");
1603    let pubkey = secret_key.public_key();
1604
1605    // First import
1606    assert_matches!(
1607        st.wallet_mut()
1608            .import_standalone_transparent_pubkey(account_id, pubkey),
1609        Ok(_)
1610    );
1611
1612    // Snapshot state after first import
1613    let receivers_before = st
1614        .wallet()
1615        .get_transparent_receivers(account_id, false, true)
1616        .unwrap();
1617
1618    // Second import to same account should also succeed (idempotent)
1619    assert_matches!(
1620        st.wallet_mut()
1621            .import_standalone_transparent_pubkey(account_id, pubkey),
1622        Ok(_)
1623    );
1624
1625    // Verify wallet state is unchanged
1626    let receivers_after = st
1627        .wallet()
1628        .get_transparent_receivers(account_id, false, true)
1629        .unwrap();
1630
1631    assert_eq!(receivers_before.len(), receivers_after.len());
1632
1633    let taddr = TransparentAddress::from_pubkey(&pubkey);
1634    let metadata = receivers_after
1635        .get(&taddr)
1636        .expect("address should be present");
1637    assert!(matches!(
1638        metadata.source(),
1639        TransparentAddressSource::StandalonePubkey(_)
1640    ));
1641}
1642
1643/// Tests that importing the same pubkey to a different account fails.
1644#[cfg(feature = "transparent-key-import")]
1645pub fn import_standalone_transparent_pubkey_conflict<DSF>(dsf: DSF)
1646where
1647    DSF: DataStoreFactory,
1648{
1649    let mut st = TestBuilder::new()
1650        .with_data_store_factory(dsf)
1651        .with_account_from_sapling_activation(BlockHash([0; 32]))
1652        .build();
1653
1654    let account1_id = st.test_account().unwrap().id();
1655
1656    let secret_key = SecretKey::from_secret_bytes([1u8; 32]).expect("valid secret key");
1657    let pubkey = secret_key.public_key();
1658
1659    // Import to first account
1660    assert_matches!(
1661        st.wallet_mut()
1662            .import_standalone_transparent_pubkey(account1_id, pubkey),
1663        Ok(_)
1664    );
1665
1666    // Create a second account
1667    let birthday = AccountBirthday::from_parts(
1668        ChainState::empty(
1669            st.network()
1670                .activation_height(NetworkUpgrade::Sapling)
1671                .unwrap()
1672                - 1,
1673            BlockHash([0; 32]),
1674        ),
1675        None,
1676    );
1677    let seed2 = Secret::new(vec![42u8; 32]);
1678    let (account2_id, _) = st
1679        .wallet_mut()
1680        .create_account("account2", &seed2, &birthday, None)
1681        .unwrap();
1682
1683    // Import same pubkey to second account should fail
1684    assert_matches!(
1685        st.wallet_mut()
1686            .import_standalone_transparent_pubkey(account2_id, pubkey),
1687        Err(_)
1688    );
1689}
1690
1691/// Tests that a UTXO received at a standalone P2PKH address is reflected in the wallet balance.
1692#[cfg(feature = "transparent-key-import")]
1693pub fn import_standalone_transparent_pubkey_balance<DSF>(dsf: DSF)
1694where
1695    DSF: DataStoreFactory,
1696    <<DSF as DataStoreFactory>::DataStore as WalletWrite>::UtxoRef: std::fmt::Debug,
1697{
1698    let mut st = TestBuilder::new()
1699        .with_data_store_factory(dsf)
1700        .with_account_from_sapling_activation(BlockHash([0; 32]))
1701        .build();
1702
1703    let account_id = st.test_account().unwrap().id();
1704    let birthday = st.test_account().unwrap().birthday().height();
1705
1706    let secret_key = SecretKey::from_secret_bytes([1u8; 32]).expect("valid secret key");
1707    let pubkey = secret_key.public_key();
1708
1709    // Import the public key.
1710    st.wallet_mut()
1711        .import_standalone_transparent_pubkey(account_id, pubkey)
1712        .unwrap();
1713
1714    // Derive the P2PKH address.
1715    let taddr = TransparentAddress::from_pubkey(&pubkey);
1716
1717    let height = birthday + 1000;
1718    st.wallet_mut().update_chain_tip(height).unwrap();
1719
1720    // Create a fake UTXO at the P2PKH address.
1721    let value = Zatoshis::const_from_u64(50_000);
1722    let outpoint = OutPoint::fake();
1723    let txout = TxOut::new(value, taddr.script().into());
1724    let utxo = WalletTransparentOutput::from_parts(
1725        outpoint,
1726        txout,
1727        Some(height),
1728        Some(account_id),
1729        None,
1730        None,
1731    )
1732    .unwrap();
1733    st.wallet_mut()
1734        .put_received_transparent_utxo(&utxo)
1735        .unwrap();
1736
1737    // Verify the balance is reflected via get_transparent_balances.
1738    let target_height = TargetHeight::from(height + 1);
1739    let balances = st
1740        .wallet()
1741        .get_transparent_balances(account_id, target_height, ConfirmationsPolicy::MIN)
1742        .unwrap();
1743    assert_eq!(
1744        balances.get(&taddr).map(|(_, b)| b.spendable_value()),
1745        Some(value),
1746    );
1747
1748    // Verify the UTXO is returned by get_spendable_transparent_outputs.
1749    let utxos = st
1750        .wallet()
1751        .get_spendable_transparent_outputs(
1752            &taddr,
1753            target_height,
1754            ConfirmationsPolicy::MIN,
1755            CoinbaseFilter::AllTransparentOutputs,
1756            LockFilter::Policy(&LockedInputPolicy::Exclude),
1757        )
1758        .unwrap();
1759    assert_eq!(utxos.len(), 1);
1760    assert_eq!(utxos[0].value(), value);
1761}
1762
1763/// Tests spending from a standalone P2PKH address by shielding its balance.
1764#[cfg(feature = "transparent-key-import")]
1765pub fn spend_from_standalone_pubkey<DSF>(dsf: DSF, cache: impl TestCache)
1766where
1767    DSF: DataStoreFactory,
1768{
1769    let mut st = TestBuilder::new()
1770        .with_data_store_factory(dsf)
1771        .with_block_cache(cache)
1772        .with_account_from_sapling_activation(BlockHash([0; 32]))
1773        .build();
1774
1775    let account = st.test_account().cloned().unwrap();
1776    let account_id = account.id();
1777
1778    // Create a keypair and derive the P2PKH address.
1779    let secret_key = SecretKey::from_secret_bytes([1u8; 32]).expect("valid secret key");
1780    let pubkey = secret_key.public_key();
1781
1782    // Import the public key.
1783    st.wallet_mut()
1784        .import_standalone_transparent_pubkey(account_id, pubkey)
1785        .unwrap();
1786
1787    // Derive the P2PKH address.
1788    let taddr = TransparentAddress::from_pubkey(&pubkey);
1789
1790    // Initialize chain data with blocks (needed for shielding transaction creation).
1791    let not_our_key = ExtendedSpendingKey::master(&[])
1792        .expect("the derivation path yields a valid key")
1793        .to_diversifiable_full_viewing_key();
1794    let not_our_value = Zatoshis::const_from_u64(10000);
1795    let (start_height, _, _) =
1796        st.generate_next_block(&not_our_key, AddressType::DefaultExternal, not_our_value);
1797    for _ in 1..10 {
1798        st.generate_next_block(&not_our_key, AddressType::DefaultExternal, not_our_value);
1799    }
1800    st.scan_cached_blocks(start_height, 10);
1801
1802    // Create a fake UTXO at the P2PKH address.
1803    let value = Zatoshis::from_u64(100000).unwrap();
1804    let height = st.wallet().chain_height().unwrap().unwrap();
1805    let txout = TxOut::new(value, taddr.script().into());
1806    let utxo = WalletTransparentOutput::from_parts(
1807        OutPoint::fake(),
1808        txout,
1809        Some(height),
1810        Some(account_id),
1811        None,
1812        None,
1813    )
1814    .unwrap();
1815    st.wallet_mut()
1816        .put_received_transparent_utxo(&utxo)
1817        .unwrap();
1818
1819    // Build SpendingKeys with the standalone key for the P2PKH address.
1820    let mut standalone_keys = HashMap::new();
1821    standalone_keys.insert(taddr, vec![secret_key]);
1822    let spending_keys = SpendingKeys::new(
1823        account.usk().clone(),
1824        #[cfg(feature = "transparent-key-import")]
1825        standalone_keys,
1826    );
1827
1828    // Shield the P2PKH UTXO.
1829    let input_selector = GreedyInputSelector::new();
1830    let change_strategy =
1831        single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Sapling);
1832
1833    let prover = ::zcash_proofs::prover::LocalTxProver::bundled();
1834    let network = *st.network();
1835    let clock = st.clock().clone();
1836    let txids = wallet::shield_transparent_funds(
1837        st.wallet_mut(),
1838        &network,
1839        &clock,
1840        &mut UnwrapErr(SysRng),
1841        &prover,
1842        &prover,
1843        &input_selector,
1844        &change_strategy,
1845        value,
1846        &spending_keys,
1847        &[taddr],
1848        account_id,
1849        ConfirmationsPolicy::MIN,
1850    )
1851    .unwrap();
1852
1853    assert!(!txids.is_empty());
1854
1855    // The wallet should have zero transparent balance after shielding.
1856    check_balance::<DSF>(
1857        &st,
1858        &account,
1859        &taddr,
1860        ConfirmationsPolicy::MIN,
1861        &Balance::ZERO,
1862    );
1863
1864    // The shielded balance should now include the value minus the fee.
1865    let fee = st
1866        .get_tx_from_history(*txids.first())
1867        .unwrap()
1868        .unwrap()
1869        .fee_paid()
1870        .expect("fee should be known for wallet-created transactions");
1871    let summary = st
1872        .wallet()
1873        .get_wallet_summary(ConfirmationsPolicy::MIN)
1874        .unwrap()
1875        .unwrap();
1876    let account_balance = summary.account_balances().get(&account_id).unwrap();
1877    assert_eq!(
1878        account_balance
1879            .sapling_balance()
1880            .change_pending_confirmation(),
1881        (value - fee).unwrap(),
1882    );
1883}
1884
1885/// Tests that importing a standalone P2SH address succeeds and the address appears
1886/// in `get_transparent_receivers` with the correct `TransparentAddressSource`.
1887#[cfg(feature = "transparent-key-import")]
1888pub fn import_standalone_transparent_p2sh<DSF>(dsf: DSF)
1889where
1890    DSF: DataStoreFactory,
1891{
1892    let mut st = TestBuilder::new()
1893        .with_data_store_factory(dsf)
1894        .with_account_from_sapling_activation(BlockHash([0; 32]))
1895        .build();
1896
1897    let account_id = st.test_account().unwrap().id();
1898    let (redeem_script, _) = build_test_redeem_script();
1899
1900    // Import should succeed
1901    assert_matches!(
1902        st.wallet_mut()
1903            .import_standalone_transparent_script(account_id, redeem_script.clone()),
1904        Ok(_)
1905    );
1906
1907    // Verify the address appears in get_transparent_receivers
1908    let receivers = st
1909        .wallet()
1910        .get_transparent_receivers(account_id, false, true)
1911        .unwrap();
1912
1913    // The P2SH address derived from the redeem script should be present
1914    let script_pubkey = sh(&redeem_script);
1915    let expected_addr =
1916        TransparentAddress::from_script_pubkey(&script_pubkey).expect("valid P2SH address");
1917
1918    let metadata = receivers
1919        .get(&expected_addr)
1920        .expect("address should be present");
1921    assert!(matches!(
1922        metadata.source(),
1923        TransparentAddressSource::StandaloneScript(_)
1924    ));
1925}
1926
1927/// Tests that importing the same P2SH address twice to the same account is idempotent.
1928#[cfg(feature = "transparent-key-import")]
1929pub fn import_standalone_transparent_p2sh_idempotent<DSF>(dsf: DSF)
1930where
1931    DSF: DataStoreFactory,
1932{
1933    let mut st = TestBuilder::new()
1934        .with_data_store_factory(dsf)
1935        .with_account_from_sapling_activation(BlockHash([0; 32]))
1936        .build();
1937
1938    let account_id = st.test_account().unwrap().id();
1939    let (redeem_script, _) = build_test_redeem_script();
1940
1941    // First import
1942    assert_matches!(
1943        st.wallet_mut()
1944            .import_standalone_transparent_script(account_id, redeem_script.clone()),
1945        Ok(_)
1946    );
1947
1948    // Snapshot state after first import
1949    let receivers_before = st
1950        .wallet()
1951        .get_transparent_receivers(account_id, false, true)
1952        .unwrap();
1953
1954    // Second import to same account should also succeed (idempotent)
1955    assert_matches!(
1956        st.wallet_mut()
1957            .import_standalone_transparent_script(account_id, redeem_script.clone()),
1958        Ok(_)
1959    );
1960
1961    // Verify wallet state is unchanged
1962    let receivers_after = st
1963        .wallet()
1964        .get_transparent_receivers(account_id, false, true)
1965        .unwrap();
1966
1967    assert_eq!(receivers_before.len(), receivers_after.len());
1968
1969    let script_pubkey = sh(&redeem_script);
1970    let expected_addr =
1971        TransparentAddress::from_script_pubkey(&script_pubkey).expect("valid P2SH address");
1972    let metadata = receivers_after
1973        .get(&expected_addr)
1974        .expect("address should be present");
1975    assert!(matches!(
1976        metadata.source(),
1977        TransparentAddressSource::StandaloneScript(_)
1978    ));
1979}
1980
1981/// Tests that importing the same P2SH address to a different account fails.
1982#[cfg(feature = "transparent-key-import")]
1983pub fn import_standalone_transparent_p2sh_conflict<DSF>(dsf: DSF)
1984where
1985    DSF: DataStoreFactory,
1986{
1987    let mut st = TestBuilder::new()
1988        .with_data_store_factory(dsf)
1989        .with_account_from_sapling_activation(BlockHash([0; 32]))
1990        .build();
1991
1992    let account1_id = st.test_account().unwrap().id();
1993    let (redeem_script, _) = build_test_redeem_script();
1994
1995    // Import to first account
1996    assert_matches!(
1997        st.wallet_mut()
1998            .import_standalone_transparent_script(account1_id, redeem_script.clone()),
1999        Ok(_)
2000    );
2001
2002    // Create a second account
2003    let birthday = AccountBirthday::from_parts(
2004        ChainState::empty(
2005            st.network()
2006                .activation_height(NetworkUpgrade::Sapling)
2007                .unwrap()
2008                - 1,
2009            BlockHash([0; 32]),
2010        ),
2011        None,
2012    );
2013    let seed2 = Secret::new(vec![42u8; 32]);
2014    let (account2_id, _) = st
2015        .wallet_mut()
2016        .create_account("account2", &seed2, &birthday, None)
2017        .unwrap();
2018
2019    // Import same redeem script to second account should fail
2020    assert_matches!(
2021        st.wallet_mut()
2022            .import_standalone_transparent_script(account2_id, redeem_script),
2023        Err(_)
2024    );
2025}
2026
2027/// Tests that a UTXO received at a standalone P2SH address is reflected in the wallet balance.
2028#[cfg(feature = "transparent-key-import")]
2029pub fn import_standalone_transparent_p2sh_balance<DSF>(dsf: DSF)
2030where
2031    DSF: DataStoreFactory,
2032    <<DSF as DataStoreFactory>::DataStore as WalletWrite>::UtxoRef: std::fmt::Debug,
2033{
2034    let mut st = TestBuilder::new()
2035        .with_data_store_factory(dsf)
2036        .with_account_from_sapling_activation(BlockHash([0; 32]))
2037        .build();
2038
2039    let account_id = st.test_account().unwrap().id();
2040    let birthday = st.test_account().unwrap().birthday().height();
2041
2042    let (redeem_script, _) = build_test_redeem_script();
2043
2044    // Import the P2SH address.
2045    st.wallet_mut()
2046        .import_standalone_transparent_script(account_id, redeem_script.clone())
2047        .unwrap();
2048
2049    // Derive the expected transparent address from the redeem script.
2050    let script_pubkey = sh(&redeem_script);
2051    let taddr = TransparentAddress::from_script_pubkey(&script_pubkey).expect("valid P2SH address");
2052
2053    let height = birthday + 1000;
2054    st.wallet_mut().update_chain_tip(height).unwrap();
2055
2056    // Create a fake UTXO at the P2SH address.
2057    let value = Zatoshis::const_from_u64(50_000);
2058    let outpoint = OutPoint::fake();
2059    let txout = TxOut::new(value, taddr.script().into());
2060    let utxo = WalletTransparentOutput::from_parts(
2061        outpoint,
2062        txout,
2063        Some(height),
2064        Some(account_id),
2065        None,
2066        None,
2067    )
2068    .unwrap();
2069    st.wallet_mut()
2070        .put_received_transparent_utxo(&utxo)
2071        .unwrap();
2072
2073    // Verify the balance is reflected via get_transparent_balances.
2074    let target_height = TargetHeight::from(height + 1);
2075    let balances = st
2076        .wallet()
2077        .get_transparent_balances(account_id, target_height, ConfirmationsPolicy::MIN)
2078        .unwrap();
2079    assert_eq!(
2080        balances.get(&taddr).map(|(_, b)| b.spendable_value()),
2081        Some(value),
2082    );
2083
2084    // Verify the UTXO is returned by get_spendable_transparent_outputs.
2085    let utxos = st
2086        .wallet()
2087        .get_spendable_transparent_outputs(
2088            &taddr,
2089            target_height,
2090            ConfirmationsPolicy::MIN,
2091            CoinbaseFilter::AllTransparentOutputs,
2092            LockFilter::Policy(&LockedInputPolicy::Exclude),
2093        )
2094        .unwrap();
2095    assert_eq!(utxos.len(), 1);
2096    assert_eq!(utxos[0].value(), value);
2097}
2098
2099/// Tests spending from a standalone P2SH (multisig) address by shielding its balance.
2100#[cfg(feature = "transparent-key-import")]
2101pub fn spend_from_standalone_p2sh<DSF>(dsf: DSF, cache: impl TestCache)
2102where
2103    DSF: DataStoreFactory,
2104{
2105    let mut st = TestBuilder::new()
2106        .with_data_store_factory(dsf)
2107        .with_block_cache(cache)
2108        .with_account_from_sapling_activation(BlockHash([0; 32]))
2109        .build();
2110
2111    let account = st.test_account().cloned().unwrap();
2112    let account_id = account.id();
2113    // Build the redeem script and get the signing key.
2114    let (redeem_script, secret_key) = build_test_redeem_script();
2115
2116    // Import the P2SH address.
2117    st.wallet_mut()
2118        .import_standalone_transparent_script(account_id, redeem_script.clone())
2119        .unwrap();
2120
2121    // Derive the P2SH address.
2122    let script_pubkey = sh(&redeem_script);
2123    let taddr = TransparentAddress::from_script_pubkey(&script_pubkey).expect("valid P2SH address");
2124
2125    // Initialize chain data with blocks (needed for shielding transaction creation).
2126    let not_our_key = ExtendedSpendingKey::master(&[])
2127        .expect("the derivation path yields a valid key")
2128        .to_diversifiable_full_viewing_key();
2129    let not_our_value = Zatoshis::const_from_u64(10000);
2130    let (start_height, _, _) =
2131        st.generate_next_block(&not_our_key, AddressType::DefaultExternal, not_our_value);
2132    for _ in 1..10 {
2133        st.generate_next_block(&not_our_key, AddressType::DefaultExternal, not_our_value);
2134    }
2135    st.scan_cached_blocks(start_height, 10);
2136
2137    // Create a fake UTXO at the P2SH address.
2138    let value = Zatoshis::from_u64(100000).unwrap();
2139    let height = st.wallet().chain_height().unwrap().unwrap();
2140    let txout = TxOut::new(value, taddr.script().into());
2141    let utxo = WalletTransparentOutput::from_parts(
2142        OutPoint::fake(),
2143        txout,
2144        Some(height),
2145        Some(account_id),
2146        None,
2147        None,
2148    )
2149    .unwrap();
2150    st.wallet_mut()
2151        .put_received_transparent_utxo(&utxo)
2152        .unwrap();
2153
2154    // Build SpendingKeys with the standalone key for the P2SH address.
2155    let mut standalone_keys = HashMap::new();
2156    standalone_keys.insert(taddr, vec![secret_key]);
2157    let spending_keys = SpendingKeys::new(
2158        account.usk().clone(),
2159        #[cfg(feature = "transparent-key-import")]
2160        standalone_keys,
2161    );
2162
2163    // Shield the P2SH UTXO.
2164    let input_selector = GreedyInputSelector::new();
2165    let change_strategy =
2166        single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Sapling);
2167
2168    let prover = ::zcash_proofs::prover::LocalTxProver::bundled();
2169    let network = *st.network();
2170    let clock = st.clock().clone();
2171    let txids = wallet::shield_transparent_funds(
2172        st.wallet_mut(),
2173        &network,
2174        &clock,
2175        &mut UnwrapErr(SysRng),
2176        &prover,
2177        &prover,
2178        &input_selector,
2179        &change_strategy,
2180        value,
2181        &spending_keys,
2182        &[taddr],
2183        account_id,
2184        ConfirmationsPolicy::MIN,
2185    )
2186    .unwrap();
2187
2188    assert!(!txids.is_empty());
2189
2190    // The wallet should have zero transparent balance after shielding.
2191    check_balance::<DSF>(
2192        &st,
2193        &account,
2194        &taddr,
2195        ConfirmationsPolicy::MIN,
2196        &Balance::ZERO,
2197    );
2198
2199    // The shielded balance should now include the value minus the fee.
2200    let fee = st
2201        .get_tx_from_history(*txids.first())
2202        .unwrap()
2203        .unwrap()
2204        .fee_paid()
2205        .expect("fee should be known for wallet-created transactions");
2206    let summary = st
2207        .wallet()
2208        .get_wallet_summary(ConfirmationsPolicy::MIN)
2209        .unwrap()
2210        .unwrap();
2211    let account_balance = summary.account_balances().get(&account_id).unwrap();
2212    assert_eq!(
2213        account_balance
2214            .sapling_balance()
2215            .change_pending_confirmation(),
2216        (value - fee).unwrap(),
2217    );
2218}
2219
2220/// Tests [`WalletWrite::mark_transparent_addresses_exposed`] by observing the effect on the
2221/// address's exposure metadata via
2222/// [`WalletRead::get_transparent_address_metadata`](crate::data_api::WalletRead::get_transparent_address_metadata).
2223pub fn mark_transparent_addresses_exposed<DSF>(dsf: DSF)
2224where
2225    DSF: DataStoreFactory,
2226{
2227    let mut st = TestBuilder::new()
2228        .with_data_store_factory(dsf)
2229        .with_account_from_sapling_activation(BlockHash([0; 32]))
2230        .build();
2231
2232    let account_id = st.test_account().unwrap().id();
2233    let taddr = *st
2234        .wallet()
2235        .get_last_generated_address_matching(account_id, UnifiedAddressRequest::AllAvailableKeys)
2236        .unwrap()
2237        .unwrap()
2238        .transparent()
2239        .unwrap();
2240
2241    let exposure_of = |st: &TestState<_, <DSF as DataStoreFactory>::DataStore, LocalNetwork>,
2242                       addr: &TransparentAddress|
2243     -> Exposure {
2244        st.wallet()
2245            .get_transparent_address_metadata(account_id, addr)
2246            .unwrap()
2247            .unwrap()
2248            .exposure()
2249    };
2250
2251    // Calling with a very high height does not raise an already-recorded exposure,
2252    // and records the provided height if no prior exposure was tracked.
2253    let initial = exposure_of(&st, &taddr);
2254    let very_high = BlockHeight::from(u32::MAX);
2255    st.wallet_mut()
2256        .mark_transparent_addresses_exposed(&[(taddr, very_high)])
2257        .unwrap();
2258    match initial {
2259        Exposure::Exposed { at_height, .. } => assert_matches!(
2260            exposure_of(&st, &taddr),
2261            Exposure::Exposed { at_height: h, .. } if h == at_height
2262        ),
2263        Exposure::Unknown | Exposure::CannotKnow => assert_matches!(
2264            exposure_of(&st, &taddr),
2265            Exposure::Exposed { at_height: h, .. } if h == very_high
2266        ),
2267    }
2268
2269    // Calling with a lower height lowers the recorded exposure.
2270    st.wallet_mut()
2271        .mark_transparent_addresses_exposed(&[(taddr, BlockHeight::from(0))])
2272        .unwrap();
2273    assert_matches!(
2274        exposure_of(&st, &taddr),
2275        Exposure::Exposed { at_height, .. } if at_height == BlockHeight::from(0)
2276    );
2277
2278    // Calling with a higher height does not raise the recorded exposure.
2279    st.wallet_mut()
2280        .mark_transparent_addresses_exposed(&[(taddr, BlockHeight::from(100))])
2281        .unwrap();
2282    assert_matches!(
2283        exposure_of(&st, &taddr),
2284        Exposure::Exposed { at_height, .. } if at_height == BlockHeight::from(0)
2285    );
2286
2287    // An address not tracked by the wallet must return an error.
2288    let unknown = TransparentAddress::PublicKeyHash([0u8; 20]);
2289    assert!(
2290        st.wallet_mut()
2291            .mark_transparent_addresses_exposed(&[(unknown, BlockHeight::from(1))])
2292            .is_err()
2293    );
2294
2295    // An empty input is a no-op.
2296    st.wallet_mut()
2297        .mark_transparent_addresses_exposed(&[])
2298        .unwrap();
2299}
2300
2301/// Tests that [`WalletWrite::mark_transparent_addresses_exposed`] correctly handles bulk
2302/// input: all addresses in a successful call must be marked, and an unrecognized address
2303/// must cause the entire call to be rolled back.
2304pub fn mark_transparent_addresses_exposed_bulk<DSF>(dsf: DSF)
2305where
2306    DSF: DataStoreFactory,
2307{
2308    let gap_limits = GapLimits::new(5, 2, 2);
2309    let mut st = TestBuilder::new()
2310        .with_data_store_factory(dsf)
2311        .with_gap_limits(gap_limits)
2312        .with_account_from_sapling_activation(BlockHash([0; 32]))
2313        .build();
2314
2315    let account_id = st.test_account().unwrap().id();
2316
2317    let mut receivers = st
2318        .wallet()
2319        .get_transparent_receivers(account_id, false, true)
2320        .unwrap()
2321        .into_iter()
2322        .filter_map(|(addr, meta)| {
2323            let exposure = meta.exposure();
2324            meta.address_index().map(|i| (i.index(), addr, exposure))
2325        })
2326        .collect::<Vec<_>>();
2327    receivers.sort_by_key(|(i, _, _)| *i);
2328
2329    // Use known-unexposed receivers for the bulk-success test, so that the post-call
2330    // recorded height is exactly the one we pass in regardless of any default exposure
2331    // that address generation may set on other receivers.
2332    let unexposed = receivers
2333        .iter()
2334        .copied()
2335        .filter(|(_, _, exposure)| matches!(exposure, Exposure::Unknown))
2336        .collect::<Vec<_>>();
2337    assert!(
2338        unexposed.len() >= 2,
2339        "account should have at least 2 unexposed derived receivers"
2340    );
2341
2342    // Mark two unexposed addresses at distinct heights in a single bulk call.
2343    let (_idx_a, addr_a, _) = unexposed[0];
2344    let (_idx_b, addr_b, _) = unexposed[1];
2345    let height_a = BlockHeight::from(10);
2346    let height_b = BlockHeight::from(20);
2347    st.wallet_mut()
2348        .mark_transparent_addresses_exposed(&[(addr_a, height_a), (addr_b, height_b)])
2349        .unwrap();
2350
2351    let exposure_of = |st: &TestState<_, <DSF as DataStoreFactory>::DataStore, LocalNetwork>,
2352                       addr: &TransparentAddress|
2353     -> Exposure {
2354        st.wallet()
2355            .get_transparent_address_metadata(account_id, addr)
2356            .unwrap()
2357            .unwrap()
2358            .exposure()
2359    };
2360    assert_matches!(
2361        exposure_of(&st, &addr_a),
2362        Exposure::Exposed { at_height, .. } if at_height == height_a
2363    );
2364    assert_matches!(
2365        exposure_of(&st, &addr_b),
2366        Exposure::Exposed { at_height, .. } if at_height == height_b
2367    );
2368
2369    // Now attempt a bulk call where the second entry is unrecognized. The whole call must
2370    // fail, and the first entry must not have been partially applied. Pick a third
2371    // receiver distinct from `addr_a`/`addr_b` — its prior exposure state is irrelevant
2372    // since the assertion is preservation, not a specific height.
2373    let (idx_c, addr_c, _) = *receivers
2374        .iter()
2375        .find(|(_, addr, _)| *addr != addr_a && *addr != addr_b)
2376        .expect("account should have a third derived receiver");
2377    let before = exposure_of(&st, &addr_c);
2378    let unknown = TransparentAddress::PublicKeyHash([0x7u8; 20]);
2379    assert!(
2380        st.wallet_mut()
2381            .mark_transparent_addresses_exposed(&[
2382                (addr_c, BlockHeight::from(5)),
2383                (unknown, BlockHeight::from(5)),
2384            ])
2385            .is_err()
2386    );
2387    assert_eq!(
2388        exposure_of(&st, &addr_c),
2389        before,
2390        "exposure at index {idx_c} must not change when bulk call fails atomically"
2391    );
2392}
2393
2394/// Tests that [`WalletWrite::mark_transparent_addresses_exposed`] returns an error when
2395/// asked to mark an address that the wallet does not track.
2396pub fn mark_transparent_addresses_exposed_unknown_address<DSF>(dsf: DSF)
2397where
2398    DSF: DataStoreFactory,
2399{
2400    let mut st = TestBuilder::new()
2401        .with_data_store_factory(dsf)
2402        .with_account_from_sapling_activation(BlockHash([0; 32]))
2403        .build();
2404
2405    let unknown = TransparentAddress::PublicKeyHash([0u8; 20]);
2406    assert!(
2407        st.wallet_mut()
2408            .mark_transparent_addresses_exposed(&[(unknown, BlockHeight::from(1))])
2409            .is_err()
2410    );
2411}
2412
2413/// Constructs a fake transparent-only transaction spending `vin` and paying `value` to `taddr`.
2414///
2415/// When `vin` is empty the transaction spends one arbitrary outpoint, so that it is never
2416/// structurally a coinbase transaction. The `lock_time` parameter has no consensus meaning here;
2417/// distinct values may be used to give otherwise-identical transactions distinct txids.
2418fn fake_transparent_payment_tx(
2419    lock_time: u32,
2420    vin: &[OutPoint],
2421    value: Zatoshis,
2422    taddr: &TransparentAddress,
2423) -> Transaction {
2424    let bundle = Bundle {
2425        vin: if vin.is_empty() {
2426            vec![TxIn::from_parts(OutPoint::fake(), Script::default(), 0)]
2427        } else {
2428            vin.iter()
2429                .map(|outpoint| TxIn::from_parts(outpoint.clone(), Script::default(), 0))
2430                .collect()
2431        },
2432        vout: vec![TxOut::new(value, taddr.script().into())],
2433        authorization: Authorized,
2434    };
2435
2436    TransactionData::<zcash_primitives::transaction::Authorized>::from_parts(
2437        TxVersion::V5,
2438        BranchId::Nu5,
2439        lock_time,
2440        BlockHeight::from(0),
2441        Some(bundle),
2442        None,
2443        None,
2444        None,
2445    )
2446    .freeze()
2447    .unwrap()
2448}
2449
2450/// Scans a block that spends a wallet transparent output *before* scanning the block that created
2451/// it, and verifies that the spend is recognized once the output is discovered.
2452///
2453/// This is the out-of-order case the transparent spend map exists for: at the time the spending
2454/// block is scanned the wallet does not know it holds the output, so the spend cannot be matched
2455/// against anything. The spending transaction is recorded against the outpoint, and the spend must
2456/// be resolved when the earlier block brings the output into the wallet.
2457pub fn scan_detects_out_of_order_transparent_spend<DSF>(dsf: DSF, cache: impl TestCache)
2458where
2459    DSF: DataStoreFactory,
2460    <<DSF as DataStoreFactory>::DataStore as WalletRead>::AccountId:
2461        ConditionallySelectable + Default + Ord + std::hash::Hash + Send + Sync + 'static,
2462{
2463    let mut st = TestBuilder::new()
2464        .with_data_store_factory(dsf)
2465        .with_block_cache(cache)
2466        .with_account_from_sapling_activation(BlockHash([0; 32]))
2467        .build();
2468
2469    let account_id = st.test_account().unwrap().id();
2470    let taddr = wallet_taddr(st.wallet(), account_id);
2471    let value = Zatoshis::const_from_u64(100000);
2472
2473    // The block that pays the wallet, followed by the block that spends what it paid.
2474    let (received_height, _, received_txid) =
2475        st.generate_next_block_transparent(&[], &[(taddr, value)]);
2476    let funded_outpoint = OutPoint::new(received_txid.into(), 0);
2477    let (spend_height, _, spend_txid) =
2478        st.generate_next_block_transparent(std::slice::from_ref(&funded_outpoint), &[]);
2479
2480    st.scan_cached_blocks(spend_height, 1);
2481
2482    // The wallet cannot recognize a spend of an output it has never seen, so at this point it
2483    // holds no record of the output at all.
2484    assert_matches!(
2485        st.wallet().get_transparent_output(&funded_outpoint, None),
2486        Ok(None),
2487        "the wallet must not know of the output before the block creating it is scanned",
2488    );
2489
2490    // Scanning the earlier block brings the output into the wallet, at which point the
2491    // already-observed spend must be attached to it.
2492    st.scan_cached_blocks(received_height, 1);
2493
2494    let target_height = TargetHeight::from(spend_height + 1);
2495
2496    // The output is now known to the wallet. Asserting this separately is what keeps the
2497    // "not unspent" assertions below from being satisfied by an output that was never recorded
2498    // at all, which is indistinguishable from a spent one in every query that filters on
2499    // spendability.
2500    assert_matches!(
2501        st.wallet().get_transparent_output(&funded_outpoint, None),
2502        Ok(Some(utxo)) if utxo.txout().value() == value,
2503        "the output must have been recorded when the block creating it was scanned",
2504    );
2505
2506    assert_matches!(
2507        st.wallet()
2508            .get_unspent_transparent_output(&funded_outpoint, target_height),
2509        Ok(None),
2510        "the spend observed before the output was discovered must have been resolved",
2511    );
2512    assert_matches!(
2513        st.wallet()
2514            .get_spendable_transparent_outputs(
2515                &taddr,
2516                target_height,
2517                ConfirmationsPolicy::MIN,
2518                CoinbaseFilter::AllTransparentOutputs,
2519                LockFilter::Policy(&LockedInputPolicy::Exclude),
2520            )
2521            .as_deref(),
2522        Ok(&[])
2523    );
2524    assert_matches!(
2525        st.wallet()
2526            .get_transparent_balances(account_id, target_height, ConfirmationsPolicy::MIN),
2527        Ok(balances) if balances.get(&taddr).map(|(_, b)| b.total()) == Some(Zatoshis::ZERO)
2528            || balances.is_empty()
2529    );
2530
2531    // The spend must be attributed to the transaction that actually effected it, which the wallet
2532    // had no other reason to store.
2533    let spending_txids = st
2534        .wallet()
2535        .get_tx_history()
2536        .unwrap()
2537        .into_iter()
2538        .map(|tx| tx.txid())
2539        .collect::<Vec<_>>();
2540    assert!(
2541        spending_txids.contains(&spend_txid),
2542        "the spending transaction {spend_txid} should have been recorded; history holds {spending_txids:?}",
2543    );
2544}
2545
2546/// Scans a block that pays a wallet transparent address, then the block that spends the output,
2547/// and verifies that the spend is recognized directly during the scan of the later block.
2548///
2549/// This is the in-order counterpart of [`scan_detects_out_of_order_transparent_spend`]: here the
2550/// wallet already tracks the outpoint when the spending block is scanned, so the spend is matched
2551/// without consulting the spend map.
2552pub fn scan_detects_transparent_spend<DSF>(dsf: DSF, cache: impl TestCache)
2553where
2554    DSF: DataStoreFactory,
2555    <<DSF as DataStoreFactory>::DataStore as WalletRead>::AccountId:
2556        ConditionallySelectable + Default + Ord + std::hash::Hash + Send + Sync + 'static,
2557{
2558    let mut st = TestBuilder::new()
2559        .with_data_store_factory(dsf)
2560        .with_block_cache(cache)
2561        .with_account_from_sapling_activation(BlockHash([0; 32]))
2562        .build();
2563
2564    let account_id = st.test_account().unwrap().id();
2565    let taddr = wallet_taddr(st.wallet(), account_id);
2566    let value = Zatoshis::const_from_u64(100000);
2567
2568    let (received_height, _, received_txid) =
2569        st.generate_next_block_transparent(&[], &[(taddr, value)]);
2570    let funded_outpoint = OutPoint::new(received_txid.into(), 0);
2571
2572    st.scan_cached_blocks(received_height, 1);
2573
2574    // The scan of the paying block must have recorded the output as a wallet UTXO.
2575    assert_matches!(
2576        st.wallet().get_unspent_transparent_output(
2577            &funded_outpoint,
2578            TargetHeight::from(received_height + 1),
2579        ),
2580        Ok(Some(utxo)) if utxo.txout().value() == value
2581    );
2582
2583    let (spend_height, _, _) =
2584        st.generate_next_block_transparent(std::slice::from_ref(&funded_outpoint), &[]);
2585    st.scan_cached_blocks(spend_height, 1);
2586
2587    assert_matches!(
2588        st.wallet().get_transparent_output(&funded_outpoint, None),
2589        Ok(Some(_)),
2590        "the output must remain known to the wallet after being spent",
2591    );
2592    assert_matches!(
2593        st.wallet()
2594            .get_unspent_transparent_output(&funded_outpoint, TargetHeight::from(spend_height + 1)),
2595        Ok(None),
2596        "the output must be recorded as spent once its spend has been scanned",
2597    );
2598}
2599
2600/// Returns the default external transparent receiver of the given account.
2601fn wallet_taddr<DbT>(wallet: &DbT, account_id: <DbT as WalletRead>::AccountId) -> TransparentAddress
2602where
2603    DbT: WalletRead,
2604    <DbT as WalletRead>::Error: std::fmt::Debug,
2605{
2606    *wallet
2607        .get_last_generated_address_matching(account_id, UnifiedAddressRequest::AllAvailableKeys)
2608        .unwrap()
2609        .unwrap()
2610        .transparent()
2611        .unwrap()
2612}
2613
2614/// Builds a block at `height` whose coinbase transaction is followed by a transparent-only
2615/// transaction paying `value` to `taddr`, scans it, and returns the scanned block together with
2616/// the outpoint of the payment.
2617///
2618/// Placing the payment behind the coinbase keeps the output it creates out of the coinbase
2619/// maturity rule. `lock_time` has no consensus meaning here; distinct values give the payment
2620/// transactions of otherwise-identical blocks distinct txids.
2621///
2622/// The scan is performed against the wallet's current tracked spend identifiers, as
2623/// `scan_cached_blocks` does, so that a spend of an output the wallet already holds is detected.
2624#[allow(clippy::too_many_arguments)]
2625fn scan_transparent_payment_block<DbT>(
2626    wallet: &DbT,
2627    network: &LocalNetwork,
2628    account_id: <DbT as WalletRead>::AccountId,
2629    height: BlockHeight,
2630    lock_time: u32,
2631    vin: &[OutPoint],
2632    value: Zatoshis,
2633    taddr: &TransparentAddress,
2634) -> (ScannedBlock<<DbT as WalletRead>::AccountId>, OutPoint)
2635where
2636    DbT: WalletRead,
2637    <DbT as WalletRead>::Error: std::fmt::Debug,
2638    <DbT as WalletRead>::AccountId:
2639        ConditionallySelectable + Default + Ord + std::hash::Hash + Send + Sync + 'static,
2640{
2641    /// The block subsidy is immaterial to what is under test; any value the miner may claim
2642    /// will do.
2643    const MINER_REWARD: Zatoshis = Zatoshis::const_from_u64(625_000_000);
2644    const MINER_ADDR: TransparentAddress = TransparentAddress::PublicKeyHash([0xf5; 20]);
2645
2646    let payment = fake_transparent_payment_tx(lock_time, vin, value, taddr);
2647    let outpoint = OutPoint::new(payment.txid().into(), 0);
2648    let block = Block::from_parts(
2649        BlockHeaderData {
2650            version: 4,
2651            prev_block: BlockHash([0; 32]),
2652            merkle_root: [0; 32],
2653            final_sapling_root: [0; 32],
2654            time: 0,
2655            bits: 0,
2656            nonce: [0; 32],
2657            solution: vec![],
2658        }
2659        .freeze()
2660        .unwrap(),
2661        NonEmpty::from((
2662            fake_transparent_coinbase_tx(lock_time, MINER_REWARD, &MINER_ADDR),
2663            vec![payment],
2664        )),
2665        height,
2666    );
2667
2668    // The block is scanned in isolation, so the note commitment trees are treated as empty as
2669    // of the immediately preceding block. The block contains no shielded outputs, so they
2670    // remain empty afterwards.
2671    let prior_block_metadata = BlockMetadata::from_parts(
2672        height - 1,
2673        BlockHash([0; 32]),
2674        Some(0),
2675        #[cfg(feature = "orchard")]
2676        Some(0),
2677        #[cfg(feature = "orchard")]
2678        Some(0),
2679    );
2680
2681    let scanning_keys =
2682        ScanningKeys::from_account_ufvks(wallet.get_unified_full_viewing_keys().unwrap());
2683    let (header, vtx) = decrypt_block(network, block, &scanning_keys);
2684    let scanned = scan_block(
2685        network,
2686        height,
2687        &header,
2688        vtx,
2689        &scanning_keys,
2690        &SpendIdentifiers::unspent(wallet).unwrap(),
2691        Some(&prior_block_metadata),
2692        |addr| {
2693            wallet
2694                .get_transparent_address_metadata(account_id, addr)
2695                .map(|meta| meta.map(|m| (account_id, m.source().scope())))
2696        },
2697    )
2698    .expect("scanning the block succeeds");
2699
2700    // The scanner must have detected the payment, and nothing from the coinbase.
2701    assert_matches!(
2702        scanned
2703            .transactions()
2704            .iter()
2705            .flat_map(|wtx| wtx.transparent_outputs())
2706            .collect::<Vec<_>>()
2707            .as_slice(),
2708        [output] if output.outpoint() == &outpoint && output.txout().value() == value
2709    );
2710
2711    (scanned, outpoint)
2712}
2713
2714/// Scans a full block containing a transparent output that pays a wallet address, and verifies
2715/// that persisting the scanned block via [`WalletWrite::put_blocks`] records that output as a
2716/// UTXO belonging to the wallet.
2717pub fn scan_full_block_persists_transparent_outputs<DSF>(dsf: DSF)
2718where
2719    DSF: DataStoreFactory,
2720    <<DSF as DataStoreFactory>::DataStore as WalletRead>::AccountId:
2721        ConditionallySelectable + Default + Ord + std::hash::Hash + Send + Sync + 'static,
2722{
2723    let mut st = TestBuilder::new()
2724        .with_data_store_factory(dsf)
2725        .with_account_from_sapling_activation(BlockHash([0; 32]))
2726        .build();
2727
2728    let account = st.test_account().unwrap();
2729    let account_id = account.id();
2730    let h = account.birthday().height() + 1;
2731    let taddr = wallet_taddr(st.wallet(), account_id);
2732
2733    st.wallet_mut().update_chain_tip(h).unwrap();
2734
2735    // The wallet must not already know of any output at this address.
2736    assert_matches!(
2737        st.wallet().get_transparent_balances(
2738            account_id,
2739            TargetHeight::from(h + 1),
2740            ConfirmationsPolicy::MIN,
2741        ),
2742        Ok(balances) if balances.is_empty()
2743    );
2744
2745    let value = Zatoshis::const_from_u64(100000);
2746    let network = *st.network();
2747    let (scanned, outpoint) =
2748        scan_transparent_payment_block(st.wallet(), &network, account_id, h, 1, &[], value, &taddr);
2749
2750    st.wallet_mut()
2751        .put_blocks(&ChainState::empty(h - 1, BlockHash([0; 32])), vec![scanned])
2752        .unwrap();
2753
2754    // The scanned output must now be a UTXO of the wallet.
2755    let target_height = TargetHeight::from(h + 1);
2756    assert_matches!(
2757        st.wallet()
2758            .get_unspent_transparent_output(&outpoint, target_height),
2759        Ok(Some(ret))
2760        if ret.txout().value() == value && ret.mined_height() == Some(h)
2761    );
2762    assert_matches!(
2763        st.wallet().get_spendable_transparent_outputs(
2764            &taddr,
2765            target_height,
2766            ConfirmationsPolicy::MIN,
2767            CoinbaseFilter::AllTransparentOutputs,
2768            LockFilter::Policy(&LockedInputPolicy::Exclude),
2769        ).as_deref(),
2770        Ok([ret]) if ret.outpoint() == &outpoint
2771    );
2772    assert_matches!(
2773        st.wallet().get_transparent_balances(
2774            account_id,
2775            target_height,
2776            ConfirmationsPolicy::MIN,
2777        ),
2778        Ok(balances) if balances.get(&taddr).map(|(_, b)| b.spendable_value()) == Some(value)
2779    );
2780}
2781
2782/// Scans a full block that pays a wallet transparent address, then a full block that spends the
2783/// resulting output, and verifies that the spend is detected.
2784///
2785/// The wallet already tracks the outpoint when the spending block is scanned, so the spend is
2786/// matched directly against its tracked outputs rather than resolved through the spend map.
2787pub fn scan_full_block_detects_transparent_spend<DSF>(dsf: DSF)
2788where
2789    DSF: DataStoreFactory,
2790    <<DSF as DataStoreFactory>::DataStore as WalletRead>::AccountId:
2791        ConditionallySelectable + Default + Ord + std::hash::Hash + Send + Sync + 'static,
2792{
2793    let mut st = TestBuilder::new()
2794        .with_data_store_factory(dsf)
2795        .with_account_from_sapling_activation(BlockHash([0; 32]))
2796        .build();
2797
2798    let account = st.test_account().unwrap();
2799    let account_id = account.id();
2800    let h = account.birthday().height() + 1;
2801    let taddr = wallet_taddr(st.wallet(), account_id);
2802    let value = Zatoshis::const_from_u64(100000);
2803    let network = *st.network();
2804
2805    st.wallet_mut().update_chain_tip(h + 1).unwrap();
2806
2807    let (received, funded_outpoint) =
2808        scan_transparent_payment_block(st.wallet(), &network, account_id, h, 1, &[], value, &taddr);
2809    st.wallet_mut()
2810        .put_blocks(
2811            &ChainState::empty(h - 1, BlockHash([0; 32])),
2812            vec![received],
2813        )
2814        .unwrap();
2815
2816    assert_matches!(
2817        st.wallet().get_transparent_output(&funded_outpoint, None),
2818        Ok(Some(utxo)) if utxo.txout().value() == value,
2819        "the output must be recorded when the block creating it is scanned",
2820    );
2821
2822    // Scanning the spending block must now detect the spend directly.
2823    let (spending, _) = scan_transparent_payment_block(
2824        st.wallet(),
2825        &network,
2826        account_id,
2827        h + 1,
2828        2,
2829        std::slice::from_ref(&funded_outpoint),
2830        value,
2831        &taddr,
2832    );
2833    assert_matches!(
2834        spending
2835            .transactions()
2836            .iter()
2837            .flat_map(|wtx| wtx.transparent_spends())
2838            .collect::<Vec<_>>()
2839            .as_slice(),
2840        [spend] if spend.outpoint() == &funded_outpoint,
2841        "the scanner must report the spend of the wallet's output",
2842    );
2843
2844    st.wallet_mut()
2845        .put_blocks(&ChainState::empty(h, BlockHash([0; 32])), vec![spending])
2846        .unwrap();
2847
2848    assert_matches!(
2849        st.wallet().get_transparent_output(&funded_outpoint, None),
2850        Ok(Some(_)),
2851        "the output must remain known to the wallet after being spent",
2852    );
2853    assert_matches!(
2854        st.wallet()
2855            .get_unspent_transparent_output(&funded_outpoint, TargetHeight::from(h + 2)),
2856        Ok(None),
2857        "the output must be recorded as spent once its spend has been scanned",
2858    );
2859}
2860
2861/// Scans a full block that spends a wallet transparent output *before* the block that created it,
2862/// and verifies that the spend is resolved once the output is discovered.
2863///
2864/// This is the out-of-order case: at the time the spending block is scanned the wallet does not
2865/// know it holds the output, so the spend must be recorded against the outpoint and applied when
2866/// the earlier block brings the output into the wallet.
2867pub fn scan_full_block_detects_out_of_order_transparent_spend<DSF>(dsf: DSF)
2868where
2869    DSF: DataStoreFactory,
2870    <<DSF as DataStoreFactory>::DataStore as WalletRead>::AccountId:
2871        ConditionallySelectable + Default + Ord + std::hash::Hash + Send + Sync + 'static,
2872{
2873    let mut st = TestBuilder::new()
2874        .with_data_store_factory(dsf)
2875        .with_account_from_sapling_activation(BlockHash([0; 32]))
2876        .build();
2877
2878    let account = st.test_account().unwrap();
2879    let account_id = account.id();
2880    let h = account.birthday().height() + 1;
2881    let taddr = wallet_taddr(st.wallet(), account_id);
2882    let value = Zatoshis::const_from_u64(100000);
2883    let network = *st.network();
2884
2885    st.wallet_mut().update_chain_tip(h + 1).unwrap();
2886
2887    // Build the paying block first so that its outpoint is known, but do not scan it yet.
2888    let (received, funded_outpoint) =
2889        scan_transparent_payment_block(st.wallet(), &network, account_id, h, 1, &[], value, &taddr);
2890
2891    // Scan and persist the block that spends it. The wallet cannot recognize the spend: it does
2892    // not yet know that it holds the output being spent.
2893    let (spending, _) = scan_transparent_payment_block(
2894        st.wallet(),
2895        &network,
2896        account_id,
2897        h + 1,
2898        2,
2899        std::slice::from_ref(&funded_outpoint),
2900        value,
2901        &taddr,
2902    );
2903    assert!(
2904        spending
2905            .transactions()
2906            .iter()
2907            .all(|wtx| wtx.transparent_spends().is_empty()),
2908        "the scanner cannot match a spend of an output the wallet has not yet seen",
2909    );
2910    st.wallet_mut()
2911        .put_blocks(&ChainState::empty(h, BlockHash([0; 32])), vec![spending])
2912        .unwrap();
2913
2914    assert_matches!(
2915        st.wallet().get_transparent_output(&funded_outpoint, None),
2916        Ok(None),
2917        "the wallet must not know of the output before the block creating it is scanned",
2918    );
2919
2920    // Persisting the earlier block brings the output into the wallet, at which point the
2921    // already-recorded spend must be applied to it.
2922    st.wallet_mut()
2923        .put_blocks(
2924            &ChainState::empty(h - 1, BlockHash([0; 32])),
2925            vec![received],
2926        )
2927        .unwrap();
2928
2929    // As in the compact-block case, asserting that the output is known is what keeps the
2930    // "not unspent" assertion below from being satisfied by an output never recorded at all.
2931    assert_matches!(
2932        st.wallet().get_transparent_output(&funded_outpoint, None),
2933        Ok(Some(utxo)) if utxo.txout().value() == value,
2934        "the output must have been recorded when the block creating it was scanned",
2935    );
2936    assert_matches!(
2937        st.wallet()
2938            .get_unspent_transparent_output(&funded_outpoint, TargetHeight::from(h + 2)),
2939        Ok(None),
2940        "the spend observed before the output was discovered must have been resolved",
2941    );
2942}
2943
2944/// Verifies that the transparent outputs written while persisting a batch of scanned blocks are
2945/// rolled back along with the rest of the batch when a later block in the batch is rejected.
2946///
2947/// The batch is made to fail by leaving a gap between its two blocks, which
2948/// [`WalletWrite::put_blocks`] rejects only after the first block has been written.
2949pub fn put_blocks_rolls_back_transparent_outputs<DSF>(dsf: DSF)
2950where
2951    DSF: DataStoreFactory,
2952    <<DSF as DataStoreFactory>::DataStore as WalletRead>::AccountId:
2953        ConditionallySelectable + Default + Ord + std::hash::Hash + Send + Sync + 'static,
2954{
2955    let mut st = TestBuilder::new()
2956        .with_data_store_factory(dsf)
2957        .with_account_from_sapling_activation(BlockHash([0; 32]))
2958        .build();
2959
2960    let account = st.test_account().unwrap();
2961    let account_id = account.id();
2962    let h = account.birthday().height() + 1;
2963    let taddr = wallet_taddr(st.wallet(), account_id);
2964
2965    st.wallet_mut().update_chain_tip(h + 2).unwrap();
2966
2967    let value = Zatoshis::const_from_u64(100000);
2968    let network = *st.network();
2969    let (first, first_outpoint) =
2970        scan_transparent_payment_block(st.wallet(), &network, account_id, h, 1, &[], value, &taddr);
2971    let (discontinuous, discontinuous_outpoint) = scan_transparent_payment_block(
2972        st.wallet(),
2973        &network,
2974        account_id,
2975        h + 2,
2976        2,
2977        &[],
2978        value,
2979        &taddr,
2980    );
2981
2982    assert_matches!(
2983        st.wallet_mut().put_blocks(
2984            &ChainState::empty(h - 1, BlockHash([0; 32])),
2985            vec![first, discontinuous],
2986        ),
2987        Err(_)
2988    );
2989
2990    // Neither block's output may have been committed: the first block's writes must have been
2991    // rolled back along with the failure of the second.
2992    let target_height = TargetHeight::from(h + 3);
2993    for outpoint in [&first_outpoint, &discontinuous_outpoint] {
2994        assert_matches!(
2995            st.wallet()
2996                .get_unspent_transparent_output(outpoint, target_height),
2997            Ok(None)
2998        );
2999    }
3000    assert_matches!(
3001        st.wallet().get_transparent_balances(
3002            account_id,
3003            target_height,
3004            ConfirmationsPolicy::MIN,
3005        ),
3006        Ok(balances) if balances.is_empty()
3007    );
3008}
3009
3010/// Sets up a wallet whose account holds no shielded notes and a single spendable
3011/// transparent UTXO at its default external transparent receiver, returning the test
3012/// state, the account, the funding outpoint, and the UTXO value.
3013///
3014/// The chain is seeded with blocks containing notes that do *not* belong to the wallet,
3015/// so that target/anchor heights resolve while the account remains shielded-empty. This
3016/// isolates the transparent-UTXO selection path.
3017#[allow(clippy::type_complexity)]
3018fn setup_transparent_only_account<DSF>(
3019    dsf: DSF,
3020    cache: impl TestCache,
3021    utxo_value: Zatoshis,
3022) -> (
3023    TestState<impl TestCache, <DSF as DataStoreFactory>::DataStore, LocalNetwork>,
3024    TestAccount<<DSF as DataStoreFactory>::Account>,
3025    OutPoint,
3026)
3027where
3028    DSF: DataStoreFactory,
3029{
3030    let mut st = TestBuilder::new()
3031        .with_data_store_factory(dsf)
3032        .with_block_cache(cache)
3033        .with_account_from_sapling_activation(BlockHash([0; 32]))
3034        .build();
3035
3036    let account = st.test_account().cloned().unwrap();
3037    let uaddr = st
3038        .wallet()
3039        .get_last_generated_address_matching(account.id(), UnifiedAddressRequest::AllAvailableKeys)
3040        .unwrap()
3041        .unwrap();
3042    let taddr = uaddr.transparent().unwrap();
3043
3044    // Seed the chain with notes that do not belong to us so that heights resolve while
3045    // the account remains without any shielded notes.
3046    let not_our_key = ExtendedSpendingKey::master(&[])
3047        .expect("the derivation path yields a valid key")
3048        .to_diversifiable_full_viewing_key();
3049    let not_our_value = Zatoshis::const_from_u64(10000);
3050    let (start_height, _, _) =
3051        st.generate_next_block(&not_our_key, AddressType::DefaultExternal, not_our_value);
3052    for _ in 1..10 {
3053        st.generate_next_block(&not_our_key, AddressType::DefaultExternal, not_our_value);
3054    }
3055    st.scan_cached_blocks(start_height, 10);
3056
3057    // Fund the account with a single transparent UTXO well above the marginal fee.
3058    let txout = TxOut::new(utxo_value, taddr.script().into());
3059    let height = st.wallet().chain_height().unwrap().unwrap();
3060    let outpoint = OutPoint::fake();
3061    let utxo = WalletTransparentOutput::from_parts(
3062        outpoint.clone(),
3063        txout,
3064        Some(height),
3065        Some(account.id()),
3066        Some(TransparentKeyScope::EXTERNAL),
3067        None,
3068    )
3069    .unwrap();
3070    st.wallet_mut()
3071        .put_received_transparent_utxo(&utxo)
3072        .unwrap();
3073
3074    (st, account, outpoint)
3075}
3076
3077/// Builds a single-payment t->t [`TransactionRequest`] paying `amount` to a fixed
3078/// external transparent recipient.
3079fn t2t_request(network: &LocalNetwork, amount: Zatoshis) -> TransactionRequest {
3080    let recipient = TransparentAddress::PublicKeyHash([7u8; 20]);
3081    TransactionRequest::new(vec![Payment::without_memo(
3082        Address::Transparent(recipient).to_zcash_address(network),
3083        amount,
3084    )])
3085    .unwrap()
3086}
3087
3088/// Regression test enforcing the privacy invariant: with the default
3089/// default spend policy (which permits no transparent spending), a transfer must NOT silently spend
3090/// the account's transparent UTXOs as a fallback. An account holding only transparent
3091/// funds must fail with [`InsufficientFunds`] rather than producing a t->t proposal.
3092///
3093/// [`InsufficientFunds`]: crate::data_api::error::Error::InsufficientFunds
3094pub fn propose_t2t_shielded_only_is_insufficient<DSF>(dsf: DSF, cache: impl TestCache)
3095where
3096    DSF: DataStoreFactory,
3097{
3098    let utxo_value = Zatoshis::const_from_u64(100_000);
3099    let (mut st, account, _outpoint) = setup_transparent_only_account(dsf, cache, utxo_value);
3100
3101    let network = *st.network();
3102    let request = t2t_request(&network, Zatoshis::const_from_u64(40_000));
3103
3104    let input_selector = GreedyInputSelector::new();
3105    let change_strategy =
3106        single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Sapling);
3107
3108    let result = st.propose_transfer_with_policy(
3109        account.id(),
3110        &input_selector,
3111        &change_strategy,
3112        request,
3113        ConfirmationsPolicy::MIN,
3114        &SpendPolicy::default(),
3115    );
3116
3117    assert_matches!(
3118        result,
3119        Err(crate::data_api::error::Error::InsufficientFunds { .. }),
3120        "shielded-only policy must not spend transparent UTXOs as a fallback",
3121    );
3122}
3123
3124/// With `TransparentSpendPolicy::any_account_addr` (the legacy `ANY_TADDR` behavior), a
3125/// transfer may spend the account's transparent UTXOs. Verifies that the funding UTXO is
3126/// selected as a transparent input and that the proposal balance is consistent.
3127pub fn propose_t2t_any_account_taddr<DSF>(dsf: DSF, cache: impl TestCache)
3128where
3129    DSF: DataStoreFactory,
3130{
3131    let utxo_value = Zatoshis::const_from_u64(100_000);
3132    let transfer_amount = Zatoshis::const_from_u64(40_000);
3133    let (mut st, account, outpoint) = setup_transparent_only_account(dsf, cache, utxo_value);
3134
3135    let network = *st.network();
3136    let request = t2t_request(&network, transfer_amount);
3137
3138    let input_selector = GreedyInputSelector::new();
3139    let change_strategy =
3140        single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Sapling);
3141
3142    let proposal = st
3143        .propose_transfer_with_policy(
3144            account.id(),
3145            &input_selector,
3146            &change_strategy,
3147            request,
3148            ConfirmationsPolicy::MIN,
3149            &SpendPolicy::default().with_transparent(TransparentSpendPolicy::any_account_addr()),
3150        )
3151        .expect("transparent spend must succeed under any-account-address transparent spending");
3152
3153    // A pure t->t transfer is a single step (no ZIP-320 ephemeral roundtrip).
3154    assert_eq!(proposal.steps().len(), 1);
3155    let step = &proposal.steps().head;
3156
3157    assert_eq!(
3158        step.transparent_inputs().len(),
3159        1,
3160        "expected exactly one transparent input selected from the account's UTXOs",
3161    );
3162    assert_eq!(step.transparent_inputs()[0].outpoint(), &outpoint);
3163    assert_eq!(step.transparent_inputs()[0].txout().value(), utxo_value);
3164
3165    // `TransactionBalance::total()` is `change + fee`, which by the balance equation equals
3166    // input total minus the explicit payment.
3167    assert_eq!(
3168        step.balance().total(),
3169        (utxo_value - transfer_amount).unwrap(),
3170    );
3171    assert!(step.balance().fee_required() > Zatoshis::ZERO);
3172    assert!(!step.balance().proposed_change().is_empty());
3173}
3174
3175/// Verifies that `GreedyInputSelector::propose_transaction` successfully re-gathers
3176/// transparent inputs when the initial fee-aware gather's estimate (which accounts only
3177/// for the transparent side of the transaction) turns out to be insufficient once the
3178/// real change strategy accounts for the additional shielded action required by a
3179/// shielded payment recipient.
3180///
3181/// This exercises the `ChangeError::InsufficientFunds` fallback path in
3182/// `GreedyInputSelector::propose_transaction`, which re-invokes
3183/// `InputSource::select_spendable_transparent_outputs` with a corrected `TargetValue`
3184/// when the first gather's reservation proves too small. Funding many small transparent
3185/// UTXOs forces the initial gather to stop with just enough inputs to cover the payment
3186/// under its own (transparent-only) fee estimate; the shielded payment output then pushes
3187/// the real required fee higher, so satisfying the request is only possible by gathering
3188/// additional inputs beyond that initial estimate.
3189pub fn propose_t2shielded_requires_transparent_regather<DSF>(dsf: DSF, cache: impl TestCache)
3190where
3191    DSF: DataStoreFactory,
3192{
3193    let mut st = TestBuilder::new()
3194        .with_data_store_factory(dsf)
3195        .with_block_cache(cache)
3196        .with_account_from_sapling_activation(BlockHash([0; 32]))
3197        .build();
3198
3199    let account = st.test_account().cloned().unwrap();
3200    let uaddr = st
3201        .wallet()
3202        .get_last_generated_address_matching(account.id(), UnifiedAddressRequest::AllAvailableKeys)
3203        .unwrap()
3204        .unwrap();
3205    let taddr = *uaddr.transparent().unwrap();
3206
3207    // Seed the chain with notes that do not belong to us so that heights resolve.
3208    let not_our_key = ExtendedSpendingKey::master(&[])
3209        .expect("the derivation path yields a valid key")
3210        .to_diversifiable_full_viewing_key();
3211    let not_our_value = Zatoshis::const_from_u64(10_000);
3212    let (start_height, _, _) =
3213        st.generate_next_block(&not_our_key, AddressType::DefaultExternal, not_our_value);
3214    for _ in 1..10 {
3215        st.generate_next_block(&not_our_key, AddressType::DefaultExternal, not_our_value);
3216    }
3217    st.scan_cached_blocks(start_height, 10);
3218
3219    // Fund the account with many small transparent UTXOs, none of which alone are
3220    // remotely close to the payment amount, so that gathering enough of them to satisfy
3221    // the (larger, corrected) real requirement remains possible.
3222    let dust_value = Zatoshis::const_from_u64(10_000);
3223    let n_dust = 30;
3224    let height = st.wallet().chain_height().unwrap().unwrap();
3225    for i in 0..n_dust {
3226        let mut hash = [0u8; 32];
3227        hash[..4].copy_from_slice(&(i as u32).to_le_bytes());
3228        let utxo = WalletTransparentOutput::from_parts(
3229            OutPoint::new(hash, 0),
3230            TxOut::new(dust_value, taddr.script().into()),
3231            Some(height),
3232            Some(account.id()),
3233            Some(TransparentKeyScope::EXTERNAL),
3234            None,
3235        )
3236        .unwrap();
3237        st.wallet_mut()
3238            .put_received_transparent_utxo(&utxo)
3239            .unwrap();
3240    }
3241
3242    // Pay a shielded recipient. The initial transparent gather's fee estimate accounts
3243    // only for the transparent inputs it selects; it does not (and cannot, since the
3244    // shielded payment output isn't known to `InputSource::select_spendable_transparent_outputs`)
3245    // account for the additional sapling action this payment requires, so the first pass
3246    // undershoots and a re-gather is required to actually satisfy the request.
3247    let network = *st.network();
3248    let recipient = ExtendedSpendingKey::master(&[1u8; 32])
3249        .expect("the derivation path yields a valid key")
3250        .to_diversifiable_full_viewing_key()
3251        .default_address()
3252        .1;
3253    let payment_amount = Zatoshis::const_from_u64(50_000);
3254    let request = TransactionRequest::new(vec![Payment::without_memo(
3255        Address::Sapling(Box::new(recipient)).to_zcash_address(&network),
3256        payment_amount,
3257    )])
3258    .unwrap();
3259
3260    let input_selector = GreedyInputSelector::new();
3261    let change_strategy =
3262        single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Sapling);
3263
3264    // Independently reproduce the initial gather that `GreedyInputSelector` will perform
3265    // (bounded only by the payment amount, since that's the only information available
3266    // before the shielded payment output's fee contribution is known). Comparing this to
3267    // the transparent inputs actually used by the successful proposal below demonstrates
3268    // that the proposal could only have succeeded via the re-gather fallback: the initial
3269    // gather's own reservation ignores the extra sapling action the payment requires, so
3270    // it cannot by itself have covered the real, higher requirement.
3271    let initial_gather = st
3272        .wallet()
3273        .select_spendable_transparent_outputs(
3274            account.id(),
3275            TargetHeight::from(height + 1),
3276            ConfirmationsPolicy::MIN,
3277            CoinbaseFilter::NonCoinbaseOnly,
3278            None,
3279            TargetValue::AtLeast(payment_amount),
3280            usize::MAX,
3281            &StandardFeeRule::Zip317,
3282            LockFilter::Policy(&LockedInputPolicy::Exclude),
3283        )
3284        .expect("initial gather should succeed");
3285    let initial_gather_value: Zatoshis = initial_gather
3286        .iter()
3287        .map(|u| u.value())
3288        .fold(Zatoshis::ZERO, |acc, v| (acc + v).unwrap());
3289
3290    let proposal = st
3291        .propose_transfer_with_policy(
3292            account.id(),
3293            &input_selector,
3294            &change_strategy,
3295            request,
3296            ConfirmationsPolicy::MIN,
3297            &SpendPolicy::default().with_transparent(TransparentSpendPolicy::any_account_addr()),
3298        )
3299        .expect(
3300            "transparent spend should succeed via the re-gather fallback despite the \
3301             initial gather's fee estimate being insufficient",
3302        );
3303
3304    let step = &proposal.steps().head;
3305    let gathered_value: Zatoshis = step
3306        .transparent_inputs()
3307        .iter()
3308        .map(|i| i.txout().value())
3309        .fold(Zatoshis::ZERO, |acc, v| (acc + v).unwrap());
3310    assert!(
3311        gathered_value > initial_gather_value,
3312        "the successful proposal should have gathered more transparent value ({}) than \
3313         the insufficient initial gather ({})",
3314        u64::from(gathered_value),
3315        u64::from(initial_gather_value),
3316    );
3317    assert!(step.balance().fee_required() > Zatoshis::ZERO);
3318}
3319
3320/// Verifies that `GreedyInputSelector::with_shielding_block_space_percent` also bounds the
3321/// transparent gather performed for general (non-shielding) transfers, not just shielding.
3322///
3323/// Funds more dust UTXOs than fit within a 1%-of-block-space cap, and requests a payment
3324/// whose post-fee cost can only be met by exceeding that cap. The gather must stop at the
3325/// cap rather than consuming every eligible UTXO, so the proposal fails with
3326/// [`InsufficientFunds`] instead of succeeding with an uncapped number of transparent inputs.
3327///
3328/// [`InsufficientFunds`]: crate::data_api::error::Error::InsufficientFunds
3329pub fn propose_transfer_transparent_input_cap<DSF>(dsf: DSF, cache: impl TestCache)
3330where
3331    DSF: DataStoreFactory,
3332{
3333    // At 1% of block space the cap is (2_000_000 * 1 / 100) / 150 = 133 inputs.
3334    const BLOCK_SPACE_PERCENT: u32 = 1;
3335    const CAP: usize = 133;
3336    const NUM_UTXOS: usize = CAP + 7; // 140; more than enough to exceed the cap.
3337    const DUST_VALUE: u64 = 10_000;
3338
3339    let mut st = TestBuilder::new()
3340        .with_data_store_factory(dsf)
3341        .with_block_cache(cache)
3342        .with_account_from_sapling_activation(BlockHash([0; 32]))
3343        .build();
3344
3345    let account = st.test_account().cloned().unwrap();
3346    let uaddr = st
3347        .wallet()
3348        .get_last_generated_address_matching(account.id(), UnifiedAddressRequest::AllAvailableKeys)
3349        .unwrap()
3350        .unwrap();
3351    let taddr = *uaddr.transparent().unwrap();
3352
3353    // Seed the chain with notes that do not belong to us so that heights resolve.
3354    let not_our_key = ExtendedSpendingKey::master(&[])
3355        .expect("the derivation path yields a valid key")
3356        .to_diversifiable_full_viewing_key();
3357    let not_our_value = Zatoshis::const_from_u64(10_000);
3358    let (start_height, _, _) =
3359        st.generate_next_block(&not_our_key, AddressType::DefaultExternal, not_our_value);
3360    for _ in 1..10 {
3361        st.generate_next_block(&not_our_key, AddressType::DefaultExternal, not_our_value);
3362    }
3363    st.scan_cached_blocks(start_height, 10);
3364
3365    // Fund the account with more dust UTXOs than fit within the cap.
3366    let height = st.wallet().chain_height().unwrap().unwrap();
3367    for i in 0..NUM_UTXOS {
3368        let mut hash = [0u8; 32];
3369        hash[..4].copy_from_slice(&(i as u32).to_le_bytes());
3370        let utxo = WalletTransparentOutput::from_parts(
3371            OutPoint::new(hash, 0),
3372            TxOut::new(Zatoshis::const_from_u64(DUST_VALUE), taddr.script().into()),
3373            Some(height),
3374            Some(account.id()),
3375            Some(TransparentKeyScope::EXTERNAL),
3376            None,
3377        )
3378        .unwrap();
3379        st.wallet_mut()
3380            .put_received_transparent_utxo(&utxo)
3381            .unwrap();
3382    }
3383
3384    // Request a post-fee amount that is only reachable by gathering more than `CAP` inputs
3385    // (with the cap, `CAP` inputs net `DUST_VALUE * CAP - 5_000 * CAP = 5_000 * CAP`
3386    // post-fee; requesting exactly that much would succeed, so request more).
3387    let payment_amount = Zatoshis::const_from_u64(5_000 * (CAP as u64) + DUST_VALUE);
3388
3389    let network = *st.network();
3390    let request = t2t_request(&network, payment_amount);
3391
3392    let input_selector =
3393        GreedyInputSelector::new().with_shielding_block_space_percent(BLOCK_SPACE_PERCENT);
3394    let change_strategy =
3395        single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Sapling);
3396
3397    let result = st.propose_transfer_with_policy(
3398        account.id(),
3399        &input_selector,
3400        &change_strategy,
3401        request,
3402        ConfirmationsPolicy::MIN,
3403        &SpendPolicy::default().with_transparent(TransparentSpendPolicy::any_account_addr()),
3404    );
3405
3406    // The gather stops at `CAP` inputs, whose value the error reports as available; the
3407    // request needs the payment plus one logical action per gathered input.
3408    let expected_available = Zatoshis::const_from_u64(DUST_VALUE * (CAP as u64));
3409    let expected_required =
3410        (payment_amount + (zip317::MARGINAL_FEE * (CAP as u64)).unwrap()).unwrap();
3411    assert_matches!(
3412        result,
3413        Err(crate::data_api::error::Error::InsufficientFunds { available, required })
3414            if available == expected_available && required == expected_required,
3415        "the transparent gather should stop at the input cap rather than consuming every \
3416         eligible dust UTXO, so the request should fail with the capped inputs reported as \
3417         available rather than succeed with an uncapped number of inputs",
3418    );
3419}
3420
3421/// With a `TransparentSource::FromAddresses` transparent source, only the explicitly named transparent
3422/// addresses are eligible. Funds two of the account's external receivers but names only
3423/// one; the proposal must select solely from the named address.
3424pub fn propose_t2t_from_addresses<DSF>(dsf: DSF, cache: impl TestCache)
3425where
3426    DSF: DataStoreFactory,
3427{
3428    let mut st = TestBuilder::new()
3429        .with_data_store_factory(dsf)
3430        .with_block_cache(cache)
3431        .with_account_from_sapling_activation(BlockHash([0; 32]))
3432        .build();
3433
3434    let account = st.test_account().cloned().unwrap();
3435
3436    // Seed the chain with notes that do not belong to us so heights resolve.
3437    let not_our_key = ExtendedSpendingKey::master(&[])
3438        .expect("the derivation path yields a valid key")
3439        .to_diversifiable_full_viewing_key();
3440    let not_our_value = Zatoshis::const_from_u64(10000);
3441    let (start_height, _, _) =
3442        st.generate_next_block(&not_our_key, AddressType::DefaultExternal, not_our_value);
3443    for _ in 1..10 {
3444        st.generate_next_block(&not_our_key, AddressType::DefaultExternal, not_our_value);
3445    }
3446    st.scan_cached_blocks(start_height, 10);
3447
3448    // Enumerate two distinct external transparent receivers belonging to the account.
3449    let external_taddrs = st
3450        .wallet()
3451        .get_transparent_receivers(account.id(), false, true)
3452        .unwrap();
3453    let mut taddrs_by_index = external_taddrs
3454        .into_iter()
3455        .filter_map(|(addr, meta)| meta.address_index().map(|i| (i, addr)))
3456        .collect::<BTreeMap<_, _>>()
3457        .into_values();
3458    let addr_named = taddrs_by_index.next().expect("at least one external taddr");
3459    let addr_other = taddrs_by_index
3460        .next()
3461        .expect("at least two external taddrs");
3462
3463    // Fund both receivers with a spendable UTXO each. The unnamed address's UTXO is
3464    // inserted first AND holds strictly more value than the named address's, so that if
3465    // the address filter were not enforced, the value-descending gather would select the
3466    // unnamed address's UTXO (which alone covers the payment) and stop -- making this test
3467    // fail. The named UTXO must be selected by policy, not merely by gather order.
3468    let named_value = Zatoshis::const_from_u64(100_000);
3469    let other_value = Zatoshis::const_from_u64(150_000);
3470    let height = st.wallet().chain_height().unwrap().unwrap();
3471    let named_outpoint = OutPoint::new([1u8; 32], 0);
3472    let other_outpoint = OutPoint::new([2u8; 32], 0);
3473    for (addr, outpoint, value) in [
3474        (addr_other, other_outpoint.clone(), other_value),
3475        (addr_named, named_outpoint.clone(), named_value),
3476    ] {
3477        let utxo = WalletTransparentOutput::from_parts(
3478            outpoint,
3479            TxOut::new(value, addr.script().into()),
3480            Some(height),
3481            Some(account.id()),
3482            Some(TransparentKeyScope::EXTERNAL),
3483            None,
3484        )
3485        .unwrap();
3486        st.wallet_mut()
3487            .put_received_transparent_utxo(&utxo)
3488            .unwrap();
3489    }
3490
3491    let network = *st.network();
3492    let request = t2t_request(&network, Zatoshis::const_from_u64(40_000));
3493
3494    let input_selector = GreedyInputSelector::new();
3495    let change_strategy =
3496        single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Sapling);
3497
3498    let proposal = st
3499        .propose_transfer_with_policy(
3500            account.id(),
3501            &input_selector,
3502            &change_strategy,
3503            request,
3504            ConfirmationsPolicy::MIN,
3505            &SpendPolicy::default()
3506                .with_transparent(TransparentSpendPolicy::from_one_address(addr_named)),
3507        )
3508        .expect("transparent spend from named address must succeed");
3509
3510    let step = &proposal.steps().head;
3511    let selected: Vec<&OutPoint> = step
3512        .transparent_inputs()
3513        .iter()
3514        .map(|i| i.outpoint())
3515        .collect();
3516    assert!(
3517        selected.contains(&&named_outpoint),
3518        "the named address's UTXO must be selected",
3519    );
3520    assert!(
3521        !selected.contains(&&other_outpoint),
3522        "an unnamed address's UTXO must not be selected",
3523    );
3524}
3525
3526/// Verifies that the value-bounded `select_spendable_transparent_outputs` gather returns
3527/// only enough UTXOs to cover the requested `TargetValue`, rather than every spendable
3528/// output held by the account. This is the behavior that prevents wallets with large
3529/// numbers of small transparent UTXOs (e.g. recovered `zcashd` imports) from falling over
3530/// when a small transfer is requested.
3531pub fn value_bounded_transparent_gather<DSF>(dsf: DSF, cache: impl TestCache)
3532where
3533    DSF: DataStoreFactory,
3534{
3535    let mut st = TestBuilder::new()
3536        .with_data_store_factory(dsf)
3537        .with_block_cache(cache)
3538        .with_account_from_sapling_activation(BlockHash([0; 32]))
3539        .build();
3540
3541    let account = st.test_account().cloned().unwrap();
3542    let uaddr = st
3543        .wallet()
3544        .get_last_generated_address_matching(account.id(), UnifiedAddressRequest::AllAvailableKeys)
3545        .unwrap()
3546        .unwrap();
3547    let taddr = *uaddr.transparent().unwrap();
3548
3549    // Seed the chain with notes that do not belong to us so that heights resolve.
3550    let not_our_key = ExtendedSpendingKey::master(&[])
3551        .expect("the derivation path yields a valid key")
3552        .to_diversifiable_full_viewing_key();
3553    let not_our_value = Zatoshis::const_from_u64(10_000);
3554    let (start_height, _, _) =
3555        st.generate_next_block(&not_our_key, AddressType::DefaultExternal, not_our_value);
3556    for _ in 1..10 {
3557        st.generate_next_block(&not_our_key, AddressType::DefaultExternal, not_our_value);
3558    }
3559    st.scan_cached_blocks(start_height, 10);
3560
3561    // Fund the account with many small dust UTXOs. Each is well above the marginal fee
3562    // (1_000 zats) so it's spendable, but tiny relative to the total. A naive "return
3563    // everything" gather would load all of these into memory; the value-bounded gather
3564    // should only return enough to cover the request.
3565    let dust_value = Zatoshis::const_from_u64(10_000);
3566    let n_dust = 50;
3567    let height = st.wallet().chain_height().unwrap().unwrap();
3568    for i in 0..n_dust {
3569        let mut hash = [0u8; 32];
3570        hash[..4].copy_from_slice(&(i as u32).to_le_bytes());
3571        let utxo = WalletTransparentOutput::from_parts(
3572            OutPoint::new(hash, 0),
3573            TxOut::new(dust_value, taddr.script().into()),
3574            Some(height),
3575            Some(account.id()),
3576            Some(TransparentKeyScope::EXTERNAL),
3577            None,
3578        )
3579        .unwrap();
3580        st.wallet_mut()
3581            .put_received_transparent_utxo(&utxo)
3582            .unwrap();
3583    }
3584
3585    // Request 30_000 zats — 3x a single dust UTXO, but a small fraction of the total.
3586    let target = Zatoshis::const_from_u64(30_000);
3587    let target_height = TargetHeight::from(height + 1);
3588
3589    let bound = st
3590        .wallet()
3591        .select_spendable_transparent_outputs(
3592            account.id(),
3593            target_height,
3594            ConfirmationsPolicy::MIN,
3595            CoinbaseFilter::AllTransparentOutputs,
3596            None,
3597            TargetValue::AtLeast(target),
3598            usize::MAX,
3599            &StandardFeeRule::Zip317,
3600            LockFilter::Policy(&LockedInputPolicy::Exclude),
3601        )
3602        .expect("value-bounded gather should succeed");
3603
3604    // The gather should return enough UTXOs to cover the target post-fee, not all 50.
3605    // Each UTXO is 10_000 zats. Under ZIP 317, `k` P2PKH inputs cost
3606    // `5_000 * max(2, k)` zats in marginal fee, so the post-fee value of the first `k`
3607    // gathered UTXOs is `10_000 * k - 5_000 * max(2, k)`. This first reaches the
3608    // 30_000-zat target at `k = 6` (60_000 - 30_000 = 30_000), one more than the 5
3609    // UTXOs (50_000 - 25_000 = 25_000) that would still fall short.
3610    assert!(
3611        !bound.is_empty(),
3612        "value-bounded gather should return at least one UTXO",
3613    );
3614    assert_eq!(
3615        bound.len(),
3616        6,
3617        "value-bounded gather should return exactly 6 UTXOs (10_000 zats each) to cover \
3618         30_000 zats net of the ZIP 317 marginal fee for 6 P2PKH inputs",
3619    );
3620    assert!(
3621        bound.len() < n_dust,
3622        "value-bounded gather should not return all {n_dust} UTXOs (returned {})",
3623        bound.len(),
3624    );
3625    // The summed value should meet the target.
3626    let total: Zatoshis = bound
3627        .iter()
3628        .map(|u| u.value())
3629        .fold(Zatoshis::ZERO, |acc, v| (acc + v).unwrap());
3630    assert!(
3631        total >= target,
3632        "value-bounded gather should cover the target (got {}, want >= {})",
3633        u64::from(total),
3634        u64::from(target),
3635    );
3636
3637    // AllFunds should return all eligible UTXOs.
3638    let all = st
3639        .wallet()
3640        .select_spendable_transparent_outputs(
3641            account.id(),
3642            target_height,
3643            ConfirmationsPolicy::MIN,
3644            CoinbaseFilter::AllTransparentOutputs,
3645            None,
3646            TargetValue::AllFunds(MaxSpendMode::MaxSpendable),
3647            usize::MAX,
3648            &StandardFeeRule::Zip317,
3649            LockFilter::Policy(&LockedInputPolicy::Exclude),
3650        )
3651        .expect("AllFunds gather should succeed");
3652    assert_eq!(all.len(), n_dust);
3653}
3654
3655/// Tests that [`WalletWrite::reserve_next_n_internal_addresses`] reserves sequential
3656/// internal-scope (change) addresses, that reservation observes the internal-scope gap
3657/// limit, and that internal-scope reservations are accounted independently of
3658/// ephemeral-scope reservations.
3659///
3660/// This test expects the data store to be configured with the default gap limits, under
3661/// which the internal-scope gap limit is 5.
3662///
3663/// The `is_reached_gap_limit` predicate must return `true` if and only if the provided
3664/// error is the backend's exact "reached gap limit" error variant, the scope reported by
3665/// that error is [`TransparentKeyScope::INTERNAL`], and the address index reported by that
3666/// error equals the provided expected index. It must not match any other error. (A
3667/// predicate is used because this test cannot name the backend's concrete error type
3668/// without inverting the crate dependency.)
3669pub fn reserve_next_n_internal_addresses_gap_limit<DSF>(
3670    dsf: DSF,
3671    cache: impl TestCache,
3672    is_reached_gap_limit: impl Fn(
3673        &<DSF::DataStore as crate::data_api::WalletRead>::Error,
3674        DSF::AccountId,
3675        u32,
3676    ) -> bool,
3677) where
3678    DSF: DataStoreFactory,
3679{
3680    let mut st = TestBuilder::new()
3681        .with_data_store_factory(dsf)
3682        .with_block_cache(cache)
3683        .with_account_from_sapling_activation(BlockHash([0; 32]))
3684        .build();
3685    let account_id = st.test_account().cloned().unwrap().id();
3686
3687    // Seed the chain so that a chain height is known; address reservation records the
3688    // exposure height of each reserved address.
3689    let not_our_key = ExtendedSpendingKey::master(&[])
3690        .expect("the derivation path yields a valid key")
3691        .to_diversifiable_full_viewing_key();
3692    let not_our_value = Zatoshis::const_from_u64(10000);
3693    let (start_height, _, _) =
3694        st.generate_next_block(&not_our_key, AddressType::DefaultExternal, not_our_value);
3695    st.scan_cached_blocks(start_height, 1);
3696
3697    // Reserving internal addresses yields distinct, sequentially-indexed addresses derived
3698    // under the internal (change) key scope.
3699    let reserved = st
3700        .wallet_mut()
3701        .reserve_next_n_internal_addresses(account_id, 3)
3702        .unwrap();
3703    assert_eq!(reserved.len(), 3);
3704    for (i, (_, meta)) in reserved.iter().enumerate() {
3705        assert_eq!(meta.scope(), Some(TransparentKeyScope::INTERNAL));
3706        assert_eq!(
3707            meta.address_index(),
3708            Some(NonHardenedChildIndex::const_from_index(
3709                u32::try_from(i).unwrap()
3710            )),
3711        );
3712    }
3713    // None of the reserved addresses have received funds, so the gap cannot advance: with
3714    // the default internal-scope gap limit of 5, only two more addresses may be reserved.
3715    // Reservation continues at the next sequential indices, so the returned addresses are
3716    // distinct from those of the first batch.
3717    let more = st
3718        .wallet_mut()
3719        .reserve_next_n_internal_addresses(account_id, 2)
3720        .unwrap();
3721    assert_eq!(more.len(), 2);
3722    for (i, (_, meta)) in more.iter().enumerate() {
3723        assert_eq!(meta.scope(), Some(TransparentKeyScope::INTERNAL));
3724        assert_eq!(
3725            meta.address_index(),
3726            Some(NonHardenedChildIndex::const_from_index(
3727                u32::try_from(reserved.len() + i).unwrap()
3728            )),
3729        );
3730    }
3731    let unique_addrs = reserved
3732        .iter()
3733        .chain(more.iter())
3734        .map(|(a, _)| *a)
3735        .collect::<HashSet<_>>();
3736    assert_eq!(unique_addrs.len(), reserved.len() + more.len());
3737
3738    assert_matches!(
3739        st.wallet_mut().reserve_next_n_internal_addresses(account_id, 1),
3740        Err(e) if is_reached_gap_limit(&e, account_id, 5)
3741    );
3742
3743    // Internal-scope reservations must not consume ephemeral-scope gap space.
3744    let ephemeral = st
3745        .wallet_mut()
3746        .reserve_next_n_ephemeral_addresses(account_id, 1)
3747        .unwrap();
3748    assert_eq!(ephemeral[0].1.scope(), Some(TransparentKeyScope::EPHEMERAL),);
3749    assert_eq!(
3750        ephemeral[0].1.address_index(),
3751        Some(NonHardenedChildIndex::const_from_index(0)),
3752    );
3753}
3754
3755/// Tests the full lifecycle of a t->t transfer with transparent change: a change strategy
3756/// configured with [`TransparentChangePolicy::TransparentChangeAllowed`] must propose a
3757/// non-ephemeral transparent change output, and transaction creation must send that change
3758/// to a previously-unexposed internal-scope (change) transparent address of the spending
3759/// account, where it is recorded as received and becomes spendable once mined.
3760///
3761/// [`TransparentChangePolicy::TransparentChangeAllowed`]: crate::fees::TransparentChangePolicy::TransparentChangeAllowed
3762pub fn propose_t2t_with_transparent_change<DSF>(dsf: DSF, cache: impl TestCache)
3763where
3764    DSF: DataStoreFactory,
3765{
3766    let utxo_value = Zatoshis::const_from_u64(100_000);
3767    let transfer_amount = Zatoshis::const_from_u64(40_000);
3768    let (mut st, account, outpoint) = setup_transparent_only_account(dsf, cache, utxo_value);
3769
3770    let network = *st.network();
3771    let request = t2t_request(&network, transfer_amount);
3772
3773    let input_selector = GreedyInputSelector::new();
3774    let change_strategy =
3775        single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Sapling)
3776            .with_transparent_change_policy(TransparentChangePolicy::TransparentChangeAllowed);
3777
3778    let proposal = st
3779        .propose_transfer_with_policy(
3780            account.id(),
3781            &input_selector,
3782            &change_strategy,
3783            request,
3784            ConfirmationsPolicy::MIN,
3785            &SpendPolicy::default().with_transparent(TransparentSpendPolicy::any_account_addr()),
3786        )
3787        .expect("t->t proposal with transparent change must succeed");
3788
3789    // A t->t transfer with non-ephemeral transparent change is a single step.
3790    assert_eq!(proposal.steps().len(), 1);
3791    let step = &proposal.steps().head;
3792    assert_eq!(step.transparent_inputs().len(), 1);
3793    assert_eq!(step.transparent_inputs()[0].outpoint(), &outpoint);
3794    assert!(step.shielded_inputs().is_none());
3795
3796    // Under ZIP 317, one P2PKH input and two P2PKH outputs (the payment plus the change
3797    // output) require `5_000 * max(1, 2) = 10_000` zats in fees.
3798    let expected_fee = Zatoshis::const_from_u64(10_000);
3799    let expected_change = ((utxo_value - transfer_amount).unwrap() - expected_fee).unwrap();
3800    assert_eq!(step.balance().fee_required(), expected_fee);
3801    assert_eq!(
3802        step.balance().proposed_change(),
3803        [ChangeValue::transparent(expected_change)],
3804    );
3805    assert!(!step.balance().proposed_change()[0].is_ephemeral());
3806
3807    // A proposal containing a transparent change output must survive a serialization
3808    // round trip.
3809    super::check_proposal_serialization_roundtrip(&network, st.wallet(), &proposal);
3810
3811    // Creating the transaction should reserve an internal-scope address for the change.
3812    let txids = st
3813        .create_proposed_transactions::<Infallible, _, Infallible, _>(
3814            account.usk(),
3815            OvkPolicy::Sender,
3816            &proposal,
3817        )
3818        .expect("transaction creation must succeed");
3819    assert_eq!(txids.len(), 1);
3820    let txid = txids.head;
3821
3822    // The transaction must be fully transparent, with exactly the payment and change outputs.
3823    let tx = st
3824        .wallet()
3825        .get_transaction(txid)
3826        .unwrap()
3827        .expect("the created transaction is retrievable");
3828    assert!(tx.sapling_bundle().is_none());
3829    #[cfg(feature = "orchard")]
3830    assert!(tx.orchard_bundle().is_none());
3831    let bundle = tx
3832        .transparent_bundle()
3833        .expect("the transaction has a transparent bundle");
3834    assert_eq!(bundle.vin.len(), 1);
3835    assert_eq!(bundle.vout.len(), 2);
3836
3837    // Identify the change output as the output that does not pay the external recipient.
3838    let payment_recipient = TransparentAddress::PublicKeyHash([7u8; 20]);
3839    let change_outputs: Vec<_> = bundle
3840        .vout
3841        .iter()
3842        .filter(|out| out.recipient_address() != Some(payment_recipient))
3843        .collect();
3844    assert_eq!(change_outputs.len(), 1);
3845    let change_output = change_outputs[0];
3846    assert_eq!(change_output.value(), expected_change);
3847    let change_address = change_output
3848        .recipient_address()
3849        .expect("the change output pays a standard P2PKH address");
3850
3851    // The change address must be an internal-scope (change) address of the spending account,
3852    // exposed at the current chain height by having been reserved for change.
3853    let receivers = st
3854        .wallet()
3855        .get_transparent_receivers(account.id(), true, false)
3856        .unwrap();
3857    let change_meta = receivers
3858        .get(&change_address)
3859        .expect("the change address belongs to the spending account");
3860    assert_eq!(change_meta.scope(), Some(TransparentKeyScope::INTERNAL));
3861    let cur_height = st.wallet().chain_height().unwrap().unwrap();
3862    assert_matches!(
3863        change_meta.exposure(),
3864        Exposure::Exposed { at_height, .. } if at_height == cur_height
3865    );
3866
3867    // Mine the transaction; the change output should then be spendable at the change address.
3868    let (h, _) = st.generate_next_block_including(txid);
3869    st.scan_cached_blocks(h, 1);
3870
3871    let mut expected_balance = Balance::ZERO;
3872    expected_balance
3873        .add_spendable_value(expected_change)
3874        .unwrap();
3875    check_balance::<DSF>(
3876        &st,
3877        &account,
3878        &change_address,
3879        ConfirmationsPolicy::MIN,
3880        &expected_balance,
3881    );
3882}
3883
3884/// Tests that when a fully-transparent transaction balances exactly (input value equals
3885/// payments plus the minimum fee), no transparent change output is produced even when the
3886/// change strategy is configured with [`TransparentChangePolicy::TransparentChangeAllowed`].
3887///
3888/// [`TransparentChangePolicy::TransparentChangeAllowed`]: crate::fees::TransparentChangePolicy::TransparentChangeAllowed
3889pub fn propose_t2t_transparent_change_exact_match<DSF>(dsf: DSF, cache: impl TestCache)
3890where
3891    DSF: DataStoreFactory,
3892{
3893    // Under ZIP 317, one P2PKH input and one P2PKH output require the minimum fee of
3894    // 10_000 zats, so a 50_000-zat UTXO exactly covers a 40_000-zat payment.
3895    let utxo_value = Zatoshis::const_from_u64(50_000);
3896    let transfer_amount = Zatoshis::const_from_u64(40_000);
3897    let (mut st, account, _outpoint) = setup_transparent_only_account(dsf, cache, utxo_value);
3898
3899    let network = *st.network();
3900    let request = t2t_request(&network, transfer_amount);
3901
3902    let input_selector = GreedyInputSelector::new();
3903    let change_strategy =
3904        single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Sapling)
3905            .with_transparent_change_policy(TransparentChangePolicy::TransparentChangeAllowed);
3906
3907    let proposal = st
3908        .propose_transfer_with_policy(
3909            account.id(),
3910            &input_selector,
3911            &change_strategy,
3912            request,
3913            ConfirmationsPolicy::MIN,
3914            &SpendPolicy::default().with_transparent(TransparentSpendPolicy::any_account_addr()),
3915        )
3916        .expect("exactly-balanced t->t proposal must succeed");
3917
3918    assert_eq!(proposal.steps().len(), 1);
3919    let step = &proposal.steps().head;
3920    assert_eq!(
3921        step.balance().fee_required(),
3922        Zatoshis::const_from_u64(10_000),
3923    );
3924    assert_eq!(step.balance().proposed_change(), []);
3925}