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
78fn 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 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 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 let value = Zatoshis::const_from_u64(100000);
166 let outpoint = OutPoint::fake();
167 let txout = TxOut::new(value, taddr.script().into());
168
169 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 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 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 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 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 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
258pub fn put_received_unmined_utxo_uses_mempool_height_expiry_delta<DSF>(dsf: DSF)
262where
263 DSF: DataStoreFactory,
264{
265 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 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 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(¬_our_key, AddressType::DefaultExternal, not_our_value);
353 for _ in 1..10 {
354 st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
355 }
356 st.scan_cached_blocks(start_height, 10);
357
358 check_balance::<DSF>(
360 &st,
361 &account,
362 taddr,
363 ConfirmationsPolicy::MIN,
364 &Balance::ZERO,
365 );
366
367 let value = Zatoshis::from_u64(100000).unwrap();
369 let txout = TxOut::new(value, taddr.script().into());
370
371 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 let mut zero_or_one_conf_value = Balance::ZERO;
388
389 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 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 check_balance::<DSF>(
419 &st,
420 &account,
421 taddr,
422 ConfirmationsPolicy::MIN,
423 &Balance::ZERO,
424 );
425
426 let (mined_height, _) = st.generate_next_block_including(txid);
428 st.scan_cached_blocks(mined_height, 1);
429
430 check_balance::<DSF>(
432 &st,
433 &account,
434 taddr,
435 ConfirmationsPolicy::MIN,
436 &Balance::ZERO,
437 );
438
439 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 check_balance::<DSF>(
447 &st,
448 &account,
449 taddr,
450 ConfirmationsPolicy::MIN,
451 &Balance::ZERO,
452 );
453
454 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
472pub fn shielding_many_transparent_utxos<DSF>(dsf: DSF, cache: impl TestCache)
485where
486 DSF: DataStoreFactory,
487{
488 const NUM_UTXOS: usize = 160;
490 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 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(¬_our_key, AddressType::DefaultExternal, not_our_value);
514 for _ in 1..10 {
515 st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
516 }
517 st.scan_cached_blocks(start_height, 10);
518
519 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 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 check_balance::<DSF>(
560 &st,
561 &account,
562 taddr,
563 ConfirmationsPolicy::MIN,
564 &Balance::ZERO,
565 );
566}
567
568pub 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 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 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 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 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 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 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
690pub fn shielding_transparent_input_cap<DSF>(dsf: DSF, cache: impl TestCache)
694where
695 DSF: DataStoreFactory,
696{
697 const BLOCK_SPACE_PERCENT: u32 = 1;
699 const CAP: usize = 133;
700 const NUM_UTXOS: usize = CAP + 7; 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 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(¬_our_key, AddressType::DefaultExternal, not_our_value);
725 for _ in 1..10 {
726 st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
727 }
728 st.scan_cached_blocks(start_height, 10);
729
730 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 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 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
785pub 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 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(¬_our_key, AddressType::DefaultExternal, not_our_value);
813 for _ in 1..10 {
814 st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
815 }
816 st.scan_cached_blocks(start_height, 10);
817
818 check_balance::<DSF>(
820 &st as &TestState<_, DSF::DataStore, _>,
821 &account,
822 taddr,
823 ConfirmationsPolicy::MIN,
824 &Balance::ZERO,
825 );
826
827 let value = Zatoshis::from_u64(100000).unwrap();
829 let txout = TxOut::new(value, taddr.script().into());
830
831 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 let mut zero_or_one_conf_value = Balance::ZERO;
848
849 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 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 ¬_confirmed_yet_value,
874 );
875
876 st.generate_empty_block();
878
879 st.scan_cached_blocks(height, 1);
881
882 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
896fn 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 BlockHeight::from(0),
924 Some(coinbase_bundle),
926 None,
927 None,
928 None,
929 )
930 .freeze()
931 .unwrap()
932}
933
934fn 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
951pub 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 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(¶ms, st.wallet_mut(), &coinbase_tx, Some(h)).unwrap();
979
980 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 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 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 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 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
1076pub 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 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(¶ms, st.wallet_mut(), &coinbase_tx, Some(h)).unwrap();
1105
1106 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 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 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 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 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 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 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 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 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 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 let query_height = st.wallet().utxo_query_height(account_uuid).unwrap();
1279 assert_eq!(query_height, h0);
1280}
1281
1282#[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#[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 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 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#[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 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#[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 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 assert_matches!(
1425 st.wallet_mut()
1426 .import_standalone_transparent_address(account2_id, taddr),
1427 Err(_)
1428 );
1429}
1430
1431#[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 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 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 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 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#[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 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 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 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#[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#[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 assert_matches!(
1607 st.wallet_mut()
1608 .import_standalone_transparent_pubkey(account_id, pubkey),
1609 Ok(_)
1610 );
1611
1612 let receivers_before = st
1614 .wallet()
1615 .get_transparent_receivers(account_id, false, true)
1616 .unwrap();
1617
1618 assert_matches!(
1620 st.wallet_mut()
1621 .import_standalone_transparent_pubkey(account_id, pubkey),
1622 Ok(_)
1623 );
1624
1625 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#[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 assert_matches!(
1661 st.wallet_mut()
1662 .import_standalone_transparent_pubkey(account1_id, pubkey),
1663 Ok(_)
1664 );
1665
1666 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 assert_matches!(
1685 st.wallet_mut()
1686 .import_standalone_transparent_pubkey(account2_id, pubkey),
1687 Err(_)
1688 );
1689}
1690
1691#[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 st.wallet_mut()
1711 .import_standalone_transparent_pubkey(account_id, pubkey)
1712 .unwrap();
1713
1714 let taddr = TransparentAddress::from_pubkey(&pubkey);
1716
1717 let height = birthday + 1000;
1718 st.wallet_mut().update_chain_tip(height).unwrap();
1719
1720 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 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 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#[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 let secret_key = SecretKey::from_secret_bytes([1u8; 32]).expect("valid secret key");
1780 let pubkey = secret_key.public_key();
1781
1782 st.wallet_mut()
1784 .import_standalone_transparent_pubkey(account_id, pubkey)
1785 .unwrap();
1786
1787 let taddr = TransparentAddress::from_pubkey(&pubkey);
1789
1790 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(¬_our_key, AddressType::DefaultExternal, not_our_value);
1797 for _ in 1..10 {
1798 st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
1799 }
1800 st.scan_cached_blocks(start_height, 10);
1801
1802 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 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 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 check_balance::<DSF>(
1857 &st,
1858 &account,
1859 &taddr,
1860 ConfirmationsPolicy::MIN,
1861 &Balance::ZERO,
1862 );
1863
1864 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#[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 assert_matches!(
1902 st.wallet_mut()
1903 .import_standalone_transparent_script(account_id, redeem_script.clone()),
1904 Ok(_)
1905 );
1906
1907 let receivers = st
1909 .wallet()
1910 .get_transparent_receivers(account_id, false, true)
1911 .unwrap();
1912
1913 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#[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 assert_matches!(
1943 st.wallet_mut()
1944 .import_standalone_transparent_script(account_id, redeem_script.clone()),
1945 Ok(_)
1946 );
1947
1948 let receivers_before = st
1950 .wallet()
1951 .get_transparent_receivers(account_id, false, true)
1952 .unwrap();
1953
1954 assert_matches!(
1956 st.wallet_mut()
1957 .import_standalone_transparent_script(account_id, redeem_script.clone()),
1958 Ok(_)
1959 );
1960
1961 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#[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 assert_matches!(
1997 st.wallet_mut()
1998 .import_standalone_transparent_script(account1_id, redeem_script.clone()),
1999 Ok(_)
2000 );
2001
2002 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 assert_matches!(
2021 st.wallet_mut()
2022 .import_standalone_transparent_script(account2_id, redeem_script),
2023 Err(_)
2024 );
2025}
2026
2027#[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 st.wallet_mut()
2046 .import_standalone_transparent_script(account_id, redeem_script.clone())
2047 .unwrap();
2048
2049 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 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 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 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#[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 let (redeem_script, secret_key) = build_test_redeem_script();
2115
2116 st.wallet_mut()
2118 .import_standalone_transparent_script(account_id, redeem_script.clone())
2119 .unwrap();
2120
2121 let script_pubkey = sh(&redeem_script);
2123 let taddr = TransparentAddress::from_script_pubkey(&script_pubkey).expect("valid P2SH address");
2124
2125 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(¬_our_key, AddressType::DefaultExternal, not_our_value);
2132 for _ in 1..10 {
2133 st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
2134 }
2135 st.scan_cached_blocks(start_height, 10);
2136
2137 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 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 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 check_balance::<DSF>(
2192 &st,
2193 &account,
2194 &taddr,
2195 ConfirmationsPolicy::MIN,
2196 &Balance::ZERO,
2197 );
2198
2199 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
2220pub 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 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 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 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 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 st.wallet_mut()
2297 .mark_transparent_addresses_exposed(&[])
2298 .unwrap();
2299}
2300
2301pub 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 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 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 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
2394pub 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
2413fn 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
2450pub 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 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 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 st.scan_cached_blocks(received_height, 1);
2493
2494 let target_height = TargetHeight::from(spend_height + 1);
2495
2496 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 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
2546pub 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 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
2600fn 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#[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 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 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 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
2714pub 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 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 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
2782pub 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 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
2861pub 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 let (received, funded_outpoint) =
2889 scan_transparent_payment_block(st.wallet(), &network, account_id, h, 1, &[], value, &taddr);
2890
2891 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 st.wallet_mut()
2923 .put_blocks(
2924 &ChainState::empty(h - 1, BlockHash([0; 32])),
2925 vec![received],
2926 )
2927 .unwrap();
2928
2929 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
2944pub 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 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#[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 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(¬_our_key, AddressType::DefaultExternal, not_our_value);
3052 for _ in 1..10 {
3053 st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
3054 }
3055 st.scan_cached_blocks(start_height, 10);
3056
3057 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
3077fn 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
3088pub 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
3124pub 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 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 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
3175pub 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 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(¬_our_key, AddressType::DefaultExternal, not_our_value);
3214 for _ in 1..10 {
3215 st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
3216 }
3217 st.scan_cached_blocks(start_height, 10);
3218
3219 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 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 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
3320pub fn propose_transfer_transparent_input_cap<DSF>(dsf: DSF, cache: impl TestCache)
3330where
3331 DSF: DataStoreFactory,
3332{
3333 const BLOCK_SPACE_PERCENT: u32 = 1;
3335 const CAP: usize = 133;
3336 const NUM_UTXOS: usize = CAP + 7; 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 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(¬_our_key, AddressType::DefaultExternal, not_our_value);
3360 for _ in 1..10 {
3361 st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
3362 }
3363 st.scan_cached_blocks(start_height, 10);
3364
3365 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 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 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
3421pub 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 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(¬_our_key, AddressType::DefaultExternal, not_our_value);
3443 for _ in 1..10 {
3444 st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
3445 }
3446 st.scan_cached_blocks(start_height, 10);
3447
3448 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 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
3526pub 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 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(¬_our_key, AddressType::DefaultExternal, not_our_value);
3556 for _ in 1..10 {
3557 st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
3558 }
3559 st.scan_cached_blocks(start_height, 10);
3560
3561 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 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 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 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 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
3655pub 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 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(¬_our_key, AddressType::DefaultExternal, not_our_value);
3695 st.scan_cached_blocks(start_height, 1);
3696
3697 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 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 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
3755pub 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 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 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 super::check_proposal_serialization_roundtrip(&network, st.wallet(), &proposal);
3810
3811 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 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 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 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 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
3884pub fn propose_t2t_transparent_change_exact_match<DSF>(dsf: DSF, cache: impl TestCache)
3890where
3891 DSF: DataStoreFactory,
3892{
3893 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}