Skip to main content

zcash_client_backend/
fees.rs

1use std::{
2    convert::Infallible,
3    fmt::{self, Debug, Display},
4    num::{NonZeroU64, NonZeroUsize},
5};
6
7use ::transparent::bundle::OutPoint;
8use zcash_primitives::transaction::fees::{
9    FeeRule,
10    transparent::{self, InputSize},
11    zip317 as prim_zip317,
12};
13use zcash_protocol::{
14    PoolType, ShieldedPool,
15    consensus::{self, BlockHeight},
16    memo::MemoBytes,
17    value::{BalanceError, Zatoshis},
18};
19
20use crate::data_api::{
21    ChangeReceivers, InputSource, anchor_retention::PoolMigrationParams, wallet::TargetHeight,
22};
23
24pub mod common;
25#[cfg(feature = "non-standard-fees")]
26pub mod fixed;
27#[cfg(feature = "orchard")]
28pub mod orchard;
29pub mod sapling;
30pub mod standard;
31pub mod zip317;
32
33/// An enumeration of the standard fee rules supported by the wallet backend.
34#[derive(Debug, Copy, Clone, PartialEq, Eq)]
35pub enum StandardFeeRule {
36    Zip317,
37}
38
39impl FeeRule for StandardFeeRule {
40    type Error = prim_zip317::FeeError;
41
42    fn fee_required<P: consensus::Parameters>(
43        &self,
44        params: &P,
45        target_height: BlockHeight,
46        transparent_input_sizes: impl IntoIterator<Item = InputSize>,
47        transparent_output_sizes: impl IntoIterator<Item = usize>,
48        sapling_input_count: usize,
49        sapling_output_count: usize,
50        orchard_action_count: usize,
51        ironwood_action_count: usize,
52    ) -> Result<Zatoshis, Self::Error> {
53        #[allow(deprecated)]
54        match self {
55            Self::Zip317 => prim_zip317::FeeRule::standard().fee_required(
56                params,
57                target_height,
58                transparent_input_sizes,
59                transparent_output_sizes,
60                sapling_input_count,
61                sapling_output_count,
62                orchard_action_count,
63                ironwood_action_count,
64            ),
65        }
66    }
67}
68
69/// A policy that determines how change should be returned to the wallet when the net flows of a
70/// transaction under construction are fully transparent.
71///
72/// This policy has no effect on transactions that have any shielded inputs or outputs; change
73/// for such transactions is always returned to a shielded pool, irrespective of the policy in
74/// use. When the flows of a transaction are fully transparent, shielding change (the default)
75/// reveals the change amount as the value of the shielded output(s) in an otherwise-transparent
76/// transaction; returning the change to the transparent pool matches the behavior of
77/// transparent-only wallets (including `zcashd`) at the cost of the change remaining unshielded.
78///
79/// Transparent change is currently always sent to a P2PKH address derived under the wallet
80/// account's internal scope; returning change to the originating address when spending from a
81/// P2SH (e.g. multisig) address is not yet supported. See [zcash/librustzcash#2570] for
82/// details.
83///
84/// [zcash/librustzcash#2570]: https://github.com/zcash/librustzcash/issues/2570
85#[cfg(feature = "transparent-inputs")]
86#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
87pub enum TransparentChangePolicy {
88    /// Change is always returned to a shielded pool, even when the net flows of the transaction
89    /// are fully transparent.
90    ///
91    /// This is the default policy.
92    #[default]
93    ShieldChange,
94    /// When the net flows of the transaction are fully transparent, change is returned to the
95    /// transparent pool at an internal-scope (change) transparent address of the wallet, as
96    /// described in [BIP 44].
97    ///
98    /// [BIP 44]: https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
99    TransparentChangeAllowed,
100}
101
102/// `ChangeValue` represents either a proposed change output to a shielded pool
103/// (with an optional change memo), or if the "transparent-inputs" feature is
104/// enabled, an output to the transparent pool: either an ephemeral output as
105/// part of a [ZIP 320] transaction pair, or a change output to an
106/// internal-scope (change) transparent address of the wallet.
107///
108/// [ZIP 320]: https://zips.z.cash/zip-0320
109#[derive(Clone, Debug, PartialEq, Eq)]
110pub struct ChangeValue(ChangeValueInner);
111
112#[derive(Clone, Debug, PartialEq, Eq)]
113enum ChangeValueInner {
114    Shielded {
115        protocol: ShieldedPool,
116        value: Zatoshis,
117        memo: Option<MemoBytes>,
118    },
119    #[cfg(feature = "transparent-inputs")]
120    EphemeralTransparent { value: Zatoshis },
121    #[cfg(feature = "transparent-inputs")]
122    Transparent { value: Zatoshis },
123}
124
125impl ChangeValue {
126    /// Constructs a new ephemeral transparent output value.
127    #[cfg(feature = "transparent-inputs")]
128    pub fn ephemeral_transparent(value: Zatoshis) -> Self {
129        Self(ChangeValueInner::EphemeralTransparent { value })
130    }
131
132    /// Constructs a new change value that will be created as a non-ephemeral transparent output
133    /// sent to an internal-scope (change) transparent address of the wallet.
134    #[cfg(feature = "transparent-inputs")]
135    pub fn transparent(value: Zatoshis) -> Self {
136        Self(ChangeValueInner::Transparent { value })
137    }
138
139    /// Constructs a new change value that will be created as a shielded output.
140    pub fn shielded(protocol: ShieldedPool, value: Zatoshis, memo: Option<MemoBytes>) -> Self {
141        Self(ChangeValueInner::Shielded {
142            protocol,
143            value,
144            memo,
145        })
146    }
147
148    /// Constructs a new change value that will be created as a Sapling output.
149    pub fn sapling(value: Zatoshis, memo: Option<MemoBytes>) -> Self {
150        Self::shielded(ShieldedPool::Sapling, value, memo)
151    }
152
153    /// Constructs a new change value that will be created as an Orchard output.
154    #[cfg(feature = "orchard")]
155    pub fn orchard(value: Zatoshis, memo: Option<MemoBytes>) -> Self {
156        Self::shielded(ShieldedPool::Orchard, value, memo)
157    }
158
159    /// Constructs a new change value that will be created as an Ironwood output.
160    #[cfg(feature = "orchard")]
161    pub fn ironwood(value: Zatoshis, memo: Option<MemoBytes>) -> Self {
162        Self::shielded(ShieldedPool::Ironwood, value, memo)
163    }
164
165    /// Returns the pool to which the change or ephemeral output should be sent.
166    pub fn output_pool(&self) -> PoolType {
167        match &self.0 {
168            ChangeValueInner::Shielded { protocol, .. } => PoolType::Shielded(*protocol),
169            #[cfg(feature = "transparent-inputs")]
170            ChangeValueInner::EphemeralTransparent { .. } => PoolType::Transparent,
171            #[cfg(feature = "transparent-inputs")]
172            ChangeValueInner::Transparent { .. } => PoolType::Transparent,
173        }
174    }
175
176    /// Returns the value of the change or ephemeral output to be created, in zatoshis.
177    pub fn value(&self) -> Zatoshis {
178        match &self.0 {
179            ChangeValueInner::Shielded { value, .. } => *value,
180            #[cfg(feature = "transparent-inputs")]
181            ChangeValueInner::EphemeralTransparent { value } => *value,
182            #[cfg(feature = "transparent-inputs")]
183            ChangeValueInner::Transparent { value } => *value,
184        }
185    }
186
187    /// Returns the memo to be associated with the output.
188    pub fn memo(&self) -> Option<&MemoBytes> {
189        match &self.0 {
190            ChangeValueInner::Shielded { memo, .. } => memo.as_ref(),
191            #[cfg(feature = "transparent-inputs")]
192            ChangeValueInner::EphemeralTransparent { .. } => None,
193            #[cfg(feature = "transparent-inputs")]
194            ChangeValueInner::Transparent { .. } => None,
195        }
196    }
197
198    /// Whether this is to be an ephemeral output.
199    #[cfg_attr(
200        not(feature = "transparent-inputs"),
201        doc = "This is always false because the `transparent-inputs` feature is
202               not enabled."
203    )]
204    pub fn is_ephemeral(&self) -> bool {
205        match &self.0 {
206            ChangeValueInner::Shielded { .. } => false,
207            #[cfg(feature = "transparent-inputs")]
208            ChangeValueInner::EphemeralTransparent { .. } => true,
209            #[cfg(feature = "transparent-inputs")]
210            ChangeValueInner::Transparent { .. } => false,
211        }
212    }
213}
214
215/// Orchard actions in a canonical ZIP 318 crossing: the spend and its change, or a padding dummy
216/// when the note's value exactly covers the crossing and its fee.
217#[cfg(feature = "orchard")]
218const CANONICAL_CROSSING_ORCHARD_ACTIONS: usize = 2;
219
220/// Ironwood actions in a canonical ZIP 318 crossing: the single unpadded output.
221#[cfg(feature = "orchard")]
222const CANONICAL_CROSSING_IRONWOOD_ACTIONS: usize = 1;
223
224/// The fee a canonical ZIP 318 crossing pays at `target_height`, obtained by asking the STANDARD
225/// ZIP 317 rule what the canonical shape costs.
226///
227/// ZIP 318 requires this exact fee. Any other value partitions the anonymity set, so a transaction
228/// paying a non-standard fee is not a canonical crossing however well its structure matches. The
229/// standard rule is used rather than the caller's: a proposal built on a fixed non-standard rule
230/// would otherwise be compared against its own fee and always agree.
231#[cfg(feature = "orchard")]
232pub fn canonical_crossing_fee<P: consensus::Parameters>(
233    params: &P,
234    target_height: BlockHeight,
235) -> Result<Zatoshis, zcash_primitives::transaction::fees::zip317::FeeError> {
236    prim_zip317::FeeRule::standard().fee_required(
237        params,
238        target_height,
239        std::iter::empty::<InputSize>(),
240        std::iter::empty::<usize>(),
241        0,
242        0,
243        CANONICAL_CROSSING_ORCHARD_ACTIONS,
244        CANONICAL_CROSSING_IRONWOOD_ACTIONS,
245    )
246}
247
248/// The amount of change and fees required to make a transaction's inputs and
249/// outputs balance under a specific fee rule, as computed by a particular
250/// [`ChangeStrategy`] that is aware of that rule.
251#[derive(Clone, Debug, PartialEq, Eq)]
252pub struct TransactionBalance {
253    proposed_change: Vec<ChangeValue>,
254    fee_required: Zatoshis,
255
256    // A cache for the sum of proposed change and fee; we compute it on construction anyway, so we
257    // cache the resulting value.
258    total: Zatoshis,
259
260    // The exact number of dummy outputs in each shielded bundle this balance was costed for.
261    // `None` is retained for compatibility with callers and serialized proposals that predate
262    // explicit transaction-shape modelling.
263    dummy_outputs: Option<DummyOutputCounts>,
264}
265
266/// The number of dummy outputs in each shielded value pool.
267#[derive(Clone, Copy, Debug, PartialEq, Eq)]
268pub struct DummyOutputCounts {
269    sapling: usize,
270    #[cfg(feature = "orchard")]
271    orchard: usize,
272    #[cfg(feature = "orchard")]
273    ironwood: usize,
274}
275
276impl DummyOutputCounts {
277    /// Constructs per-pool dummy-output counts.
278    pub fn new(
279        sapling: usize,
280        #[cfg(feature = "orchard")] orchard: usize,
281        #[cfg(feature = "orchard")] ironwood: usize,
282    ) -> Self {
283        Self {
284            sapling,
285            #[cfg(feature = "orchard")]
286            orchard,
287            #[cfg(feature = "orchard")]
288            ironwood,
289        }
290    }
291
292    /// Returns the number of Sapling dummy outputs.
293    pub fn sapling(&self) -> usize {
294        self.sapling
295    }
296
297    /// Returns the number of Orchard dummy outputs.
298    #[cfg(feature = "orchard")]
299    pub fn orchard(&self) -> usize {
300        self.orchard
301    }
302
303    /// Returns the number of Ironwood dummy outputs.
304    #[cfg(feature = "orchard")]
305    pub fn ironwood(&self) -> usize {
306        self.ironwood
307    }
308}
309
310impl TransactionBalance {
311    /// Constructs a new balance from its constituent parts.
312    pub fn new(
313        proposed_change: Vec<ChangeValue>,
314        fee_required: Zatoshis,
315    ) -> Result<Self, BalanceError> {
316        let total = proposed_change
317            .iter()
318            .map(|c| c.value())
319            .chain(Some(fee_required))
320            .sum::<Option<Zatoshis>>()
321            .ok_or(BalanceError::Overflow)?;
322
323        Ok(Self {
324            proposed_change,
325            fee_required,
326            total,
327            dummy_outputs: None,
328        })
329    }
330
331    /// Records the exact dummy-output counts this balance was computed for.
332    pub fn with_dummy_outputs(mut self, dummy_outputs: DummyOutputCounts) -> Self {
333        self.dummy_outputs = Some(dummy_outputs);
334        self
335    }
336
337    /// Returns the exact dummy-output counts this balance was computed for, when recorded.
338    pub fn dummy_outputs(&self) -> Option<DummyOutputCounts> {
339        self.dummy_outputs
340    }
341
342    /// The change values proposed by the [`ChangeStrategy`] that computed this balance.
343    pub fn proposed_change(&self) -> &[ChangeValue] {
344        &self.proposed_change
345    }
346
347    /// Returns the fee computed for the transaction, assuming that the suggested
348    /// change outputs are added to the transaction.
349    pub fn fee_required(&self) -> Zatoshis {
350        self.fee_required
351    }
352
353    /// Returns the sum of the proposed change outputs and the required fee.
354    pub fn total(&self) -> Zatoshis {
355        self.total
356    }
357}
358
359/// Errors that can occur in computing suggested change and/or fees.
360#[derive(Clone, Debug, PartialEq, Eq)]
361#[non_exhaustive]
362pub enum ChangeError<E, NoteRefT> {
363    /// Insufficient inputs were provided to change selection to fund the
364    /// required outputs and fees.
365    InsufficientFunds {
366        /// The total of the inputs provided to change selection
367        available: Zatoshis,
368        /// The total amount of input value required to fund the requested outputs,
369        /// including the required fees.
370        required: Zatoshis,
371    },
372    /// Some of the inputs provided to the transaction have value less than the
373    /// marginal fee, and could not be determined to have any economic value in
374    /// the context of this input selection.
375    ///
376    /// This determination is potentially conservative in the sense that inputs
377    /// with value less than or equal to the marginal fee might be excluded, even
378    /// though in practice they would not cause the fee to increase. Inputs with
379    /// value greater than the marginal fee will never be excluded.
380    ///
381    /// The ordering of the inputs in each list is unspecified.
382    DustInputs {
383        /// The outpoints for transparent inputs that could not be determined to
384        /// have economic value in the context of this input selection.
385        transparent: Vec<OutPoint>,
386        /// The identifiers for Sapling inputs that could not be determined to
387        /// have economic value in the context of this input selection.
388        sapling: Vec<NoteRefT>,
389        /// The identifiers for Orchard inputs that could not be determined to
390        /// have economic value in the context of this input selection.
391        #[cfg(feature = "orchard")]
392        orchard: Vec<NoteRefT>,
393        /// The identifiers for Ironwood inputs that could not be determined to
394        /// have economic value in the context of this input selection.
395        #[cfg(feature = "orchard")]
396        ironwood: Vec<NoteRefT>,
397    },
398    /// The change strategy would send change to a pool in which the account cannot receive
399    /// it: the account's viewing key cannot derive an address in that pool (for an ephemeral
400    /// transparent output, a P2PKH address).
401    UnsupportedChangePool(PoolType),
402    /// An error occurred that was specific to the change selection strategy in use.
403    StrategyError(E),
404    /// The proposed bundle structure would violate bundle type construction rules.
405    BundleError(&'static str),
406}
407
408impl<CE: fmt::Display, N: fmt::Display> fmt::Display for ChangeError<CE, N> {
409    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
410        match &self {
411            ChangeError::InsufficientFunds {
412                available,
413                required,
414            } => write!(
415                f,
416                "Insufficient funds: required {} zatoshis, but only {} zatoshis were available.",
417                u64::from(*required),
418                u64::from(*available)
419            ),
420            ChangeError::DustInputs {
421                transparent,
422                sapling,
423                #[cfg(feature = "orchard")]
424                orchard,
425                #[cfg(feature = "orchard")]
426                ironwood,
427            } => {
428                #[cfg(feature = "orchard")]
429                let orchard_len = orchard.len() + ironwood.len();
430                #[cfg(not(feature = "orchard"))]
431                let orchard_len = 0;
432
433                // we can't encode the UA to its string representation because we
434                // don't have network parameters here
435                write!(
436                    f,
437                    "Insufficient funds: {} dust inputs were present, but would cost more to spend than they are worth.",
438                    transparent.len() + sapling.len() + orchard_len,
439                )
440            }
441            ChangeError::UnsupportedChangePool(pool) => {
442                write!(f, "The account cannot receive change in the {pool} pool.")
443            }
444            ChangeError::StrategyError(err) => {
445                write!(f, "{err}")
446            }
447            ChangeError::BundleError(err) => {
448                write!(
449                    f,
450                    "The proposed transaction structure violates bundle type constraints: {err}"
451                )
452            }
453        }
454    }
455}
456
457impl<E, N> std::error::Error for ChangeError<E, N>
458where
459    E: Debug + Display + std::error::Error + 'static,
460    N: Debug + Display + 'static,
461{
462    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
463        match &self {
464            ChangeError::StrategyError(e) => Some(e),
465            _ => None,
466        }
467    }
468}
469
470/// An enumeration of actions to take when a transaction would potentially create dust
471/// outputs (outputs that are likely to be without economic value due to fee rules).
472#[derive(Clone, Copy, Debug, PartialEq, Eq)]
473pub enum DustAction {
474    /// Do not allow creation of dust outputs; instead, require that additional inputs be provided.
475    Reject,
476    /// Explicitly allow the creation of dust change amounts greater than the specified value.
477    AllowDustChange,
478    /// Allow dust amounts to be added to the transaction fee.
479    AddDustToFee,
480}
481
482/// A policy describing how a [`ChangeStrategy`] should treat potentially dust-valued change
483/// outputs (outputs that are likely to be without economic value due to fee rules).
484#[derive(Clone, Copy, Debug, PartialEq, Eq)]
485pub struct DustOutputPolicy {
486    action: DustAction,
487    dust_threshold: Option<Zatoshis>,
488}
489
490impl DustOutputPolicy {
491    /// Constructs a new dust output policy.
492    ///
493    /// A dust policy created with `None` as the dust threshold will delegate determination
494    /// of the dust threshold to the change strategy that is evaluating the strategy; this
495    /// is recommended, but an explicit value (including zero) may be provided to explicitly
496    /// override the determination of the change strategy.
497    pub fn new(action: DustAction, dust_threshold: Option<Zatoshis>) -> Self {
498        Self {
499            action,
500            dust_threshold,
501        }
502    }
503
504    /// Returns the action to take in the event that a dust change amount would be produced.
505    pub fn action(&self) -> DustAction {
506        self.action
507    }
508    /// Returns a value that will be used to override the dust determination logic of the
509    /// change policy, if any.
510    pub fn dust_threshold(&self) -> Option<Zatoshis> {
511        self.dust_threshold
512    }
513}
514
515impl Default for DustOutputPolicy {
516    fn default() -> Self {
517        DustOutputPolicy::new(DustAction::Reject, None)
518    }
519}
520
521/// A policy that describes how change output should be split into multiple notes for the purpose
522/// of note management.
523///
524/// If an account contains at least [`Self::target_output_count`] notes having at least value
525/// [`Self::min_split_output_value`], this policy will recommend a single output; if the account
526/// contains fewer such notes, this policy will recommend that multiple outputs be produced in
527/// order to achieve the target.
528#[derive(Clone, Copy, Debug)]
529pub struct SplitPolicy {
530    target_output_count: NonZeroUsize,
531    min_split_output_value: Option<Zatoshis>,
532}
533
534impl SplitPolicy {
535    /// In the case that no other conditions provided by the user are available to fall back on,
536    /// a default value of [`MARGINAL_FEE`] * 100 will be used as the "minimum usable note value"
537    /// when retrieving wallet metadata.
538    ///
539    /// [`MARGINAL_FEE`]: zcash_primitives::transaction::fees::zip317::MARGINAL_FEE
540    pub(crate) const MIN_NOTE_VALUE: Zatoshis = Zatoshis::const_from_u64(500000);
541
542    /// Constructs a new [`SplitPolicy`] that splits change to ensure the given number of spendable
543    /// outputs exists within an account, each having at least the specified minimum note value.
544    pub fn with_min_output_value(
545        target_output_count: NonZeroUsize,
546        min_split_output_value: Zatoshis,
547    ) -> Self {
548        Self {
549            target_output_count,
550            min_split_output_value: Some(min_split_output_value),
551        }
552    }
553
554    /// Constructs a [`SplitPolicy`] that prescribes a single output (no splitting).
555    pub fn single_output() -> Self {
556        Self {
557            target_output_count: NonZeroUsize::MIN,
558            min_split_output_value: None,
559        }
560    }
561
562    /// Returns the number of outputs that this policy will attempt to ensure that the wallet has
563    /// available for spending.
564    pub fn target_output_count(&self) -> NonZeroUsize {
565        self.target_output_count
566    }
567
568    /// Returns the minimum value for a note resulting from splitting of change.
569    pub fn min_split_output_value(&self) -> Option<Zatoshis> {
570        self.min_split_output_value
571    }
572
573    /// Returns the number of output notes to produce from the given total change value, given the
574    /// total value and number of existing unspent notes in the account and this policy.
575    ///
576    /// If splitting change to produce [`Self::target_output_count`] would result in notes of value
577    /// less than [`Self::min_split_output_value`], then this will suggest a smaller number of
578    /// splits so that each resulting change note has sufficient value.
579    pub fn split_count(
580        &self,
581        existing_notes: Option<usize>,
582        existing_notes_total: Option<Zatoshis>,
583        total_change: Zatoshis,
584    ) -> NonZeroUsize {
585        fn to_nonzero_u64(value: usize) -> NonZeroU64 {
586            NonZeroU64::new(u64::try_from(value).expect("usize fits into u64"))
587                .expect("NonZeroU64 input derived from NonZeroUsize")
588        }
589
590        let mut split_count = NonZeroUsize::new(
591            usize::from(self.target_output_count)
592                .saturating_sub(existing_notes.unwrap_or(usize::MAX)),
593        )
594        .unwrap_or(NonZeroUsize::MIN);
595
596        let min_split_output_value = self.min_split_output_value.or_else(|| {
597            // If no minimum split output size is set, we choose the minimum split size to be a
598            // quarter of the average value of notes in the wallet after the transaction.
599            (existing_notes_total + total_change).map(|total| {
600                *total
601                    .div_with_remainder(to_nonzero_u64(
602                        usize::from(self.target_output_count).saturating_mul(4),
603                    ))
604                    .quotient()
605            })
606        });
607
608        if let Some(min_split_output_value) = min_split_output_value {
609            loop {
610                let per_output_change =
611                    total_change.div_with_remainder(to_nonzero_u64(usize::from(split_count)));
612                if *per_output_change.quotient() >= min_split_output_value {
613                    return split_count;
614                } else if let Some(new_count) = NonZeroUsize::new(usize::from(split_count) - 1) {
615                    split_count = new_count;
616                } else {
617                    // We always create at least one change output.
618                    return NonZeroUsize::MIN;
619                }
620            }
621        } else {
622            NonZeroUsize::MIN
623        }
624    }
625}
626
627/// `EphemeralBalance` describes the ephemeral input or output value for a transaction. It is used
628/// in fee computation for series of transactions that use an ephemeral transparent output in an
629/// intermediate step, such as when sending from a shielded pool to a [ZIP 320] "TEX" address.
630///
631/// [ZIP 320]: https://zips.z.cash/zip-0320
632#[derive(Clone, Copy, Debug, PartialEq, Eq)]
633pub enum EphemeralBalance {
634    Input(Zatoshis),
635    Output(Zatoshis),
636}
637
638impl EphemeralBalance {
639    pub fn is_input(&self) -> bool {
640        matches!(self, EphemeralBalance::Input(_))
641    }
642
643    pub fn is_output(&self) -> bool {
644        matches!(self, EphemeralBalance::Output(_))
645    }
646
647    pub fn ephemeral_input_amount(&self) -> Option<Zatoshis> {
648        match self {
649            EphemeralBalance::Input(v) => Some(*v),
650            EphemeralBalance::Output(_) => None,
651        }
652    }
653
654    pub fn ephemeral_output_amount(&self) -> Option<Zatoshis> {
655        match self {
656            EphemeralBalance::Input(_) => None,
657            EphemeralBalance::Output(v) => Some(*v),
658        }
659    }
660}
661
662/// A trait that defines a set of types used in wallet metadata retrieval. Ordinarily, this will
663/// correspond to a type that implements [`InputSource`], and a blanket implementation of this
664/// trait is provided for all types that implement [`InputSource`].
665///
666/// If more capabilities are required of the backend than are exposed in the [`InputSource`] trait,
667/// the implementer of this trait should define their own trait that descends from [`InputSource`]
668/// and adds the required capabilities there, and then implement that trait for their desired
669/// database backend.
670pub trait MetaSource {
671    type Error;
672    type AccountId;
673    type NoteRef;
674
675    /// Returns the outputs in which the given account can receive change.
676    fn change_receivers(&self, account: Self::AccountId) -> Result<ChangeReceivers, Self::Error>;
677}
678
679impl MetaSource for Infallible {
680    type Error = Infallible;
681    type AccountId = Infallible;
682    type NoteRef = Infallible;
683
684    fn change_receivers(&self, account: Infallible) -> Result<ChangeReceivers, Infallible> {
685        match account {}
686    }
687}
688
689impl<I: InputSource> MetaSource for I {
690    type Error = I::Error;
691    type AccountId = I::AccountId;
692    type NoteRef = I::NoteRef;
693
694    fn change_receivers(&self, account: I::AccountId) -> Result<ChangeReceivers, I::Error> {
695        self.get_change_receivers(account)
696    }
697}
698
699/// A trait that represents the ability to compute the suggested change and fees that must be paid
700/// by a transaction having a specified set of inputs and outputs.
701pub trait ChangeStrategy {
702    type FeeRule: FeeRule + Clone;
703    type Error: From<<Self::FeeRule as FeeRule>::Error>;
704
705    /// The type of metadata source that this change strategy requires in order to be able to
706    /// retrieve required wallet metadata.
707    type MetaSource: MetaSource;
708
709    /// Tye type of wallet metadata that this change strategy relies upon in order to compute
710    /// change.
711    type AccountMetaT;
712
713    /// Returns the fee rule that this change strategy will respect when performing
714    /// balance computations.
715    fn fee_rule(&self) -> &Self::FeeRule;
716
717    /// Uses the provided metadata source to obtain the wallet metadata required for change
718    /// creation determinations.
719    fn fetch_wallet_meta(
720        &self,
721        meta_source: &Self::MetaSource,
722        account: <Self::MetaSource as MetaSource>::AccountId,
723        target_height: TargetHeight,
724        exclude: &[<Self::MetaSource as MetaSource>::NoteRef],
725    ) -> Result<Self::AccountMetaT, <Self::MetaSource as MetaSource>::Error>;
726
727    /// Computes the totals of inputs, suggested change amounts, and fees given the
728    /// provided inputs and outputs being used to construct a transaction.
729    ///
730    /// The fee computed as part of this operation should take into account the prospective
731    /// change outputs recommended by this operation. If insufficient funds are available to
732    /// supply the requested outputs and required fees, implementations should return
733    /// [`ChangeError::InsufficientFunds`], with a `required` value that exceeds the total
734    /// value of the inputs provided.
735    ///
736    /// If the inputs include notes or UTXOs that are not economic to spend in the context
737    /// of this input selection, a [`ChangeError::DustInputs`] error can be returned
738    /// indicating inputs that should be removed from the selection (all of which will
739    /// have value less than or equal to the marginal fee). Every input it names must be
740    /// one of the inputs provided, and it must name at least one. The caller should order
741    /// the inputs from most to least preferred to spend within each pool, so that the most
742    /// preferred ones are less likely to be indicated to remove.
743    ///
744    /// - `ironwood`: the Ironwood bundle view (behind the `orchard` feature). A V6
745    ///   transaction carries a separate Ironwood bundle, distinct from `orchard`,
746    ///   with its own action count; pass an empty view when nothing targets the
747    ///   Ironwood pool.
748    /// - `ephemeral_balance`: if the transaction is to be constructed with either an
749    ///   ephemeral transparent input or an ephemeral transparent output this argument
750    ///   may be used to provide the value of that input or output. The value of this
751    ///   argument should be `None` in the case that there are no such items.
752    /// - `wallet_meta`: Additional wallet metadata that the change strategy may use
753    ///   in determining how to construct change outputs. This wallet metadata value
754    ///   should be computed excluding the inputs provided in the `transparent_inputs`,
755    ///   `sapling`, `orchard`, and `ironwood` arguments.
756    ///
757    /// [ZIP 320]: https://zips.z.cash/zip-0320
758    #[allow(clippy::too_many_arguments)]
759    fn compute_balance<P: consensus::Parameters, NoteRefT: Clone>(
760        &self,
761        params: &P,
762        target_height: TargetHeight,
763        anchor_height: BlockHeight,
764        zip318: &PoolMigrationParams,
765        transparent_inputs: &[impl transparent::InputView],
766        transparent_outputs: &[impl transparent::OutputView],
767        sapling: &impl sapling::BundleView<NoteRefT>,
768        #[cfg(feature = "orchard")] orchard: &impl orchard::BundleView<NoteRefT>,
769        #[cfg(feature = "orchard")] ironwood: &impl orchard::BundleView<NoteRefT>,
770        ephemeral_balance: Option<EphemeralBalance>,
771        wallet_meta: &Self::AccountMetaT,
772    ) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>>;
773}
774
775#[cfg(test)]
776pub(crate) mod tests {
777    #[cfg(feature = "orchard")]
778    use {
779        zcash_primitives::transaction::fees::zip317::MARGINAL_FEE,
780        zcash_protocol::consensus::{BlockHeight, MAIN_NETWORK},
781    };
782
783    use ::transparent::bundle::{OutPoint, TxOut};
784    use zcash_keys::keys::UnifiedSpendingKey;
785    use zcash_primitives::transaction::fees::transparent;
786    use zcash_protocol::{consensus::Network, value::Zatoshis};
787
788    use crate::data_api::ChangeReceivers;
789
790    /// The seed of the account of [`all_change_receivers`].
791    const ALL_RECEIVERS_SEED: [u8; 32] = [0; 32];
792
793    /// Returns the change receivers of an account that has a key in every pool.
794    pub(crate) fn all_change_receivers() -> ChangeReceivers {
795        ChangeReceivers::from_uivk(
796            &UnifiedSpendingKey::from_seed(
797                &Network::TestNetwork,
798                &ALL_RECEIVERS_SEED,
799                zip32::AccountId::ZERO,
800            )
801            .unwrap()
802            .to_unified_full_viewing_key()
803            .to_unified_incoming_viewing_key(),
804        )
805    }
806
807    /// The canonical crossing fee is three ZIP 317 marginal fees: the Orchard bundle's two actions
808    /// plus the single unpadded Ironwood one, which together exceed the grace allowance. Pinning it
809    /// means a change to the marginal fee or to the canonical shape surfaces here rather than
810    /// silently reclassifying transactions.
811    #[test]
812    #[cfg(feature = "orchard")]
813    fn canonical_crossing_fee_is_three_marginal_fees() {
814        let fee = super::canonical_crossing_fee(&MAIN_NETWORK, BlockHeight::from_u32(2_000_000))
815            .expect("the canonical shape is a valid input to the ZIP 317 rule");
816        assert_eq!(fee, (MARGINAL_FEE * 3u64).expect("a valid amount"));
817        assert_eq!(u64::from(fee), 15_000);
818    }
819
820    use super::sapling;
821
822    #[derive(Debug)]
823    pub(crate) struct TestTransparentInput {
824        pub outpoint: OutPoint,
825        pub coin: TxOut,
826    }
827
828    impl transparent::InputView for TestTransparentInput {
829        fn outpoint(&self) -> &OutPoint {
830            &self.outpoint
831        }
832        fn coin(&self) -> &TxOut {
833            &self.coin
834        }
835    }
836
837    pub(crate) struct TestSaplingInput {
838        pub note_id: u32,
839        pub value: Zatoshis,
840    }
841
842    impl sapling::InputView<u32> for TestSaplingInput {
843        fn note_id(&self) -> &u32 {
844            &self.note_id
845        }
846        fn value(&self) -> Zatoshis {
847            self.value
848        }
849    }
850
851    #[cfg(feature = "orchard")]
852    pub(crate) struct TestOrchardInput {
853        pub note_id: u32,
854        pub value: Zatoshis,
855    }
856
857    #[cfg(feature = "orchard")]
858    impl super::orchard::InputView<u32> for TestOrchardInput {
859        fn note_id(&self) -> &u32 {
860            &self.note_id
861        }
862        fn value(&self) -> Zatoshis {
863            self.value
864        }
865    }
866}