1use core::marker::PhantomData;
8
9use zcash_primitives::transaction::fees::{FeeRule, transparent, zip317 as prim_zip317};
10use zcash_protocol::{
11 ShieldedPool,
12 consensus::{self, BlockHeight},
13 memo::MemoBytes,
14 value::{BalanceError, Zatoshis},
15};
16
17use crate::{
18 data_api::{
19 AccountMeta, ChangeReceivers, InputSource, NoteFilter,
20 anchor_retention::PoolMigrationParams,
21 wallet::{
22 TargetHeight,
23 input_selection::{LockFilter, LockedInputPolicy},
24 },
25 },
26 fees::StandardFeeRule,
27};
28
29use super::{
30 ChangeError, ChangeStrategy, DustOutputPolicy, EphemeralBalance, MetaSource, SplitPolicy,
31 TransactionBalance,
32 common::{SinglePoolBalanceConfig, single_pool_output_balance},
33 sapling as sapling_fees,
34};
35
36#[cfg(feature = "transparent-inputs")]
37use super::TransparentChangePolicy;
38#[cfg(feature = "orchard")]
39use {super::orchard as orchard_fees, zcash_primitives::transaction::builder::BundlePadding};
40
41pub trait Zip317FeeRule: FeeRule {
44 fn marginal_fee(&self) -> Zatoshis;
46
47 fn grace_actions(&self) -> usize;
49}
50
51impl Zip317FeeRule for prim_zip317::FeeRule {
52 fn marginal_fee(&self) -> Zatoshis {
53 self.marginal_fee()
54 }
55
56 fn grace_actions(&self) -> usize {
57 self.grace_actions()
58 }
59}
60
61impl Zip317FeeRule for StandardFeeRule {
62 fn marginal_fee(&self) -> Zatoshis {
63 prim_zip317::FeeRule::standard().marginal_fee()
64 }
65
66 fn grace_actions(&self) -> usize {
67 prim_zip317::FeeRule::standard().grace_actions()
68 }
69}
70
71pub struct SingleOutputChangeStrategy<R, I> {
76 fee_rule: R,
77 change_memo: Option<MemoBytes>,
78 fallback_change_pool: ShieldedPool,
79 dust_output_policy: DustOutputPolicy,
80 #[cfg(feature = "transparent-inputs")]
81 transparent_change_policy: TransparentChangePolicy,
82 meta_source: PhantomData<I>,
83}
84
85impl<R, I> SingleOutputChangeStrategy<R, I> {
86 pub fn new(
92 fee_rule: R,
93 change_memo: Option<MemoBytes>,
94 fallback_change_pool: ShieldedPool,
95 dust_output_policy: DustOutputPolicy,
96 ) -> Self {
97 Self {
98 fee_rule,
99 change_memo,
100 fallback_change_pool,
101 dust_output_policy,
102 #[cfg(feature = "transparent-inputs")]
103 transparent_change_policy: TransparentChangePolicy::ShieldChange,
104 meta_source: PhantomData,
105 }
106 }
107
108 #[cfg(feature = "transparent-inputs")]
115 pub fn with_transparent_change_policy(
116 mut self,
117 transparent_change_policy: TransparentChangePolicy,
118 ) -> Self {
119 self.transparent_change_policy = transparent_change_policy;
120 self
121 }
122}
123
124impl<R, I> ChangeStrategy for SingleOutputChangeStrategy<R, I>
125where
126 R: Zip317FeeRule + Clone,
127 I: MetaSource,
128 <R as FeeRule>::Error: From<BalanceError>,
129{
130 type FeeRule = R;
131 type Error = <R as FeeRule>::Error;
132 type MetaSource = I;
133 type AccountMetaT = ChangeReceivers;
134
135 fn fee_rule(&self) -> &Self::FeeRule {
136 &self.fee_rule
137 }
138
139 fn fetch_wallet_meta(
140 &self,
141 meta_source: &Self::MetaSource,
142 account: <Self::MetaSource as MetaSource>::AccountId,
143 _target_height: TargetHeight,
144 _exclude: &[<Self::MetaSource as MetaSource>::NoteRef],
145 ) -> Result<Self::AccountMetaT, <Self::MetaSource as MetaSource>::Error> {
146 meta_source.change_receivers(account)
147 }
148
149 fn compute_balance<P: consensus::Parameters, NoteRefT: Clone>(
150 &self,
151 params: &P,
152 target_height: TargetHeight,
153 anchor_height: BlockHeight,
154 zip318: &PoolMigrationParams,
155 transparent_inputs: &[impl transparent::InputView],
156 transparent_outputs: &[impl transparent::OutputView],
157 sapling: &impl sapling_fees::BundleView<NoteRefT>,
158 #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
159 #[cfg(feature = "orchard")] ironwood: &impl orchard_fees::BundleView<NoteRefT>,
160 ephemeral_balance: Option<EphemeralBalance>,
161 wallet_meta: &Self::AccountMetaT,
162 ) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
163 let split_policy = SplitPolicy::single_output();
164 let cfg = SinglePoolBalanceConfig::new(
165 params,
166 &self.fee_rule,
167 &self.dust_output_policy,
168 self.fee_rule.marginal_fee(),
169 &split_policy,
170 self.fallback_change_pool,
171 #[cfg(feature = "transparent-inputs")]
172 self.transparent_change_policy,
173 self.fee_rule.marginal_fee(),
174 self.fee_rule.grace_actions(),
175 );
176
177 single_pool_output_balance(
178 cfg,
179 None,
180 wallet_meta,
181 target_height,
182 transparent_inputs,
183 transparent_outputs,
184 sapling,
185 #[cfg(feature = "orchard")]
186 orchard,
187 #[cfg(feature = "orchard")]
188 ironwood,
189 #[cfg(feature = "orchard")]
193 BundlePadding::DEFAULT,
194 anchor_height,
195 zip318,
196 self.change_memo.as_ref(),
197 ephemeral_balance,
198 )
199 }
200}
201
202pub struct MultiOutputChangeStrategy<R, I> {
205 fee_rule: R,
206 change_memo: Option<MemoBytes>,
207 fallback_change_pool: ShieldedPool,
208 dust_output_policy: DustOutputPolicy,
209 split_policy: SplitPolicy,
210 #[cfg(feature = "transparent-inputs")]
211 transparent_change_policy: TransparentChangePolicy,
212 meta_source: PhantomData<I>,
213}
214
215impl<R, I> MultiOutputChangeStrategy<R, I> {
216 pub fn new(
228 fee_rule: R,
229 change_memo: Option<MemoBytes>,
230 fallback_change_pool: ShieldedPool,
231 dust_output_policy: DustOutputPolicy,
232 split_policy: SplitPolicy,
233 ) -> Self {
234 Self {
235 fee_rule,
236 change_memo,
237 fallback_change_pool,
238 dust_output_policy,
239 split_policy,
240 #[cfg(feature = "transparent-inputs")]
241 transparent_change_policy: TransparentChangePolicy::ShieldChange,
242 meta_source: PhantomData,
243 }
244 }
245
246 #[cfg(feature = "transparent-inputs")]
255 pub fn with_transparent_change_policy(
256 mut self,
257 transparent_change_policy: TransparentChangePolicy,
258 ) -> Self {
259 self.transparent_change_policy = transparent_change_policy;
260 self
261 }
262}
263
264impl<R, I> ChangeStrategy for MultiOutputChangeStrategy<R, I>
265where
266 R: Zip317FeeRule + Clone,
267 I: InputSource,
268 <R as FeeRule>::Error: From<BalanceError>,
269{
270 type FeeRule = R;
271 type Error = <R as FeeRule>::Error;
272 type MetaSource = I;
273 type AccountMetaT = AccountMeta;
274
275 fn fee_rule(&self) -> &Self::FeeRule {
276 &self.fee_rule
277 }
278
279 fn fetch_wallet_meta(
280 &self,
281 meta_source: &Self::MetaSource,
282 account: <Self::MetaSource as InputSource>::AccountId,
283 target_height: TargetHeight,
284 exclude: &[<Self::MetaSource as InputSource>::NoteRef],
285 ) -> Result<Self::AccountMetaT, <Self::MetaSource as InputSource>::Error> {
286 let note_selector = NoteFilter::ExceedsMinValue(
287 self.split_policy
288 .min_split_output_value()
289 .unwrap_or(SplitPolicy::MIN_NOTE_VALUE),
290 );
291
292 meta_source.get_account_metadata(
296 account,
297 ¬e_selector,
298 target_height,
299 exclude,
300 LockFilter::Policy(&LockedInputPolicy::Exclude),
301 )
302 }
303
304 fn compute_balance<P: consensus::Parameters, NoteRefT: Clone>(
305 &self,
306 params: &P,
307 target_height: TargetHeight,
308 anchor_height: BlockHeight,
309 zip318: &PoolMigrationParams,
310 transparent_inputs: &[impl transparent::InputView],
311 transparent_outputs: &[impl transparent::OutputView],
312 sapling: &impl sapling_fees::BundleView<NoteRefT>,
313 #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
314 #[cfg(feature = "orchard")] ironwood: &impl orchard_fees::BundleView<NoteRefT>,
315 ephemeral_balance: Option<EphemeralBalance>,
316 wallet_meta: &Self::AccountMetaT,
317 ) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
318 let cfg = SinglePoolBalanceConfig::new(
319 params,
320 &self.fee_rule,
321 &self.dust_output_policy,
322 self.fee_rule.marginal_fee(),
323 &self.split_policy,
324 self.fallback_change_pool,
325 #[cfg(feature = "transparent-inputs")]
326 self.transparent_change_policy,
327 self.fee_rule.marginal_fee(),
328 self.fee_rule.grace_actions(),
329 );
330
331 single_pool_output_balance(
332 cfg,
333 Some(wallet_meta),
334 wallet_meta.change_receivers(),
335 target_height,
336 transparent_inputs,
337 transparent_outputs,
338 sapling,
339 #[cfg(feature = "orchard")]
340 orchard,
341 #[cfg(feature = "orchard")]
342 ironwood,
343 #[cfg(feature = "orchard")]
347 BundlePadding::DEFAULT,
348 anchor_height,
349 zip318,
350 self.change_memo.as_ref(),
351 ephemeral_balance,
352 )
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use crate::fees::tests::all_change_receivers;
359 #[cfg(any(feature = "orchard", feature = "transparent-inputs"))]
361 use crate::fees::sapling as sapling_fees;
362
363 #[cfg(feature = "transparent-inputs")]
364 use {
365 crate::{
366 data_api::TransparentChangeKind,
367 fees::{EphemeralBalance, TransparentChangePolicy},
368 },
369 ::transparent::{address::TransparentAddress, bundle::OutPoint},
370 zcash_primitives::transaction::fees::zip317::MINIMUM_FEE,
371 };
372
373 #[cfg(feature = "orchard")]
374 use {
375 crate::{
376 data_api::wallet::{TargetHeight, input_selection::OrchardPayment},
377 fees::{orchard as orchard_fees, tests::TestOrchardInput},
378 },
379 zcash_protocol::zip318::{AnchorBucketInterval, MAX_RESIDUAL_VALUE},
380 };
381
382 use crate::{
383 data_api::{
384 AccountMeta, ChangeReceivers, PoolMeta,
385 anchor_retention::{AnchorRetentionInterval, PoolMigrationParams},
386 testing::MockWalletDb,
387 wallet::input_selection::SaplingPayment,
388 },
389 fees::{
390 ChangeError, ChangeStrategy, ChangeValue, DustAction, DustOutputPolicy, SplitPolicy,
391 tests::{TestSaplingInput, TestTransparentInput},
392 zip317::MultiOutputChangeStrategy,
393 },
394 };
395 use core::{convert::Infallible, num::NonZeroUsize};
396 use zcash_protocol::{
397 PoolType, ShieldedPool,
398 consensus::{BlockHeight, Network, NetworkUpgrade, Parameters},
399 value::Zatoshis,
400 };
401
402 use ::transparent::{address::Script, bundle::TxOut};
403 use zcash_primitives::transaction::fees::zip317::FeeRule as Zip317FeeRule;
404
405 use super::SingleOutputChangeStrategy;
406
407 #[test]
408 fn change_without_dust() {
409 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
410 Zip317FeeRule::standard(),
411 None,
412 ShieldedPool::Sapling,
413 DustOutputPolicy::default(),
414 );
415
416 let result = change_strategy.compute_balance(
418 &Network::TestNetwork,
419 Network::TestNetwork
420 .activation_height(NetworkUpgrade::Nu5)
421 .unwrap()
422 .into(),
423 BlockHeight::from_u32(1),
424 &PoolMigrationParams::new(
425 zcash_protocol::zip318::BlockSpacing::UNIFORM,
426 AnchorRetentionInterval::ZIP_318,
427 ),
428 &[] as &[TestTransparentInput],
429 &[] as &[TxOut],
430 &(
431 sapling::builder::BundleType::DEFAULT,
432 &[TestSaplingInput {
433 note_id: 0,
434 value: Zatoshis::const_from_u64(55000),
435 }][..],
436 &[SaplingPayment::new(Zatoshis::const_from_u64(40000))][..],
437 ),
438 #[cfg(feature = "orchard")]
439 &orchard_fees::EmptyBundleView,
440 #[cfg(feature = "orchard")]
441 &orchard_fees::EmptyBundleView,
442 None,
443 &all_change_receivers(),
444 );
445
446 assert_matches!(
447 result,
448 Ok(balance) if
449 balance.proposed_change() == [ChangeValue::sapling(Zatoshis::const_from_u64(5000), None)] &&
450 balance.fee_required() == Zatoshis::const_from_u64(10000)
451 );
452 }
453
454 #[test]
455 fn change_without_dust_multi() {
456 let change_strategy = MultiOutputChangeStrategy::<_, MockWalletDb>::new(
457 Zip317FeeRule::standard(),
458 None,
459 ShieldedPool::Sapling,
460 DustOutputPolicy::default(),
461 SplitPolicy::with_min_output_value(
462 NonZeroUsize::new(5).unwrap(),
463 Zatoshis::const_from_u64(100_0000),
464 ),
465 );
466
467 {
468 let balance = |existing_notes, total| {
470 change_strategy.compute_balance(
471 &Network::TestNetwork,
472 Network::TestNetwork
473 .activation_height(NetworkUpgrade::Nu5)
474 .unwrap()
475 .into(),
476 BlockHeight::from_u32(1),
477 &PoolMigrationParams::new(
478 zcash_protocol::zip318::BlockSpacing::UNIFORM,
479 AnchorRetentionInterval::ZIP_318,
480 ),
481 &[] as &[TestTransparentInput],
482 &[] as &[TxOut],
483 &(
484 sapling::builder::BundleType::DEFAULT,
485 &[TestSaplingInput {
486 note_id: 0,
487 value: Zatoshis::const_from_u64(750_0000),
488 }][..],
489 &[SaplingPayment::new(Zatoshis::const_from_u64(100_0000))][..],
490 ),
491 #[cfg(feature = "orchard")]
492 &orchard_fees::EmptyBundleView,
493 #[cfg(feature = "orchard")]
494 &orchard_fees::EmptyBundleView,
495 None,
496 &AccountMeta::new(
497 Some(PoolMeta::new(existing_notes, total)),
498 None,
499 None,
500 all_change_receivers(),
501 ),
502 )
503 };
504
505 assert_matches!(
506 balance(0, Zatoshis::ZERO),
507 Ok(balance) if
508 balance.proposed_change() == [
509 ChangeValue::sapling(Zatoshis::const_from_u64(129_4000), None),
510 ChangeValue::sapling(Zatoshis::const_from_u64(129_4000), None),
511 ChangeValue::sapling(Zatoshis::const_from_u64(129_4000), None),
512 ChangeValue::sapling(Zatoshis::const_from_u64(129_4000), None),
513 ChangeValue::sapling(Zatoshis::const_from_u64(129_4000), None),
514 ] &&
515 balance.fee_required() == Zatoshis::const_from_u64(30000)
516 );
517
518 assert_matches!(
519 balance(2, Zatoshis::const_from_u64(100_0000)),
520 Ok(balance) if
521 balance.proposed_change() == [
522 ChangeValue::sapling(Zatoshis::const_from_u64(216_0000), None),
523 ChangeValue::sapling(Zatoshis::const_from_u64(216_0000), None),
524 ChangeValue::sapling(Zatoshis::const_from_u64(216_0000), None),
525 ] &&
526 balance.fee_required() == Zatoshis::const_from_u64(20000)
527 );
528 }
529
530 {
531 let result = change_strategy.compute_balance(
534 &Network::TestNetwork,
535 Network::TestNetwork
536 .activation_height(NetworkUpgrade::Nu5)
537 .unwrap()
538 .into(),
539 BlockHeight::from_u32(1),
540 &PoolMigrationParams::new(
541 zcash_protocol::zip318::BlockSpacing::UNIFORM,
542 AnchorRetentionInterval::ZIP_318,
543 ),
544 &[] as &[TestTransparentInput],
545 &[] as &[TxOut],
546 &(
547 sapling::builder::BundleType::DEFAULT,
548 &[TestSaplingInput {
549 note_id: 0,
550 value: Zatoshis::const_from_u64(600_0000),
551 }][..],
552 &[SaplingPayment::new(Zatoshis::const_from_u64(100_0000))][..],
553 ),
554 #[cfg(feature = "orchard")]
555 &orchard_fees::EmptyBundleView,
556 #[cfg(feature = "orchard")]
557 &orchard_fees::EmptyBundleView,
558 None,
559 &AccountMeta::new(
560 Some(PoolMeta::new(0, Zatoshis::ZERO)),
561 Some(PoolMeta::new(0, Zatoshis::ZERO)),
562 None,
563 all_change_receivers(),
564 ),
565 );
566
567 assert_matches!(
568 result,
569 Ok(balance) if
570 balance.proposed_change() == [
571 ChangeValue::sapling(Zatoshis::const_from_u64(124_3750), None),
572 ChangeValue::sapling(Zatoshis::const_from_u64(124_3750), None),
573 ChangeValue::sapling(Zatoshis::const_from_u64(124_3750), None),
574 ChangeValue::sapling(Zatoshis::const_from_u64(124_3750), None),
575 ] &&
576 balance.fee_required() == Zatoshis::const_from_u64(25000)
577 );
578 }
579
580 {
581 let result = change_strategy.compute_balance(
584 &Network::TestNetwork,
585 Network::TestNetwork
586 .activation_height(NetworkUpgrade::Nu5)
587 .unwrap()
588 .into(),
589 BlockHeight::from_u32(1),
590 &PoolMigrationParams::new(
591 zcash_protocol::zip318::BlockSpacing::UNIFORM,
592 AnchorRetentionInterval::ZIP_318,
593 ),
594 &[] as &[TestTransparentInput],
595 &[] as &[TxOut],
596 &(
597 sapling::builder::BundleType::DEFAULT,
598 &[TestSaplingInput {
599 note_id: 0,
600 value: Zatoshis::const_from_u64(50000),
601 }][..],
602 &[SaplingPayment::new(Zatoshis::const_from_u64(40000))][..],
603 ),
604 #[cfg(feature = "orchard")]
605 &orchard_fees::EmptyBundleView,
606 #[cfg(feature = "orchard")]
607 &orchard_fees::EmptyBundleView,
608 None,
609 &AccountMeta::new(
611 Some(PoolMeta::new(0, Zatoshis::ZERO)),
612 Some(PoolMeta::new(0, Zatoshis::ZERO)),
613 None,
614 all_change_receivers(),
615 ),
616 );
617
618 assert_matches!(
619 result,
620 Ok(balance) if
621 balance.proposed_change() == [ChangeValue::sapling(Zatoshis::ZERO, None)] &&
622 balance.fee_required() == Zatoshis::const_from_u64(10000)
623 );
624 }
625
626 {
627 let result = change_strategy.compute_balance(
629 &Network::TestNetwork,
630 Network::TestNetwork
631 .activation_height(NetworkUpgrade::Nu5)
632 .unwrap()
633 .into(),
634 BlockHeight::from_u32(1),
635 &PoolMigrationParams::new(
636 zcash_protocol::zip318::BlockSpacing::UNIFORM,
637 AnchorRetentionInterval::ZIP_318,
638 ),
639 &[] as &[TestTransparentInput],
640 &[] as &[TxOut],
641 &(
642 sapling::builder::BundleType::DEFAULT,
643 &[TestSaplingInput {
644 note_id: 0,
645 value: Zatoshis::const_from_u64(50000),
646 }][..],
647 &[SaplingPayment::new(Zatoshis::const_from_u64(40001))][..],
648 ),
649 #[cfg(feature = "orchard")]
650 &orchard_fees::EmptyBundleView,
651 #[cfg(feature = "orchard")]
652 &orchard_fees::EmptyBundleView,
653 None,
654 &AccountMeta::new(
656 Some(PoolMeta::new(0, Zatoshis::ZERO)),
657 Some(PoolMeta::new(0, Zatoshis::ZERO)),
658 None,
659 all_change_receivers(),
660 ),
661 );
662
663 assert_matches!(
664 result,
665 Err(ChangeError::InsufficientFunds { available, required })
666 if available == Zatoshis::const_from_u64(50000)
667 && required == Zatoshis::const_from_u64(50001)
668 );
669 }
670
671 {
672 let result = change_strategy.compute_balance(
679 &Network::TestNetwork,
680 Network::TestNetwork
681 .activation_height(NetworkUpgrade::Nu5)
682 .unwrap()
683 .into(),
684 BlockHeight::from_u32(1),
685 &PoolMigrationParams::new(
686 zcash_protocol::zip318::BlockSpacing::UNIFORM,
687 AnchorRetentionInterval::ZIP_318,
688 ),
689 &[] as &[TestTransparentInput],
690 &[] as &[TxOut],
691 &(
692 sapling::builder::BundleType::DEFAULT,
693 &[TestSaplingInput {
694 note_id: 0,
695 value: Zatoshis::const_from_u64(50000),
696 }][..],
697 &[
698 SaplingPayment::new(Zatoshis::const_from_u64(30000)),
699 SaplingPayment::new(Zatoshis::const_from_u64(10000)),
700 ][..],
701 ),
702 #[cfg(feature = "orchard")]
703 &orchard_fees::EmptyBundleView,
704 #[cfg(feature = "orchard")]
705 &orchard_fees::EmptyBundleView,
706 None,
707 &AccountMeta::new(
709 Some(PoolMeta::new(0, Zatoshis::ZERO)),
710 Some(PoolMeta::new(0, Zatoshis::ZERO)),
711 None,
712 all_change_receivers(),
713 ),
714 );
715
716 assert_matches!(
717 result,
718 Err(ChangeError::InsufficientFunds { available, required })
719 if available == Zatoshis::const_from_u64(50000)
720 && required == Zatoshis::const_from_u64(55000)
721 );
722 }
723 }
724
725 #[test]
726 #[cfg(feature = "orchard")]
727 fn cross_pool_change_without_dust() {
728 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
729 Zip317FeeRule::standard(),
730 None,
731 ShieldedPool::Orchard,
732 DustOutputPolicy::default(),
733 );
734
735 let result = change_strategy.compute_balance(
737 &Network::TestNetwork,
738 Network::TestNetwork
739 .activation_height(NetworkUpgrade::Nu5)
740 .unwrap()
741 .into(),
742 BlockHeight::from_u32(1),
743 &PoolMigrationParams::new(
744 zcash_protocol::zip318::BlockSpacing::UNIFORM,
745 AnchorRetentionInterval::ZIP_318,
746 ),
747 &[] as &[TestTransparentInput],
748 &[] as &[TxOut],
749 &(
750 sapling::builder::BundleType::DEFAULT,
751 &[TestSaplingInput {
752 note_id: 0,
753 value: Zatoshis::const_from_u64(55000),
754 }][..],
755 &[] as &[Infallible],
756 ),
757 &(
758 ::orchard::bundle::BundleVersion::orchard_v2(),
759 &[] as &[Infallible],
760 &[OrchardPayment::new(Zatoshis::const_from_u64(30000))][..],
761 ),
762 &orchard_fees::EmptyBundleView,
763 None,
764 &all_change_receivers(),
765 );
766
767 assert_matches!(
768 result,
769 Ok(balance) if
770 balance.proposed_change() == [ChangeValue::orchard(Zatoshis::const_from_u64(5000), None)] &&
771 balance.fee_required() == Zatoshis::const_from_u64(20000)
772 );
773 }
774
775 #[test]
776 #[cfg(feature = "orchard")]
777 fn orchard_v3_change_counts_spends_and_outputs_separately() {
778 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
779 Zip317FeeRule::standard(),
780 None,
781 ShieldedPool::Orchard,
782 DustOutputPolicy::default(),
783 );
784
785 let result = change_strategy.compute_balance(
790 &Network::TestNetwork,
791 Network::TestNetwork
792 .activation_height(NetworkUpgrade::Nu6_3)
793 .unwrap()
794 .into(),
795 BlockHeight::from_u32(1),
796 &PoolMigrationParams::new(
797 zcash_protocol::zip318::BlockSpacing::UNIFORM,
798 AnchorRetentionInterval::ZIP_318,
799 ),
800 &[] as &[TestTransparentInput],
801 &[] as &[TxOut],
802 &sapling_fees::EmptyBundleView,
803 &(
804 ::orchard::bundle::BundleVersion::orchard_v3(),
805 &[TestOrchardInput {
806 note_id: 0,
807 value: Zatoshis::const_from_u64(80000),
808 }][..],
809 &[OrchardPayment::new(Zatoshis::const_from_u64(30000))][..],
810 ),
811 &orchard_fees::EmptyBundleView,
812 None,
813 &all_change_receivers(),
814 );
815
816 assert_matches!(
817 result,
818 Ok(balance) if
819 balance.proposed_change() == [ChangeValue::orchard(Zatoshis::const_from_u64(35000), None)] &&
820 balance.fee_required() == Zatoshis::const_from_u64(15000)
821 );
822 }
823
824 #[test]
825 #[cfg(all(feature = "orchard", feature = "transparent-inputs"))]
826 fn orchard_fallback_change_pool_is_promoted_to_ironwood_after_nu6_3() {
827 let change_strategy = MultiOutputChangeStrategy::<_, MockWalletDb>::new(
829 Zip317FeeRule::standard(),
830 None,
831 ShieldedPool::Orchard,
832 DustOutputPolicy::default(),
833 SplitPolicy::with_min_output_value(
834 NonZeroUsize::new(2).unwrap(),
835 Zatoshis::const_from_u64(100_0000),
836 ),
837 );
838
839 let transparent_inputs = [TestTransparentInput {
845 outpoint: OutPoint::fake(),
846 coin: TxOut::new(
847 Zatoshis::const_from_u64(63000),
848 TransparentAddress::PublicKeyHash([0u8; 20]).script().into(),
849 ),
850 }];
851 let transparent_outputs = [TxOut::new(
852 Zatoshis::const_from_u64(40000),
853 Script::default(),
854 )];
855
856 let sapling_view = sapling_fees::EmptyBundleView;
859 let ironwood_view = (
860 ::orchard::bundle::BundleVersion::ironwood_v3(),
861 &[] as &[Infallible],
862 &[] as &[Infallible],
863 );
864
865 let ephemeral_balance = None;
867
868 let wallet_meta = AccountMeta::new(None, None, None, all_change_receivers());
871
872 let pre_nu6_3_orchard_view = (
876 ::orchard::bundle::BundleVersion::orchard_v2(),
877 &[] as &[Infallible],
878 &[] as &[Infallible],
879 );
880 let post_nu6_3_orchard_view = (
881 ::orchard::bundle::BundleVersion::orchard_v3(),
882 &[] as &[Infallible],
883 &[] as &[Infallible],
884 );
885
886 let pre_nu6_3_height: TargetHeight = Network::TestNetwork
887 .activation_height(NetworkUpgrade::Nu5)
888 .unwrap()
889 .into();
890 let post_nu6_3_height: TargetHeight = Network::TestNetwork
891 .activation_height(NetworkUpgrade::Nu6_3)
892 .unwrap()
893 .into();
894
895 let pre_nu6_3_balance = change_strategy.compute_balance::<_, Infallible>(
898 &Network::TestNetwork,
899 pre_nu6_3_height,
900 BlockHeight::from_u32(1),
901 &PoolMigrationParams::new(
902 zcash_protocol::zip318::BlockSpacing::UNIFORM,
903 AnchorRetentionInterval::ZIP_318,
904 ),
905 &transparent_inputs,
906 &transparent_outputs,
907 &sapling_view,
908 &pre_nu6_3_orchard_view,
909 &ironwood_view,
910 ephemeral_balance,
911 &wallet_meta,
912 );
913
914 assert_matches!(
915 pre_nu6_3_balance,
916 Ok(balance) if
917 balance.proposed_change() == [ChangeValue::orchard(Zatoshis::const_from_u64(8000), None)] &&
918 balance.fee_required() == Zatoshis::const_from_u64(15000)
919 );
920
921 let post_nu6_3_balance = change_strategy.compute_balance::<_, Infallible>(
928 &Network::TestNetwork,
929 post_nu6_3_height,
930 BlockHeight::from_u32(1),
931 &PoolMigrationParams::new(
932 zcash_protocol::zip318::BlockSpacing::UNIFORM,
933 AnchorRetentionInterval::ZIP_318,
934 ),
935 &transparent_inputs,
936 &transparent_outputs,
937 &sapling_view,
938 &post_nu6_3_orchard_view,
939 &ironwood_view,
940 ephemeral_balance,
941 &wallet_meta,
942 );
943
944 assert_matches!(
945 post_nu6_3_balance,
946 Ok(balance) if
947 balance.proposed_change() == [ChangeValue::ironwood(Zatoshis::const_from_u64(8000), None)] &&
948 balance.fee_required() == Zatoshis::const_from_u64(15000)
949 );
950 }
951
952 #[test]
956 #[cfg(feature = "orchard")]
957 fn the_change_strategy_records_the_dummy_outputs_it_costed() {
958 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
959 Zip317FeeRule::standard(),
960 None,
961 ShieldedPool::Orchard,
962 DustOutputPolicy::default(),
963 );
964 let zip318 = PoolMigrationParams::new(
965 zcash_protocol::zip318::BlockSpacing::UNIFORM,
966 AnchorRetentionInterval::ZIP_318,
967 );
968 let interval = AnchorBucketInterval::ZIP_318;
969
970 let anchor = zcash_protocol::zip318::AnchorGrid::new(
972 interval,
973 zcash_protocol::zip318::BlockSpacing::UNIFORM,
974 )
975 .boundary_at_or_below(BlockHeight::from_u32(2_000_000));
976 let height = TargetHeight::from(BlockHeight::from_u32(u32::from(anchor) + 10));
977
978 let orchard_inputs = [TestOrchardInput {
981 note_id: 0,
982 value: Zatoshis::const_from_u64(10_000_000),
983 }];
984 let orchard_view = (
985 ::orchard::bundle::BundleVersion::orchard_v3(),
986 &orchard_inputs[..],
987 &[] as &[Infallible],
988 );
989 let sapling_view = (
990 sapling::builder::BundleType::DEFAULT,
991 &[] as &[Infallible],
992 &[] as &[Infallible],
993 );
994
995 let recorded_for = |value: Zatoshis| {
996 let ironwood_outputs = [OrchardPayment::new(value)];
997 let ironwood_view = (
998 ::orchard::bundle::BundleVersion::ironwood_v3(),
999 &[] as &[Infallible],
1000 &ironwood_outputs[..],
1001 );
1002 change_strategy
1003 .compute_balance::<_, u32>(
1004 &Network::TestNetwork,
1005 height,
1006 anchor,
1007 &zip318,
1008 &[] as &[TestTransparentInput],
1009 &[] as &[TxOut],
1010 &sapling_view,
1011 &orchard_view,
1012 &ironwood_view,
1013 None,
1014 &all_change_receivers(),
1015 )
1016 .expect("the input covers the payment and its fee")
1017 .dummy_outputs()
1018 .expect("the change strategy records dummy outputs")
1019 .ironwood()
1020 };
1021
1022 assert_eq!(recorded_for(MAX_RESIDUAL_VALUE), 0);
1023 assert_eq!(
1024 recorded_for((MAX_RESIDUAL_VALUE + Zatoshis::const_from_u64(1)).unwrap()),
1025 1
1026 );
1027 }
1028
1029 #[test]
1030 #[cfg(feature = "orchard")]
1031 fn ironwood_outputs_are_charged_actions() {
1032 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1037 Zip317FeeRule::standard(),
1038 None,
1039 ShieldedPool::Orchard,
1040 DustOutputPolicy::default(),
1041 );
1042
1043 let height = Network::TestNetwork
1044 .activation_height(NetworkUpgrade::Nu5)
1045 .unwrap()
1046 .into();
1047 let sapling_inputs = [TestSaplingInput {
1048 note_id: 0,
1049 value: Zatoshis::const_from_u64(100000),
1050 }];
1051 let orchard_outputs = [OrchardPayment::new(Zatoshis::const_from_u64(30000))];
1052 let sapling_view = (
1053 sapling::builder::BundleType::DEFAULT,
1054 &sapling_inputs[..],
1055 &[] as &[Infallible],
1056 );
1057 let orchard_view = (
1058 ::orchard::bundle::BundleVersion::orchard_v2(),
1059 &[] as &[Infallible],
1060 &orchard_outputs[..],
1061 );
1062
1063 let without_ironwood = change_strategy
1064 .compute_balance(
1065 &Network::TestNetwork,
1066 height,
1067 BlockHeight::from_u32(1),
1068 &PoolMigrationParams::new(
1069 zcash_protocol::zip318::BlockSpacing::UNIFORM,
1070 AnchorRetentionInterval::ZIP_318,
1071 ),
1072 &[] as &[TestTransparentInput],
1073 &[] as &[TxOut],
1074 &sapling_view,
1075 &orchard_view,
1076 &orchard_fees::EmptyBundleView,
1077 None,
1078 &all_change_receivers(),
1079 )
1080 .unwrap();
1081
1082 let with_ironwood = change_strategy
1083 .compute_balance(
1084 &Network::TestNetwork,
1085 height,
1086 BlockHeight::from_u32(1),
1087 &PoolMigrationParams::new(
1088 zcash_protocol::zip318::BlockSpacing::UNIFORM,
1089 AnchorRetentionInterval::ZIP_318,
1090 ),
1091 &[] as &[TestTransparentInput],
1092 &[] as &[TxOut],
1093 &sapling_view,
1094 &orchard_view,
1095 &(
1096 ::orchard::bundle::BundleVersion::ironwood_v3(),
1097 &[] as &[Infallible],
1098 &orchard_outputs[..],
1099 ),
1100 None,
1101 &all_change_receivers(),
1102 )
1103 .unwrap();
1104
1105 assert_eq!(
1110 without_ironwood.fee_required(),
1111 Zatoshis::const_from_u64(20000)
1112 );
1113 assert_eq!(
1114 with_ironwood.fee_required(),
1115 Zatoshis::const_from_u64(30000)
1116 );
1117 }
1118
1119 #[test]
1120 fn change_with_transparent_payments_implicitly_allowing_zero_change() {
1121 change_with_transparent_payments(DustOutputPolicy::default())
1122 }
1123
1124 #[test]
1125 fn change_with_transparent_payments_explicitly_allowing_zero_change() {
1126 change_with_transparent_payments(DustOutputPolicy::new(
1127 DustAction::AllowDustChange,
1128 Some(Zatoshis::ZERO),
1129 ))
1130 }
1131
1132 fn change_with_transparent_payments(dust_output_policy: DustOutputPolicy) {
1133 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1134 Zip317FeeRule::standard(),
1135 None,
1136 ShieldedPool::Sapling,
1137 dust_output_policy,
1138 );
1139
1140 let result = change_strategy.compute_balance(
1142 &Network::TestNetwork,
1143 Network::TestNetwork
1144 .activation_height(NetworkUpgrade::Nu5)
1145 .unwrap()
1146 .into(),
1147 BlockHeight::from_u32(1),
1148 &PoolMigrationParams::new(
1149 zcash_protocol::zip318::BlockSpacing::UNIFORM,
1150 AnchorRetentionInterval::ZIP_318,
1151 ),
1152 &[] as &[TestTransparentInput],
1153 &[TxOut::new(
1154 Zatoshis::const_from_u64(40000),
1155 Script::default(),
1156 )],
1157 &(
1158 sapling::builder::BundleType::DEFAULT,
1159 &[TestSaplingInput {
1160 note_id: 0,
1161 value: Zatoshis::const_from_u64(55000),
1162 }][..],
1163 &[] as &[Infallible],
1164 ),
1165 #[cfg(feature = "orchard")]
1166 &orchard_fees::EmptyBundleView,
1167 #[cfg(feature = "orchard")]
1168 &orchard_fees::EmptyBundleView,
1169 None,
1170 &all_change_receivers(),
1171 );
1172
1173 assert_matches!(
1174 result,
1175 Ok(balance) if
1176 balance.proposed_change() == [ChangeValue::sapling(Zatoshis::ZERO, None)]
1177 && balance.fee_required() == Zatoshis::const_from_u64(15000)
1178 );
1179 }
1180
1181 #[test]
1182 #[cfg(feature = "transparent-inputs")]
1183 fn change_fully_transparent_no_change() {
1184 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1185 Zip317FeeRule::standard(),
1186 None,
1187 ShieldedPool::Sapling,
1188 DustOutputPolicy::default(),
1189 );
1190
1191 let result = change_strategy.compute_balance::<_, Infallible>(
1193 &Network::TestNetwork,
1194 Network::TestNetwork
1195 .activation_height(NetworkUpgrade::Nu5)
1196 .unwrap()
1197 .into(),
1198 BlockHeight::from_u32(1),
1199 &PoolMigrationParams::new(
1200 zcash_protocol::zip318::BlockSpacing::UNIFORM,
1201 AnchorRetentionInterval::ZIP_318,
1202 ),
1203 &[TestTransparentInput {
1204 outpoint: OutPoint::fake(),
1205 coin: TxOut::new(
1206 Zatoshis::const_from_u64(50000),
1207 TransparentAddress::PublicKeyHash([0u8; 20]).script().into(),
1208 ),
1209 }],
1210 &[TxOut::new(
1211 Zatoshis::const_from_u64(40000),
1212 Script::default(),
1213 )],
1214 &sapling_fees::EmptyBundleView,
1215 #[cfg(feature = "orchard")]
1216 &orchard_fees::EmptyBundleView,
1217 #[cfg(feature = "orchard")]
1218 &orchard_fees::EmptyBundleView,
1219 None,
1220 &all_change_receivers(),
1221 );
1222
1223 assert_matches!(
1224 result,
1225 Ok(balance) if
1226 balance.proposed_change().is_empty() &&
1227 balance.fee_required() == Zatoshis::const_from_u64(10000)
1228 );
1229 }
1230
1231 #[test]
1232 #[cfg(feature = "transparent-inputs")]
1233 fn change_transparent_flows_with_shielded_change() {
1234 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1235 Zip317FeeRule::standard(),
1236 None,
1237 ShieldedPool::Sapling,
1238 DustOutputPolicy::default(),
1239 );
1240
1241 let result = change_strategy.compute_balance::<_, Infallible>(
1243 &Network::TestNetwork,
1244 Network::TestNetwork
1245 .activation_height(NetworkUpgrade::Nu5)
1246 .unwrap()
1247 .into(),
1248 BlockHeight::from_u32(1),
1249 &PoolMigrationParams::new(
1250 zcash_protocol::zip318::BlockSpacing::UNIFORM,
1251 AnchorRetentionInterval::ZIP_318,
1252 ),
1253 &[TestTransparentInput {
1254 outpoint: OutPoint::fake(),
1255 coin: TxOut::new(
1256 Zatoshis::const_from_u64(63000),
1257 TransparentAddress::PublicKeyHash([0u8; 20]).script().into(),
1258 ),
1259 }],
1260 &[TxOut::new(
1261 Zatoshis::const_from_u64(40000),
1262 Script::default(),
1263 )],
1264 &sapling_fees::EmptyBundleView,
1265 #[cfg(feature = "orchard")]
1266 &orchard_fees::EmptyBundleView,
1267 #[cfg(feature = "orchard")]
1268 &orchard_fees::EmptyBundleView,
1269 None,
1270 &all_change_receivers(),
1271 );
1272
1273 assert_matches!(
1274 result,
1275 Ok(balance) if
1276 balance.proposed_change() == [ChangeValue::sapling(Zatoshis::const_from_u64(8000), None)] &&
1277 balance.fee_required() == Zatoshis::const_from_u64(15000)
1278 );
1279 }
1280
1281 #[test]
1282 #[cfg(feature = "transparent-inputs")]
1283 fn change_transparent_flows_with_shielded_dust_change() {
1284 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1285 Zip317FeeRule::standard(),
1286 None,
1287 ShieldedPool::Sapling,
1288 DustOutputPolicy::new(
1289 DustAction::AllowDustChange,
1290 Some(Zatoshis::const_from_u64(1000)),
1291 ),
1292 );
1293
1294 let result = change_strategy.compute_balance::<_, Infallible>(
1299 &Network::TestNetwork,
1300 Network::TestNetwork
1301 .activation_height(NetworkUpgrade::Nu5)
1302 .unwrap()
1303 .into(),
1304 BlockHeight::from_u32(1),
1305 &PoolMigrationParams::new(
1306 zcash_protocol::zip318::BlockSpacing::UNIFORM,
1307 AnchorRetentionInterval::ZIP_318,
1308 ),
1309 &[TestTransparentInput {
1310 outpoint: OutPoint::fake(),
1311 coin: TxOut::new(
1312 Zatoshis::const_from_u64(56000),
1313 TransparentAddress::PublicKeyHash([0u8; 20]).script().into(),
1314 ),
1315 }],
1316 &[TxOut::new(
1317 Zatoshis::const_from_u64(40000),
1318 Script::default(),
1319 )],
1320 &sapling_fees::EmptyBundleView,
1321 #[cfg(feature = "orchard")]
1322 &orchard_fees::EmptyBundleView,
1323 #[cfg(feature = "orchard")]
1324 &orchard_fees::EmptyBundleView,
1325 None,
1326 &all_change_receivers(),
1327 );
1328
1329 assert_matches!(
1330 result,
1331 Ok(balance) if
1332 balance.proposed_change() == [ChangeValue::sapling(Zatoshis::const_from_u64(1000), None)] &&
1333 balance.fee_required() == Zatoshis::const_from_u64(15000)
1334 );
1335 }
1336
1337 #[test]
1338 #[cfg(feature = "transparent-inputs")]
1339 fn change_fully_transparent_with_transparent_change() {
1340 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1341 Zip317FeeRule::standard(),
1342 None,
1343 ShieldedPool::Sapling,
1344 DustOutputPolicy::default(),
1345 )
1346 .with_transparent_change_policy(TransparentChangePolicy::TransparentChangeAllowed);
1347
1348 let result = change_strategy.compute_balance::<_, Infallible>(
1353 &Network::TestNetwork,
1354 Network::TestNetwork
1355 .activation_height(NetworkUpgrade::Nu5)
1356 .unwrap()
1357 .into(),
1358 BlockHeight::from_u32(1),
1359 &PoolMigrationParams::new(
1360 zcash_protocol::zip318::BlockSpacing::UNIFORM,
1361 AnchorRetentionInterval::ZIP_318,
1362 ),
1363 &[TestTransparentInput {
1364 outpoint: OutPoint::fake(),
1365 coin: TxOut::new(
1366 Zatoshis::const_from_u64(63000),
1367 TransparentAddress::PublicKeyHash([0u8; 20]).script().into(),
1368 ),
1369 }],
1370 &[TxOut::new(
1371 Zatoshis::const_from_u64(40000),
1372 Script::default(),
1373 )],
1374 &sapling_fees::EmptyBundleView,
1375 #[cfg(feature = "orchard")]
1376 &orchard_fees::EmptyBundleView,
1377 #[cfg(feature = "orchard")]
1378 &orchard_fees::EmptyBundleView,
1379 None,
1380 &all_change_receivers(),
1381 );
1382
1383 assert_matches!(
1384 result,
1385 Ok(balance) if
1386 balance.proposed_change() == [ChangeValue::transparent(Zatoshis::const_from_u64(13000))] &&
1387 balance.fee_required() == Zatoshis::const_from_u64(10000)
1388 );
1389 }
1390
1391 #[test]
1392 #[cfg(feature = "transparent-inputs")]
1393 fn change_fully_transparent_exact_match_with_transparent_change() {
1394 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1395 Zip317FeeRule::standard(),
1396 None,
1397 ShieldedPool::Sapling,
1398 DustOutputPolicy::default(),
1399 )
1400 .with_transparent_change_policy(TransparentChangePolicy::TransparentChangeAllowed);
1401
1402 let result = change_strategy.compute_balance::<_, Infallible>(
1405 &Network::TestNetwork,
1406 Network::TestNetwork
1407 .activation_height(NetworkUpgrade::Nu5)
1408 .unwrap()
1409 .into(),
1410 BlockHeight::from_u32(1),
1411 &PoolMigrationParams::new(
1412 zcash_protocol::zip318::BlockSpacing::UNIFORM,
1413 AnchorRetentionInterval::ZIP_318,
1414 ),
1415 &[TestTransparentInput {
1416 outpoint: OutPoint::fake(),
1417 coin: TxOut::new(
1418 Zatoshis::const_from_u64(50000),
1419 TransparentAddress::PublicKeyHash([0u8; 20]).script().into(),
1420 ),
1421 }],
1422 &[TxOut::new(
1423 Zatoshis::const_from_u64(40000),
1424 Script::default(),
1425 )],
1426 &sapling_fees::EmptyBundleView,
1427 #[cfg(feature = "orchard")]
1428 &orchard_fees::EmptyBundleView,
1429 #[cfg(feature = "orchard")]
1430 &orchard_fees::EmptyBundleView,
1431 None,
1432 &all_change_receivers(),
1433 );
1434
1435 assert_matches!(
1436 result,
1437 Ok(balance) if
1438 balance.proposed_change().is_empty() &&
1439 balance.fee_required() == Zatoshis::const_from_u64(10000)
1440 );
1441 }
1442
1443 #[test]
1444 #[cfg(feature = "transparent-inputs")]
1445 fn transparent_change_policy_has_no_effect_on_shielded_flows() {
1446 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1447 Zip317FeeRule::standard(),
1448 None,
1449 ShieldedPool::Sapling,
1450 DustOutputPolicy::default(),
1451 )
1452 .with_transparent_change_policy(TransparentChangePolicy::TransparentChangeAllowed);
1453
1454 let result = change_strategy.compute_balance(
1457 &Network::TestNetwork,
1458 Network::TestNetwork
1459 .activation_height(NetworkUpgrade::Nu5)
1460 .unwrap()
1461 .into(),
1462 BlockHeight::from_u32(1),
1463 &PoolMigrationParams::new(
1464 zcash_protocol::zip318::BlockSpacing::UNIFORM,
1465 AnchorRetentionInterval::ZIP_318,
1466 ),
1467 &[] as &[TestTransparentInput],
1468 &[] as &[TxOut],
1469 &(
1470 sapling::builder::BundleType::DEFAULT,
1471 &[TestSaplingInput {
1472 note_id: 0,
1473 value: Zatoshis::const_from_u64(55000),
1474 }][..],
1475 &[SaplingPayment::new(Zatoshis::const_from_u64(40000))][..],
1476 ),
1477 #[cfg(feature = "orchard")]
1478 &orchard_fees::EmptyBundleView,
1479 #[cfg(feature = "orchard")]
1480 &orchard_fees::EmptyBundleView,
1481 None,
1482 &all_change_receivers(),
1483 );
1484
1485 assert_matches!(
1486 result,
1487 Ok(balance) if
1488 balance.proposed_change() == [ChangeValue::sapling(Zatoshis::const_from_u64(5000), None)] &&
1489 balance.fee_required() == Zatoshis::const_from_u64(10000)
1490 );
1491 }
1492
1493 #[test]
1494 #[cfg(feature = "transparent-inputs")]
1495 fn transparent_change_is_not_split() {
1496 let change_strategy = MultiOutputChangeStrategy::<_, MockWalletDb>::new(
1497 Zip317FeeRule::standard(),
1498 None,
1499 ShieldedPool::Sapling,
1500 DustOutputPolicy::default(),
1501 SplitPolicy::with_min_output_value(
1502 NonZeroUsize::new(5).unwrap(),
1503 Zatoshis::const_from_u64(100_0000),
1504 ),
1505 )
1506 .with_transparent_change_policy(TransparentChangePolicy::TransparentChangeAllowed);
1507
1508 let result = change_strategy.compute_balance::<_, Infallible>(
1512 &Network::TestNetwork,
1513 Network::TestNetwork
1514 .activation_height(NetworkUpgrade::Nu5)
1515 .unwrap()
1516 .into(),
1517 BlockHeight::from_u32(1),
1518 &PoolMigrationParams::new(
1519 zcash_protocol::zip318::BlockSpacing::UNIFORM,
1520 AnchorRetentionInterval::ZIP_318,
1521 ),
1522 &[TestTransparentInput {
1523 outpoint: OutPoint::fake(),
1524 coin: TxOut::new(
1525 Zatoshis::const_from_u64(750_0000),
1526 TransparentAddress::PublicKeyHash([0u8; 20]).script().into(),
1527 ),
1528 }],
1529 &[TxOut::new(
1530 Zatoshis::const_from_u64(100_0000),
1531 Script::default(),
1532 )],
1533 &sapling_fees::EmptyBundleView,
1534 #[cfg(feature = "orchard")]
1535 &orchard_fees::EmptyBundleView,
1536 #[cfg(feature = "orchard")]
1537 &orchard_fees::EmptyBundleView,
1538 None,
1539 &AccountMeta::new(
1540 Some(PoolMeta::new(0, Zatoshis::ZERO)),
1541 None,
1542 None,
1543 all_change_receivers(),
1544 ),
1545 );
1546
1547 assert_matches!(
1548 result,
1549 Ok(balance) if
1550 balance.proposed_change() == [ChangeValue::transparent(Zatoshis::const_from_u64(649_0000))] &&
1551 balance.fee_required() == Zatoshis::const_from_u64(10000)
1552 );
1553 }
1554
1555 #[test]
1556 #[cfg(feature = "transparent-inputs")]
1557 fn transparent_change_rejects_dust() {
1558 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1559 Zip317FeeRule::standard(),
1560 None,
1561 ShieldedPool::Sapling,
1562 DustOutputPolicy::default(),
1563 )
1564 .with_transparent_change_policy(TransparentChangePolicy::TransparentChangeAllowed);
1565
1566 let result = change_strategy.compute_balance::<_, Infallible>(
1571 &Network::TestNetwork,
1572 Network::TestNetwork
1573 .activation_height(NetworkUpgrade::Nu5)
1574 .unwrap()
1575 .into(),
1576 BlockHeight::from_u32(1),
1577 &PoolMigrationParams::new(
1578 zcash_protocol::zip318::BlockSpacing::UNIFORM,
1579 AnchorRetentionInterval::ZIP_318,
1580 ),
1581 &[TestTransparentInput {
1582 outpoint: OutPoint::fake(),
1583 coin: TxOut::new(
1584 Zatoshis::const_from_u64(50100),
1585 TransparentAddress::PublicKeyHash([0u8; 20]).script().into(),
1586 ),
1587 }],
1588 &[TxOut::new(
1589 Zatoshis::const_from_u64(40000),
1590 Script::default(),
1591 )],
1592 &sapling_fees::EmptyBundleView,
1593 #[cfg(feature = "orchard")]
1594 &orchard_fees::EmptyBundleView,
1595 #[cfg(feature = "orchard")]
1596 &orchard_fees::EmptyBundleView,
1597 None,
1598 &all_change_receivers(),
1599 );
1600
1601 assert_matches!(
1602 result,
1603 Err(ChangeError::InsufficientFunds { available, required })
1604 if available == Zatoshis::const_from_u64(50100)
1605 && required == Zatoshis::const_from_u64(55000)
1606 );
1607 }
1608
1609 #[test]
1610 #[cfg(feature = "transparent-inputs")]
1611 fn transparent_change_allows_dust() {
1612 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1613 Zip317FeeRule::standard(),
1614 None,
1615 ShieldedPool::Sapling,
1616 DustOutputPolicy::new(
1617 DustAction::AllowDustChange,
1618 Some(Zatoshis::const_from_u64(1000)),
1619 ),
1620 )
1621 .with_transparent_change_policy(TransparentChangePolicy::TransparentChangeAllowed);
1622
1623 let result = change_strategy.compute_balance::<_, Infallible>(
1627 &Network::TestNetwork,
1628 Network::TestNetwork
1629 .activation_height(NetworkUpgrade::Nu5)
1630 .unwrap()
1631 .into(),
1632 BlockHeight::from_u32(1),
1633 &PoolMigrationParams::new(
1634 zcash_protocol::zip318::BlockSpacing::UNIFORM,
1635 AnchorRetentionInterval::ZIP_318,
1636 ),
1637 &[TestTransparentInput {
1638 outpoint: OutPoint::fake(),
1639 coin: TxOut::new(
1640 Zatoshis::const_from_u64(50100),
1641 TransparentAddress::PublicKeyHash([0u8; 20]).script().into(),
1642 ),
1643 }],
1644 &[TxOut::new(
1645 Zatoshis::const_from_u64(40000),
1646 Script::default(),
1647 )],
1648 &sapling_fees::EmptyBundleView,
1649 #[cfg(feature = "orchard")]
1650 &orchard_fees::EmptyBundleView,
1651 #[cfg(feature = "orchard")]
1652 &orchard_fees::EmptyBundleView,
1653 None,
1654 &all_change_receivers(),
1655 );
1656
1657 assert_matches!(
1658 result,
1659 Ok(balance) if
1660 balance.proposed_change() == [ChangeValue::transparent(Zatoshis::const_from_u64(100))] &&
1661 balance.fee_required() == Zatoshis::const_from_u64(10000)
1662 );
1663 }
1664
1665 #[test]
1666 #[cfg(feature = "transparent-inputs")]
1667 fn transparent_change_dust_added_to_fee() {
1668 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1669 Zip317FeeRule::standard(),
1670 None,
1671 ShieldedPool::Sapling,
1672 DustOutputPolicy::new(DustAction::AddDustToFee, None),
1673 )
1674 .with_transparent_change_policy(TransparentChangePolicy::TransparentChangeAllowed);
1675
1676 let result = change_strategy.compute_balance::<_, Infallible>(
1680 &Network::TestNetwork,
1681 Network::TestNetwork
1682 .activation_height(NetworkUpgrade::Nu5)
1683 .unwrap()
1684 .into(),
1685 BlockHeight::from_u32(1),
1686 &PoolMigrationParams::new(
1687 zcash_protocol::zip318::BlockSpacing::UNIFORM,
1688 AnchorRetentionInterval::ZIP_318,
1689 ),
1690 &[TestTransparentInput {
1691 outpoint: OutPoint::fake(),
1692 coin: TxOut::new(
1693 Zatoshis::const_from_u64(50100),
1694 TransparentAddress::PublicKeyHash([0u8; 20]).script().into(),
1695 ),
1696 }],
1697 &[TxOut::new(
1698 Zatoshis::const_from_u64(40000),
1699 Script::default(),
1700 )],
1701 &sapling_fees::EmptyBundleView,
1702 #[cfg(feature = "orchard")]
1703 &orchard_fees::EmptyBundleView,
1704 #[cfg(feature = "orchard")]
1705 &orchard_fees::EmptyBundleView,
1706 None,
1707 &all_change_receivers(),
1708 );
1709
1710 assert_matches!(
1711 result,
1712 Ok(balance) if
1713 balance.proposed_change().is_empty() &&
1714 balance.fee_required() == Zatoshis::const_from_u64(10100)
1715 );
1716 }
1717
1718 #[test]
1719 fn change_with_allowable_dust_implicitly_allowing_zero_change() {
1720 change_with_allowable_dust(DustOutputPolicy::default())
1721 }
1722
1723 #[test]
1724 fn change_with_allowable_dust_explicitly_allowing_zero_change() {
1725 change_with_allowable_dust(DustOutputPolicy::new(
1726 DustAction::AllowDustChange,
1727 Some(Zatoshis::ZERO),
1728 ))
1729 }
1730
1731 fn change_with_allowable_dust(dust_output_policy: DustOutputPolicy) {
1732 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1733 Zip317FeeRule::standard(),
1734 None,
1735 ShieldedPool::Sapling,
1736 dust_output_policy,
1737 );
1738
1739 let result = change_strategy.compute_balance(
1744 &Network::TestNetwork,
1745 Network::TestNetwork
1746 .activation_height(NetworkUpgrade::Nu5)
1747 .unwrap()
1748 .into(),
1749 BlockHeight::from_u32(1),
1750 &PoolMigrationParams::new(
1751 zcash_protocol::zip318::BlockSpacing::UNIFORM,
1752 AnchorRetentionInterval::ZIP_318,
1753 ),
1754 &[] as &[TestTransparentInput],
1755 &[] as &[TxOut],
1756 &(
1757 sapling::builder::BundleType::DEFAULT,
1758 &[
1759 TestSaplingInput {
1760 note_id: 0,
1761 value: Zatoshis::const_from_u64(49000),
1762 },
1763 TestSaplingInput {
1764 note_id: 1,
1765 value: Zatoshis::const_from_u64(1000),
1766 },
1767 ][..],
1768 &[SaplingPayment::new(Zatoshis::const_from_u64(40000))][..],
1769 ),
1770 #[cfg(feature = "orchard")]
1771 &orchard_fees::EmptyBundleView,
1772 #[cfg(feature = "orchard")]
1773 &orchard_fees::EmptyBundleView,
1774 None,
1775 &all_change_receivers(),
1776 );
1777
1778 assert_matches!(
1779 result,
1780 Ok(balance) if
1781 balance.proposed_change() == [ChangeValue::sapling(Zatoshis::ZERO, None)] &&
1782 balance.fee_required() == Zatoshis::const_from_u64(10000)
1783 );
1784 }
1785
1786 #[test]
1787 fn change_with_disallowed_dust() {
1788 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1789 Zip317FeeRule::standard(),
1790 None,
1791 ShieldedPool::Sapling,
1792 DustOutputPolicy::default(),
1793 );
1794
1795 let result = change_strategy.compute_balance(
1798 &Network::TestNetwork,
1799 Network::TestNetwork
1800 .activation_height(NetworkUpgrade::Nu5)
1801 .unwrap()
1802 .into(),
1803 BlockHeight::from_u32(1),
1804 &PoolMigrationParams::new(
1805 zcash_protocol::zip318::BlockSpacing::UNIFORM,
1806 AnchorRetentionInterval::ZIP_318,
1807 ),
1808 &[] as &[TestTransparentInput],
1809 &[] as &[TxOut],
1810 &(
1811 sapling::builder::BundleType::DEFAULT,
1812 &[
1813 TestSaplingInput {
1814 note_id: 0,
1815 value: Zatoshis::const_from_u64(29000),
1816 },
1817 TestSaplingInput {
1818 note_id: 1,
1819 value: Zatoshis::const_from_u64(20000),
1820 },
1821 TestSaplingInput {
1822 note_id: 2,
1823 value: Zatoshis::const_from_u64(1000),
1824 },
1825 ][..],
1826 &[SaplingPayment::new(Zatoshis::const_from_u64(30000))][..],
1827 ),
1828 #[cfg(feature = "orchard")]
1829 &orchard_fees::EmptyBundleView,
1830 #[cfg(feature = "orchard")]
1831 &orchard_fees::EmptyBundleView,
1832 None,
1833 &all_change_receivers(),
1834 );
1835
1836 assert_matches!(
1839 result,
1840 Err(ChangeError::DustInputs { sapling, .. }) if sapling == vec![2]
1841 );
1842 }
1843
1844 #[test]
1845 #[cfg(feature = "orchard")]
1846 fn sapling_only_account_paying_orchard_gets_sapling_change() {
1847 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1848 Zip317FeeRule::standard(),
1849 None,
1850 ShieldedPool::Orchard,
1851 DustOutputPolicy::default(),
1852 );
1853
1854 let result = change_strategy.compute_balance(
1857 &Network::TestNetwork,
1858 Network::TestNetwork
1859 .activation_height(NetworkUpgrade::Nu5)
1860 .unwrap()
1861 .into(),
1862 BlockHeight::from_u32(1),
1863 &PoolMigrationParams::new(
1864 zcash_protocol::zip318::BlockSpacing::UNIFORM,
1865 AnchorRetentionInterval::ZIP_318,
1866 ),
1867 &[] as &[TestTransparentInput],
1868 &[] as &[TxOut],
1869 &(
1870 sapling::builder::BundleType::DEFAULT,
1871 &[TestSaplingInput {
1872 note_id: 0,
1873 value: Zatoshis::const_from_u64(55000),
1874 }][..],
1875 &[] as &[Infallible],
1876 ),
1877 &(
1878 ::orchard::bundle::BundleVersion::orchard_v2(),
1879 &[] as &[Infallible],
1880 &[OrchardPayment::new(Zatoshis::const_from_u64(30000))][..],
1881 ),
1882 &orchard_fees::EmptyBundleView,
1883 None,
1884 &ChangeReceivers::new(
1885 true,
1886 false,
1887 #[cfg(feature = "transparent-inputs")]
1888 None,
1889 ),
1890 );
1891
1892 assert_matches!(
1893 result,
1894 Ok(balance) if
1895 balance.proposed_change() == [ChangeValue::sapling(Zatoshis::const_from_u64(5000), None)] &&
1896 balance.fee_required() == Zatoshis::const_from_u64(20000)
1897 );
1898 }
1899
1900 #[test]
1901 fn shielded_change_requires_a_shielded_receiver() {
1902 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1903 Zip317FeeRule::standard(),
1904 None,
1905 ShieldedPool::Sapling,
1906 DustOutputPolicy::default(),
1907 );
1908
1909 let result = change_strategy.compute_balance(
1911 &Network::TestNetwork,
1912 Network::TestNetwork
1913 .activation_height(NetworkUpgrade::Nu5)
1914 .unwrap()
1915 .into(),
1916 BlockHeight::from_u32(1),
1917 &PoolMigrationParams::new(
1918 zcash_protocol::zip318::BlockSpacing::UNIFORM,
1919 AnchorRetentionInterval::ZIP_318,
1920 ),
1921 &[] as &[TestTransparentInput],
1922 &[] as &[TxOut],
1923 &(
1924 sapling::builder::BundleType::DEFAULT,
1925 &[TestSaplingInput {
1926 note_id: 0,
1927 value: Zatoshis::const_from_u64(55000),
1928 }][..],
1929 &[SaplingPayment::new(Zatoshis::const_from_u64(40000))][..],
1930 ),
1931 #[cfg(feature = "orchard")]
1932 &orchard_fees::EmptyBundleView,
1933 #[cfg(feature = "orchard")]
1934 &orchard_fees::EmptyBundleView,
1935 None,
1936 &ChangeReceivers::new(
1937 false,
1938 false,
1939 #[cfg(feature = "transparent-inputs")]
1940 Some(TransparentChangeKind::P2pkh),
1941 ),
1942 );
1943
1944 assert_matches!(
1945 result,
1946 Err(ChangeError::UnsupportedChangePool(pool)) if pool == PoolType::SAPLING
1947 );
1948 }
1949
1950 #[test]
1951 #[cfg(feature = "transparent-inputs")]
1952 fn transparent_change_requires_a_transparent_receiver() {
1953 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
1954 Zip317FeeRule::standard(),
1955 None,
1956 ShieldedPool::Sapling,
1957 DustOutputPolicy::default(),
1958 )
1959 .with_transparent_change_policy(TransparentChangePolicy::TransparentChangeAllowed);
1960
1961 let result = change_strategy.compute_balance::<_, Infallible>(
1963 &Network::TestNetwork,
1964 Network::TestNetwork
1965 .activation_height(NetworkUpgrade::Nu5)
1966 .unwrap()
1967 .into(),
1968 BlockHeight::from_u32(1),
1969 &PoolMigrationParams::new(
1970 zcash_protocol::zip318::BlockSpacing::UNIFORM,
1971 AnchorRetentionInterval::ZIP_318,
1972 ),
1973 &[TestTransparentInput {
1974 outpoint: OutPoint::fake(),
1975 coin: TxOut::new(
1976 Zatoshis::const_from_u64(63000),
1977 TransparentAddress::PublicKeyHash([0u8; 20]).script().into(),
1978 ),
1979 }],
1980 &[TxOut::new(
1981 Zatoshis::const_from_u64(40000),
1982 Script::default(),
1983 )],
1984 &sapling_fees::EmptyBundleView,
1985 #[cfg(feature = "orchard")]
1986 &orchard_fees::EmptyBundleView,
1987 #[cfg(feature = "orchard")]
1988 &orchard_fees::EmptyBundleView,
1989 None,
1990 &ChangeReceivers::new(true, true, None),
1991 );
1992
1993 assert_matches!(
1994 result,
1995 Err(ChangeError::UnsupportedChangePool(pool)) if pool == PoolType::TRANSPARENT
1996 );
1997 }
1998
1999 #[test]
2000 #[cfg(feature = "transparent-inputs")]
2001 fn missing_transparent_receiver_does_not_hide_insufficient_funds() {
2002 const PAYMENT_VALUE: Zatoshis = Zatoshis::const_from_u64(40000);
2004
2005 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
2006 Zip317FeeRule::standard(),
2007 None,
2008 ShieldedPool::Sapling,
2009 DustOutputPolicy::default(),
2010 )
2011 .with_transparent_change_policy(TransparentChangePolicy::TransparentChangeAllowed);
2012
2013 let result = change_strategy.compute_balance::<_, Infallible>(
2017 &Network::TestNetwork,
2018 Network::TestNetwork
2019 .activation_height(NetworkUpgrade::Nu5)
2020 .unwrap()
2021 .into(),
2022 BlockHeight::from_u32(1),
2023 &PoolMigrationParams::new(
2024 zcash_protocol::zip318::BlockSpacing::UNIFORM,
2025 AnchorRetentionInterval::ZIP_318,
2026 ),
2027 &[] as &[TestTransparentInput],
2028 &[TxOut::new(PAYMENT_VALUE, Script::default())],
2029 &sapling_fees::EmptyBundleView,
2030 #[cfg(feature = "orchard")]
2031 &orchard_fees::EmptyBundleView,
2032 #[cfg(feature = "orchard")]
2033 &orchard_fees::EmptyBundleView,
2034 None,
2035 &ChangeReceivers::new(true, true, None),
2036 );
2037
2038 assert_matches!(
2039 result,
2040 Err(ChangeError::InsufficientFunds { available, required })
2041 if available == Zatoshis::ZERO && required == (PAYMENT_VALUE + MINIMUM_FEE).unwrap()
2042 );
2043 }
2044
2045 #[test]
2046 #[cfg(feature = "transparent-inputs")]
2047 fn changeless_transparent_transaction_needs_no_transparent_receiver() {
2048 const PAYMENT_VALUE: Zatoshis = Zatoshis::const_from_u64(40000);
2050
2051 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
2052 Zip317FeeRule::standard(),
2053 None,
2054 ShieldedPool::Sapling,
2055 DustOutputPolicy::default(),
2056 )
2057 .with_transparent_change_policy(TransparentChangePolicy::TransparentChangeAllowed);
2058
2059 let result = change_strategy.compute_balance::<_, Infallible>(
2062 &Network::TestNetwork,
2063 Network::TestNetwork
2064 .activation_height(NetworkUpgrade::Nu5)
2065 .unwrap()
2066 .into(),
2067 BlockHeight::from_u32(1),
2068 &PoolMigrationParams::new(
2069 zcash_protocol::zip318::BlockSpacing::UNIFORM,
2070 AnchorRetentionInterval::ZIP_318,
2071 ),
2072 &[] as &[TestTransparentInput],
2073 &[TxOut::new(PAYMENT_VALUE, Script::default())],
2074 &sapling_fees::EmptyBundleView,
2075 #[cfg(feature = "orchard")]
2076 &orchard_fees::EmptyBundleView,
2077 #[cfg(feature = "orchard")]
2078 &orchard_fees::EmptyBundleView,
2079 Some(EphemeralBalance::Input(
2080 (PAYMENT_VALUE + MINIMUM_FEE).unwrap(),
2081 )),
2082 &ChangeReceivers::new(true, true, None),
2083 );
2084
2085 assert_matches!(
2086 result,
2087 Ok(balance) if balance.proposed_change().is_empty() && balance.fee_required() == MINIMUM_FEE
2088 );
2089 }
2090
2091 #[test]
2092 #[cfg(feature = "transparent-inputs")]
2093 fn transparent_change_is_sized_by_its_kind() {
2094 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
2095 Zip317FeeRule::standard(),
2096 None,
2097 ShieldedPool::Sapling,
2098 DustOutputPolicy::default(),
2099 )
2100 .with_transparent_change_policy(TransparentChangePolicy::TransparentChangeAllowed);
2101
2102 let payments = vec![
2106 TxOut::new(
2107 Zatoshis::const_from_u64(5000),
2108 TransparentAddress::ScriptHash([0u8; 20]).script().into(),
2109 );
2110 16
2111 ];
2112 let balance = |kind| {
2113 change_strategy.compute_balance::<_, Infallible>(
2114 &Network::TestNetwork,
2115 Network::TestNetwork
2116 .activation_height(NetworkUpgrade::Nu5)
2117 .unwrap()
2118 .into(),
2119 BlockHeight::from_u32(1),
2120 &PoolMigrationParams::new(
2121 zcash_protocol::zip318::BlockSpacing::UNIFORM,
2122 AnchorRetentionInterval::ZIP_318,
2123 ),
2124 &[TestTransparentInput {
2125 outpoint: OutPoint::fake(),
2126 coin: TxOut::new(
2127 Zatoshis::const_from_u64(200000),
2128 TransparentAddress::PublicKeyHash([0u8; 20]).script().into(),
2129 ),
2130 }],
2131 &payments[..],
2132 &sapling_fees::EmptyBundleView,
2133 #[cfg(feature = "orchard")]
2134 &orchard_fees::EmptyBundleView,
2135 #[cfg(feature = "orchard")]
2136 &orchard_fees::EmptyBundleView,
2137 None,
2138 &ChangeReceivers::new(false, false, Some(kind)),
2139 )
2140 };
2141
2142 assert_matches!(
2143 balance(TransparentChangeKind::P2sh),
2144 Ok(balance) if
2145 balance.proposed_change() == [ChangeValue::transparent(Zatoshis::const_from_u64(40000))] &&
2146 balance.fee_required() == Zatoshis::const_from_u64(80000)
2147 );
2148 assert_matches!(
2149 balance(TransparentChangeKind::P2pkh),
2150 Ok(balance) if
2151 balance.proposed_change() == [ChangeValue::transparent(Zatoshis::const_from_u64(35000))] &&
2152 balance.fee_required() == Zatoshis::const_from_u64(85000)
2153 );
2154 }
2155
2156 #[test]
2157 #[cfg(feature = "transparent-inputs")]
2158 fn ephemeral_output_requires_a_p2pkh_receiver() {
2159 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
2160 Zip317FeeRule::standard(),
2161 None,
2162 ShieldedPool::Sapling,
2163 DustOutputPolicy::default(),
2164 );
2165
2166 let balance = |kind| {
2170 change_strategy.compute_balance::<_, Infallible>(
2171 &Network::TestNetwork,
2172 Network::TestNetwork
2173 .activation_height(NetworkUpgrade::Nu5)
2174 .unwrap()
2175 .into(),
2176 BlockHeight::from_u32(1),
2177 &PoolMigrationParams::new(
2178 zcash_protocol::zip318::BlockSpacing::UNIFORM,
2179 AnchorRetentionInterval::ZIP_318,
2180 ),
2181 &[TestTransparentInput {
2182 outpoint: OutPoint::fake(),
2183 coin: TxOut::new(
2184 Zatoshis::const_from_u64(100000),
2185 TransparentAddress::PublicKeyHash([0u8; 20]).script().into(),
2186 ),
2187 }],
2188 &[] as &[TxOut],
2189 &sapling_fees::EmptyBundleView,
2190 #[cfg(feature = "orchard")]
2191 &orchard_fees::EmptyBundleView,
2192 #[cfg(feature = "orchard")]
2193 &orchard_fees::EmptyBundleView,
2194 Some(EphemeralBalance::Output(Zatoshis::const_from_u64(50000))),
2195 &ChangeReceivers::new(true, false, Some(kind)),
2196 )
2197 };
2198
2199 assert_matches!(
2200 balance(TransparentChangeKind::P2sh),
2201 Err(ChangeError::UnsupportedChangePool(pool)) if pool == PoolType::TRANSPARENT
2202 );
2203 assert_matches!(
2204 balance(TransparentChangeKind::P2pkh),
2205 Ok(balance) if balance.proposed_change().iter().any(|c| c.is_ephemeral())
2206 );
2207 }
2208}