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