1use core::marker::PhantomData;
8
9use zcash_primitives::transaction::fees::{transparent, zip317 as prim_zip317, FeeRule};
10use zcash_protocol::{
11 consensus::{self, BlockHeight},
12 memo::MemoBytes,
13 value::{BalanceError, Zatoshis},
14 ShieldedProtocol,
15};
16
17use crate::{
18 data_api::{AccountMeta, InputSource, NoteFilter},
19 fees::StandardFeeRule,
20};
21
22use super::{
23 common::{single_pool_output_balance, SinglePoolBalanceConfig},
24 sapling as sapling_fees, ChangeError, ChangeStrategy, DustOutputPolicy, EphemeralBalance,
25 SplitPolicy, TransactionBalance,
26};
27
28#[cfg(feature = "orchard")]
29use super::orchard as orchard_fees;
30
31pub trait Zip317FeeRule: FeeRule {
34 fn marginal_fee(&self) -> Zatoshis;
36
37 fn grace_actions(&self) -> usize;
39}
40
41impl Zip317FeeRule for prim_zip317::FeeRule {
42 fn marginal_fee(&self) -> Zatoshis {
43 self.marginal_fee()
44 }
45
46 fn grace_actions(&self) -> usize {
47 self.grace_actions()
48 }
49}
50
51impl Zip317FeeRule for StandardFeeRule {
52 fn marginal_fee(&self) -> Zatoshis {
53 prim_zip317::FeeRule::standard().marginal_fee()
54 }
55
56 fn grace_actions(&self) -> usize {
57 prim_zip317::FeeRule::standard().grace_actions()
58 }
59}
60
61pub struct SingleOutputChangeStrategy<R, I> {
66 fee_rule: R,
67 change_memo: Option<MemoBytes>,
68 fallback_change_pool: ShieldedProtocol,
69 dust_output_policy: DustOutputPolicy,
70 meta_source: PhantomData<I>,
71}
72
73impl<R, I> SingleOutputChangeStrategy<R, I> {
74 pub fn new(
80 fee_rule: R,
81 change_memo: Option<MemoBytes>,
82 fallback_change_pool: ShieldedProtocol,
83 dust_output_policy: DustOutputPolicy,
84 ) -> Self {
85 Self {
86 fee_rule,
87 change_memo,
88 fallback_change_pool,
89 dust_output_policy,
90 meta_source: PhantomData,
91 }
92 }
93}
94
95impl<R, I> ChangeStrategy for SingleOutputChangeStrategy<R, I>
96where
97 R: Zip317FeeRule + Clone,
98 I: InputSource,
99 <R as FeeRule>::Error: From<BalanceError>,
100{
101 type FeeRule = R;
102 type Error = <R as FeeRule>::Error;
103 type MetaSource = I;
104 type AccountMetaT = ();
105
106 fn fee_rule(&self) -> &Self::FeeRule {
107 &self.fee_rule
108 }
109
110 fn fetch_wallet_meta(
111 &self,
112 _meta_source: &Self::MetaSource,
113 _account: <Self::MetaSource as InputSource>::AccountId,
114 _exclude: &[<Self::MetaSource as InputSource>::NoteRef],
115 ) -> Result<Self::AccountMetaT, <Self::MetaSource as InputSource>::Error> {
116 Ok(())
117 }
118
119 fn compute_balance<P: consensus::Parameters, NoteRefT: Clone>(
120 &self,
121 params: &P,
122 target_height: BlockHeight,
123 transparent_inputs: &[impl transparent::InputView],
124 transparent_outputs: &[impl transparent::OutputView],
125 sapling: &impl sapling_fees::BundleView<NoteRefT>,
126 #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
127 ephemeral_balance: Option<&EphemeralBalance>,
128 _wallet_meta: &Self::AccountMetaT,
129 ) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
130 let split_policy = SplitPolicy::single_output();
131 let cfg = SinglePoolBalanceConfig::new(
132 params,
133 &self.fee_rule,
134 &self.dust_output_policy,
135 self.fee_rule.marginal_fee(),
136 &split_policy,
137 self.fallback_change_pool,
138 self.fee_rule.marginal_fee(),
139 self.fee_rule.grace_actions(),
140 );
141
142 single_pool_output_balance(
143 cfg,
144 None,
145 target_height,
146 transparent_inputs,
147 transparent_outputs,
148 sapling,
149 #[cfg(feature = "orchard")]
150 orchard,
151 self.change_memo.as_ref(),
152 ephemeral_balance,
153 )
154 }
155}
156
157pub struct MultiOutputChangeStrategy<R, I> {
160 fee_rule: R,
161 change_memo: Option<MemoBytes>,
162 fallback_change_pool: ShieldedProtocol,
163 dust_output_policy: DustOutputPolicy,
164 split_policy: SplitPolicy,
165 meta_source: PhantomData<I>,
166}
167
168impl<R, I> MultiOutputChangeStrategy<R, I> {
169 pub fn new(
181 fee_rule: R,
182 change_memo: Option<MemoBytes>,
183 fallback_change_pool: ShieldedProtocol,
184 dust_output_policy: DustOutputPolicy,
185 split_policy: SplitPolicy,
186 ) -> Self {
187 Self {
188 fee_rule,
189 change_memo,
190 fallback_change_pool,
191 dust_output_policy,
192 split_policy,
193 meta_source: PhantomData,
194 }
195 }
196}
197
198impl<R, I> ChangeStrategy for MultiOutputChangeStrategy<R, I>
199where
200 R: Zip317FeeRule + Clone,
201 I: InputSource,
202 <R as FeeRule>::Error: From<BalanceError>,
203{
204 type FeeRule = R;
205 type Error = <R as FeeRule>::Error;
206 type MetaSource = I;
207 type AccountMetaT = AccountMeta;
208
209 fn fee_rule(&self) -> &Self::FeeRule {
210 &self.fee_rule
211 }
212
213 fn fetch_wallet_meta(
214 &self,
215 meta_source: &Self::MetaSource,
216 account: <Self::MetaSource as InputSource>::AccountId,
217 exclude: &[<Self::MetaSource as InputSource>::NoteRef],
218 ) -> Result<Self::AccountMetaT, <Self::MetaSource as InputSource>::Error> {
219 let note_selector = NoteFilter::ExceedsMinValue(
220 self.split_policy
221 .min_split_output_value()
222 .unwrap_or(SplitPolicy::MIN_NOTE_VALUE),
223 );
224
225 meta_source.get_account_metadata(account, ¬e_selector, exclude)
226 }
227
228 fn compute_balance<P: consensus::Parameters, NoteRefT: Clone>(
229 &self,
230 params: &P,
231 target_height: BlockHeight,
232 transparent_inputs: &[impl transparent::InputView],
233 transparent_outputs: &[impl transparent::OutputView],
234 sapling: &impl sapling_fees::BundleView<NoteRefT>,
235 #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
236 ephemeral_balance: Option<&EphemeralBalance>,
237 wallet_meta: &Self::AccountMetaT,
238 ) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
239 let cfg = SinglePoolBalanceConfig::new(
240 params,
241 &self.fee_rule,
242 &self.dust_output_policy,
243 self.fee_rule.marginal_fee(),
244 &self.split_policy,
245 self.fallback_change_pool,
246 self.fee_rule.marginal_fee(),
247 self.fee_rule.grace_actions(),
248 );
249
250 single_pool_output_balance(
251 cfg,
252 Some(wallet_meta),
253 target_height,
254 transparent_inputs,
255 transparent_outputs,
256 sapling,
257 #[cfg(feature = "orchard")]
258 orchard,
259 self.change_memo.as_ref(),
260 ephemeral_balance,
261 )
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use core::{convert::Infallible, num::NonZeroUsize};
268
269 use ::transparent::{address::Script, bundle::TxOut};
270 use zcash_primitives::transaction::fees::zip317::FeeRule as Zip317FeeRule;
271 use zcash_protocol::{
272 consensus::{Network, NetworkUpgrade, Parameters},
273 value::Zatoshis,
274 ShieldedProtocol,
275 };
276
277 use super::SingleOutputChangeStrategy;
278 use crate::{
279 data_api::{
280 testing::MockWalletDb, wallet::input_selection::SaplingPayment, AccountMeta, PoolMeta,
281 },
282 fees::{
283 tests::{TestSaplingInput, TestTransparentInput},
284 zip317::MultiOutputChangeStrategy,
285 ChangeError, ChangeStrategy, ChangeValue, DustAction, DustOutputPolicy, SplitPolicy,
286 },
287 };
288
289 #[cfg(feature = "orchard")]
290 use {
291 crate::data_api::wallet::input_selection::OrchardPayment,
292 crate::fees::orchard as orchard_fees,
293 };
294
295 #[test]
296 fn change_without_dust() {
297 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
298 Zip317FeeRule::standard(),
299 None,
300 ShieldedProtocol::Sapling,
301 DustOutputPolicy::default(),
302 );
303
304 let result = change_strategy.compute_balance(
306 &Network::TestNetwork,
307 Network::TestNetwork
308 .activation_height(NetworkUpgrade::Nu5)
309 .unwrap(),
310 &[] as &[TestTransparentInput],
311 &[] as &[TxOut],
312 &(
313 sapling::builder::BundleType::DEFAULT,
314 &[TestSaplingInput {
315 note_id: 0,
316 value: Zatoshis::const_from_u64(55000),
317 }][..],
318 &[SaplingPayment::new(Zatoshis::const_from_u64(40000))][..],
319 ),
320 #[cfg(feature = "orchard")]
321 &orchard_fees::EmptyBundleView,
322 None,
323 &(),
324 );
325
326 assert_matches!(
327 result,
328 Ok(balance) if
329 balance.proposed_change() == [ChangeValue::sapling(Zatoshis::const_from_u64(5000), None)] &&
330 balance.fee_required() == Zatoshis::const_from_u64(10000)
331 );
332 }
333
334 #[test]
335 fn change_without_dust_multi() {
336 let change_strategy = MultiOutputChangeStrategy::<_, MockWalletDb>::new(
337 Zip317FeeRule::standard(),
338 None,
339 ShieldedProtocol::Sapling,
340 DustOutputPolicy::default(),
341 SplitPolicy::with_min_output_value(
342 NonZeroUsize::new(5).unwrap(),
343 Zatoshis::const_from_u64(100_0000),
344 ),
345 );
346
347 {
348 let balance = |existing_notes, total| {
350 change_strategy.compute_balance(
351 &Network::TestNetwork,
352 Network::TestNetwork
353 .activation_height(NetworkUpgrade::Nu5)
354 .unwrap(),
355 &[] as &[TestTransparentInput],
356 &[] as &[TxOut],
357 &(
358 sapling::builder::BundleType::DEFAULT,
359 &[TestSaplingInput {
360 note_id: 0,
361 value: Zatoshis::const_from_u64(750_0000),
362 }][..],
363 &[SaplingPayment::new(Zatoshis::const_from_u64(100_0000))][..],
364 ),
365 #[cfg(feature = "orchard")]
366 &orchard_fees::EmptyBundleView,
367 None,
368 &AccountMeta::new(Some(PoolMeta::new(existing_notes, total)), None),
369 )
370 };
371
372 assert_matches!(
373 balance(0, Zatoshis::ZERO),
374 Ok(balance) if
375 balance.proposed_change() == [
376 ChangeValue::sapling(Zatoshis::const_from_u64(129_4000), None),
377 ChangeValue::sapling(Zatoshis::const_from_u64(129_4000), None),
378 ChangeValue::sapling(Zatoshis::const_from_u64(129_4000), None),
379 ChangeValue::sapling(Zatoshis::const_from_u64(129_4000), None),
380 ChangeValue::sapling(Zatoshis::const_from_u64(129_4000), None),
381 ] &&
382 balance.fee_required() == Zatoshis::const_from_u64(30000)
383 );
384
385 assert_matches!(
386 balance(2, Zatoshis::const_from_u64(100_0000)),
387 Ok(balance) if
388 balance.proposed_change() == [
389 ChangeValue::sapling(Zatoshis::const_from_u64(216_0000), None),
390 ChangeValue::sapling(Zatoshis::const_from_u64(216_0000), None),
391 ChangeValue::sapling(Zatoshis::const_from_u64(216_0000), None),
392 ] &&
393 balance.fee_required() == Zatoshis::const_from_u64(20000)
394 );
395 }
396
397 {
398 let result = change_strategy.compute_balance(
401 &Network::TestNetwork,
402 Network::TestNetwork
403 .activation_height(NetworkUpgrade::Nu5)
404 .unwrap(),
405 &[] as &[TestTransparentInput],
406 &[] as &[TxOut],
407 &(
408 sapling::builder::BundleType::DEFAULT,
409 &[TestSaplingInput {
410 note_id: 0,
411 value: Zatoshis::const_from_u64(600_0000),
412 }][..],
413 &[SaplingPayment::new(Zatoshis::const_from_u64(100_0000))][..],
414 ),
415 #[cfg(feature = "orchard")]
416 &orchard_fees::EmptyBundleView,
417 None,
418 &AccountMeta::new(
419 Some(PoolMeta::new(0, Zatoshis::ZERO)),
420 Some(PoolMeta::new(0, Zatoshis::ZERO)),
421 ),
422 );
423
424 assert_matches!(
425 result,
426 Ok(balance) if
427 balance.proposed_change() == [
428 ChangeValue::sapling(Zatoshis::const_from_u64(124_3750), None),
429 ChangeValue::sapling(Zatoshis::const_from_u64(124_3750), None),
430 ChangeValue::sapling(Zatoshis::const_from_u64(124_3750), None),
431 ChangeValue::sapling(Zatoshis::const_from_u64(124_3750), None),
432 ] &&
433 balance.fee_required() == Zatoshis::const_from_u64(25000)
434 );
435 }
436
437 {
438 let result = change_strategy.compute_balance(
441 &Network::TestNetwork,
442 Network::TestNetwork
443 .activation_height(NetworkUpgrade::Nu5)
444 .unwrap(),
445 &[] as &[TestTransparentInput],
446 &[] as &[TxOut],
447 &(
448 sapling::builder::BundleType::DEFAULT,
449 &[TestSaplingInput {
450 note_id: 0,
451 value: Zatoshis::const_from_u64(50000),
452 }][..],
453 &[SaplingPayment::new(Zatoshis::const_from_u64(40000))][..],
454 ),
455 #[cfg(feature = "orchard")]
456 &orchard_fees::EmptyBundleView,
457 None,
458 &AccountMeta::new(
460 Some(PoolMeta::new(0, Zatoshis::ZERO)),
461 Some(PoolMeta::new(0, Zatoshis::ZERO)),
462 ),
463 );
464
465 assert_matches!(
466 result,
467 Ok(balance) if
468 balance.proposed_change() == [ChangeValue::sapling(Zatoshis::ZERO, None)] &&
469 balance.fee_required() == Zatoshis::const_from_u64(10000)
470 );
471 }
472
473 {
474 let result = change_strategy.compute_balance(
476 &Network::TestNetwork,
477 Network::TestNetwork
478 .activation_height(NetworkUpgrade::Nu5)
479 .unwrap(),
480 &[] as &[TestTransparentInput],
481 &[] as &[TxOut],
482 &(
483 sapling::builder::BundleType::DEFAULT,
484 &[TestSaplingInput {
485 note_id: 0,
486 value: Zatoshis::const_from_u64(50000),
487 }][..],
488 &[SaplingPayment::new(Zatoshis::const_from_u64(40001))][..],
489 ),
490 #[cfg(feature = "orchard")]
491 &orchard_fees::EmptyBundleView,
492 None,
493 &AccountMeta::new(
495 Some(PoolMeta::new(0, Zatoshis::ZERO)),
496 Some(PoolMeta::new(0, Zatoshis::ZERO)),
497 ),
498 );
499
500 assert_matches!(
501 result,
502 Err(ChangeError::InsufficientFunds { available, required })
503 if available == Zatoshis::const_from_u64(50000)
504 && required == Zatoshis::const_from_u64(50001)
505 );
506 }
507
508 {
509 let result = change_strategy.compute_balance(
516 &Network::TestNetwork,
517 Network::TestNetwork
518 .activation_height(NetworkUpgrade::Nu5)
519 .unwrap(),
520 &[] as &[TestTransparentInput],
521 &[] as &[TxOut],
522 &(
523 sapling::builder::BundleType::DEFAULT,
524 &[TestSaplingInput {
525 note_id: 0,
526 value: Zatoshis::const_from_u64(50000),
527 }][..],
528 &[
529 SaplingPayment::new(Zatoshis::const_from_u64(30000)),
530 SaplingPayment::new(Zatoshis::const_from_u64(10000)),
531 ][..],
532 ),
533 #[cfg(feature = "orchard")]
534 &orchard_fees::EmptyBundleView,
535 None,
536 &AccountMeta::new(
538 Some(PoolMeta::new(0, Zatoshis::ZERO)),
539 Some(PoolMeta::new(0, Zatoshis::ZERO)),
540 ),
541 );
542
543 assert_matches!(
544 result,
545 Err(ChangeError::InsufficientFunds { available, required })
546 if available == Zatoshis::const_from_u64(50000)
547 && required == Zatoshis::const_from_u64(55000)
548 );
549 }
550 }
551
552 #[test]
553 #[cfg(feature = "orchard")]
554 fn cross_pool_change_without_dust() {
555 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
556 Zip317FeeRule::standard(),
557 None,
558 ShieldedProtocol::Orchard,
559 DustOutputPolicy::default(),
560 );
561
562 let result = change_strategy.compute_balance(
564 &Network::TestNetwork,
565 Network::TestNetwork
566 .activation_height(NetworkUpgrade::Nu5)
567 .unwrap(),
568 &[] as &[TestTransparentInput],
569 &[] as &[TxOut],
570 &(
571 sapling::builder::BundleType::DEFAULT,
572 &[TestSaplingInput {
573 note_id: 0,
574 value: Zatoshis::const_from_u64(55000),
575 }][..],
576 &[] as &[Infallible],
577 ),
578 &(
579 orchard::builder::BundleType::DEFAULT,
580 &[] as &[Infallible],
581 &[OrchardPayment::new(Zatoshis::const_from_u64(30000))][..],
582 ),
583 None,
584 &(),
585 );
586
587 assert_matches!(
588 result,
589 Ok(balance) if
590 balance.proposed_change() == [ChangeValue::orchard(Zatoshis::const_from_u64(5000), None)] &&
591 balance.fee_required() == Zatoshis::const_from_u64(20000)
592 );
593 }
594
595 #[test]
596 fn change_with_transparent_payments_implicitly_allowing_zero_change() {
597 change_with_transparent_payments(DustOutputPolicy::default())
598 }
599
600 #[test]
601 fn change_with_transparent_payments_explicitly_allowing_zero_change() {
602 change_with_transparent_payments(DustOutputPolicy::new(
603 DustAction::AllowDustChange,
604 Some(Zatoshis::ZERO),
605 ))
606 }
607
608 fn change_with_transparent_payments(dust_output_policy: DustOutputPolicy) {
609 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
610 Zip317FeeRule::standard(),
611 None,
612 ShieldedProtocol::Sapling,
613 dust_output_policy,
614 );
615
616 let result = change_strategy.compute_balance(
618 &Network::TestNetwork,
619 Network::TestNetwork
620 .activation_height(NetworkUpgrade::Nu5)
621 .unwrap(),
622 &[] as &[TestTransparentInput],
623 &[TxOut {
624 value: Zatoshis::const_from_u64(40000),
625 script_pubkey: Script(vec![]),
626 }],
627 &(
628 sapling::builder::BundleType::DEFAULT,
629 &[TestSaplingInput {
630 note_id: 0,
631 value: Zatoshis::const_from_u64(55000),
632 }][..],
633 &[] as &[Infallible],
634 ),
635 #[cfg(feature = "orchard")]
636 &orchard_fees::EmptyBundleView,
637 None,
638 &(),
639 );
640
641 assert_matches!(
642 result,
643 Ok(balance) if
644 balance.proposed_change() == [ChangeValue::sapling(Zatoshis::ZERO, None)]
645 && balance.fee_required() == Zatoshis::const_from_u64(15000)
646 );
647 }
648
649 #[test]
650 #[cfg(feature = "transparent-inputs")]
651 fn change_fully_transparent_no_change() {
652 use crate::fees::sapling as sapling_fees;
653 use ::transparent::{address::TransparentAddress, bundle::OutPoint};
654
655 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
656 Zip317FeeRule::standard(),
657 None,
658 ShieldedProtocol::Sapling,
659 DustOutputPolicy::default(),
660 );
661
662 let result = change_strategy.compute_balance::<_, Infallible>(
664 &Network::TestNetwork,
665 Network::TestNetwork
666 .activation_height(NetworkUpgrade::Nu5)
667 .unwrap(),
668 &[TestTransparentInput {
669 outpoint: OutPoint::fake(),
670 coin: TxOut {
671 value: Zatoshis::const_from_u64(50000),
672 script_pubkey: TransparentAddress::PublicKeyHash([0u8; 20]).script(),
673 },
674 }],
675 &[TxOut {
676 value: Zatoshis::const_from_u64(40000),
677 script_pubkey: Script(vec![]),
678 }],
679 &sapling_fees::EmptyBundleView,
680 #[cfg(feature = "orchard")]
681 &orchard_fees::EmptyBundleView,
682 None,
683 &(),
684 );
685
686 assert_matches!(
687 result,
688 Ok(balance) if
689 balance.proposed_change().is_empty() &&
690 balance.fee_required() == Zatoshis::const_from_u64(10000)
691 );
692 }
693
694 #[test]
695 #[cfg(feature = "transparent-inputs")]
696 fn change_transparent_flows_with_shielded_change() {
697 use crate::fees::sapling as sapling_fees;
698 use ::transparent::{address::TransparentAddress, bundle::OutPoint};
699
700 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
701 Zip317FeeRule::standard(),
702 None,
703 ShieldedProtocol::Sapling,
704 DustOutputPolicy::default(),
705 );
706
707 let result = change_strategy.compute_balance::<_, Infallible>(
709 &Network::TestNetwork,
710 Network::TestNetwork
711 .activation_height(NetworkUpgrade::Nu5)
712 .unwrap(),
713 &[TestTransparentInput {
714 outpoint: OutPoint::fake(),
715 coin: TxOut {
716 value: Zatoshis::const_from_u64(63000),
717 script_pubkey: TransparentAddress::PublicKeyHash([0u8; 20]).script(),
718 },
719 }],
720 &[TxOut {
721 value: Zatoshis::const_from_u64(40000),
722 script_pubkey: Script(vec![]),
723 }],
724 &sapling_fees::EmptyBundleView,
725 #[cfg(feature = "orchard")]
726 &orchard_fees::EmptyBundleView,
727 None,
728 &(),
729 );
730
731 assert_matches!(
732 result,
733 Ok(balance) if
734 balance.proposed_change() == [ChangeValue::sapling(Zatoshis::const_from_u64(8000), None)] &&
735 balance.fee_required() == Zatoshis::const_from_u64(15000)
736 );
737 }
738
739 #[test]
740 #[cfg(feature = "transparent-inputs")]
741 fn change_transparent_flows_with_shielded_dust_change() {
742 use crate::fees::sapling as sapling_fees;
743 use ::transparent::{address::TransparentAddress, bundle::OutPoint};
744
745 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
746 Zip317FeeRule::standard(),
747 None,
748 ShieldedProtocol::Sapling,
749 DustOutputPolicy::new(
750 DustAction::AllowDustChange,
751 Some(Zatoshis::const_from_u64(1000)),
752 ),
753 );
754
755 let result = change_strategy.compute_balance::<_, Infallible>(
760 &Network::TestNetwork,
761 Network::TestNetwork
762 .activation_height(NetworkUpgrade::Nu5)
763 .unwrap(),
764 &[TestTransparentInput {
765 outpoint: OutPoint::fake(),
766 coin: TxOut {
767 value: Zatoshis::const_from_u64(56000),
768 script_pubkey: TransparentAddress::PublicKeyHash([0u8; 20]).script(),
769 },
770 }],
771 &[TxOut {
772 value: Zatoshis::const_from_u64(40000),
773 script_pubkey: Script(vec![]),
774 }],
775 &sapling_fees::EmptyBundleView,
776 #[cfg(feature = "orchard")]
777 &orchard_fees::EmptyBundleView,
778 None,
779 &(),
780 );
781
782 assert_matches!(
783 result,
784 Ok(balance) if
785 balance.proposed_change() == [ChangeValue::sapling(Zatoshis::const_from_u64(1000), None)] &&
786 balance.fee_required() == Zatoshis::const_from_u64(15000)
787 );
788 }
789
790 #[test]
791 fn change_with_allowable_dust_implicitly_allowing_zero_change() {
792 change_with_allowable_dust(DustOutputPolicy::default())
793 }
794
795 #[test]
796 fn change_with_allowable_dust_explicitly_allowing_zero_change() {
797 change_with_allowable_dust(DustOutputPolicy::new(
798 DustAction::AllowDustChange,
799 Some(Zatoshis::ZERO),
800 ))
801 }
802
803 fn change_with_allowable_dust(dust_output_policy: DustOutputPolicy) {
804 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
805 Zip317FeeRule::standard(),
806 None,
807 ShieldedProtocol::Sapling,
808 dust_output_policy,
809 );
810
811 let result = change_strategy.compute_balance(
816 &Network::TestNetwork,
817 Network::TestNetwork
818 .activation_height(NetworkUpgrade::Nu5)
819 .unwrap(),
820 &[] as &[TestTransparentInput],
821 &[] as &[TxOut],
822 &(
823 sapling::builder::BundleType::DEFAULT,
824 &[
825 TestSaplingInput {
826 note_id: 0,
827 value: Zatoshis::const_from_u64(49000),
828 },
829 TestSaplingInput {
830 note_id: 1,
831 value: Zatoshis::const_from_u64(1000),
832 },
833 ][..],
834 &[SaplingPayment::new(Zatoshis::const_from_u64(40000))][..],
835 ),
836 #[cfg(feature = "orchard")]
837 &orchard_fees::EmptyBundleView,
838 None,
839 &(),
840 );
841
842 assert_matches!(
843 result,
844 Ok(balance) if
845 balance.proposed_change() == [ChangeValue::sapling(Zatoshis::ZERO, None)] &&
846 balance.fee_required() == Zatoshis::const_from_u64(10000)
847 );
848 }
849
850 #[test]
851 fn change_with_disallowed_dust() {
852 let change_strategy = SingleOutputChangeStrategy::<_, MockWalletDb>::new(
853 Zip317FeeRule::standard(),
854 None,
855 ShieldedProtocol::Sapling,
856 DustOutputPolicy::default(),
857 );
858
859 let result = change_strategy.compute_balance(
862 &Network::TestNetwork,
863 Network::TestNetwork
864 .activation_height(NetworkUpgrade::Nu5)
865 .unwrap(),
866 &[] as &[TestTransparentInput],
867 &[] as &[TxOut],
868 &(
869 sapling::builder::BundleType::DEFAULT,
870 &[
871 TestSaplingInput {
872 note_id: 0,
873 value: Zatoshis::const_from_u64(29000),
874 },
875 TestSaplingInput {
876 note_id: 1,
877 value: Zatoshis::const_from_u64(20000),
878 },
879 TestSaplingInput {
880 note_id: 2,
881 value: Zatoshis::const_from_u64(1000),
882 },
883 ][..],
884 &[SaplingPayment::new(Zatoshis::const_from_u64(30000))][..],
885 ),
886 #[cfg(feature = "orchard")]
887 &orchard_fees::EmptyBundleView,
888 None,
889 &(),
890 );
891
892 assert_matches!(
895 result,
896 Err(ChangeError::DustInputs { sapling, .. }) if sapling == vec![2]
897 );
898 }
899}