zcash_client_backend/fees/
fixed.rs1use core::marker::PhantomData;
4
5use zcash_primitives::transaction::fees::{fixed::FeeRule as FixedFeeRule, transparent};
6use zcash_protocol::{
7 ShieldedPool,
8 consensus::{self, BlockHeight},
9 memo::MemoBytes,
10 value::{BalanceError, Zatoshis},
11};
12
13use crate::data_api::anchor_retention::PoolMigrationParams;
14use crate::data_api::{ChangeReceivers, InputSource, wallet::TargetHeight};
15
16use super::{
17 ChangeError, ChangeStrategy, DustOutputPolicy, EphemeralBalance, SplitPolicy,
18 TransactionBalance,
19 common::{SinglePoolBalanceConfig, single_pool_output_balance},
20 sapling as sapling_fees,
21};
22
23#[cfg(feature = "transparent-inputs")]
24use super::TransparentChangePolicy;
25#[cfg(feature = "orchard")]
26use super::orchard as orchard_fees;
27#[cfg(feature = "orchard")]
28use zcash_primitives::transaction::builder::BundlePadding;
29
30pub struct SingleOutputChangeStrategy<I> {
35 fee_rule: FixedFeeRule,
36 change_memo: Option<MemoBytes>,
37 fallback_change_pool: ShieldedPool,
38 dust_output_policy: DustOutputPolicy,
39 #[cfg(feature = "transparent-inputs")]
40 transparent_change_policy: TransparentChangePolicy,
41 meta_source: PhantomData<I>,
42}
43
44impl<I> SingleOutputChangeStrategy<I> {
45 pub fn new(
51 fee_rule: FixedFeeRule,
52 change_memo: Option<MemoBytes>,
53 fallback_change_pool: ShieldedPool,
54 dust_output_policy: DustOutputPolicy,
55 ) -> Self {
56 Self {
57 fee_rule,
58 change_memo,
59 fallback_change_pool,
60 dust_output_policy,
61 #[cfg(feature = "transparent-inputs")]
62 transparent_change_policy: TransparentChangePolicy::ShieldChange,
63 meta_source: PhantomData,
64 }
65 }
66
67 #[cfg(feature = "transparent-inputs")]
74 pub fn with_transparent_change_policy(
75 mut self,
76 transparent_change_policy: TransparentChangePolicy,
77 ) -> Self {
78 self.transparent_change_policy = transparent_change_policy;
79 self
80 }
81}
82
83impl<I: InputSource> ChangeStrategy for SingleOutputChangeStrategy<I> {
84 type FeeRule = FixedFeeRule;
85 type Error = BalanceError;
86 type MetaSource = I;
87 type AccountMetaT = ChangeReceivers;
88
89 fn fee_rule(&self) -> &Self::FeeRule {
90 &self.fee_rule
91 }
92
93 fn fetch_wallet_meta(
94 &self,
95 meta_source: &Self::MetaSource,
96 account: <Self::MetaSource as InputSource>::AccountId,
97 _target_height: TargetHeight,
98 _exclude: &[<Self::MetaSource as crate::data_api::InputSource>::NoteRef],
99 ) -> Result<Self::AccountMetaT, <Self::MetaSource as crate::data_api::InputSource>::Error> {
100 meta_source.get_change_receivers(account)
101 }
102
103 fn compute_balance<P: consensus::Parameters, NoteRefT: Clone>(
104 &self,
105 params: &P,
106 target_height: TargetHeight,
107 anchor_height: BlockHeight,
108 zip318: &PoolMigrationParams,
109 transparent_inputs: &[impl transparent::InputView],
110 transparent_outputs: &[impl transparent::OutputView],
111 sapling: &impl sapling_fees::BundleView<NoteRefT>,
112 #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
113 #[cfg(feature = "orchard")] ironwood: &impl orchard_fees::BundleView<NoteRefT>,
114 ephemeral_balance: Option<EphemeralBalance>,
115 wallet_meta: &Self::AccountMetaT,
116 ) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
117 let split_policy = SplitPolicy::single_output();
118 let cfg = SinglePoolBalanceConfig::new(
119 params,
120 &self.fee_rule,
121 &self.dust_output_policy,
122 self.fee_rule.fixed_fee(),
123 &split_policy,
124 self.fallback_change_pool,
125 #[cfg(feature = "transparent-inputs")]
126 self.transparent_change_policy,
127 Zatoshis::ZERO,
128 0,
129 );
130
131 single_pool_output_balance(
132 cfg,
133 None,
134 wallet_meta,
135 target_height,
136 transparent_inputs,
137 transparent_outputs,
138 sapling,
139 #[cfg(feature = "orchard")]
140 orchard,
141 #[cfg(feature = "orchard")]
142 ironwood,
143 #[cfg(feature = "orchard")]
145 BundlePadding::DEFAULT,
146 anchor_height,
147 zip318,
148 self.change_memo.as_ref(),
149 ephemeral_balance,
150 )
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use crate::data_api::anchor_retention::{AnchorRetentionInterval, PoolMigrationParams};
157 use crate::fees::tests::all_change_receivers;
158 use ::transparent::bundle::TxOut;
159 use zcash_primitives::transaction::fees::{
160 fixed::FeeRule as FixedFeeRule, zip317::MINIMUM_FEE,
161 };
162 use zcash_protocol::consensus::BlockHeight;
163 use zcash_protocol::{
164 ShieldedPool,
165 consensus::{Network, NetworkUpgrade, Parameters},
166 value::Zatoshis,
167 };
168
169 use super::SingleOutputChangeStrategy;
170 use crate::{
171 data_api::{testing::MockWalletDb, wallet::input_selection::SaplingPayment},
172 fees::{
173 ChangeError, ChangeStrategy, ChangeValue, DustOutputPolicy,
174 tests::{TestSaplingInput, TestTransparentInput},
175 },
176 };
177
178 #[cfg(feature = "orchard")]
179 use crate::fees::orchard as orchard_fees;
180
181 #[test]
182 fn change_without_dust() {
183 let fee_rule = FixedFeeRule::non_standard(MINIMUM_FEE);
184 let change_strategy = SingleOutputChangeStrategy::<MockWalletDb>::new(
185 fee_rule,
186 None,
187 ShieldedPool::Sapling,
188 DustOutputPolicy::default(),
189 );
190
191 let result = change_strategy.compute_balance(
193 &Network::TestNetwork,
194 Network::TestNetwork
195 .activation_height(NetworkUpgrade::Nu5)
196 .unwrap()
197 .into(),
198 BlockHeight::from_u32(1),
199 &PoolMigrationParams::new(
200 zcash_protocol::zip318::BlockSpacing::UNIFORM,
201 AnchorRetentionInterval::ZIP_318,
202 ),
203 &[] as &[TestTransparentInput],
204 &[] as &[TxOut],
205 &(
206 sapling::builder::BundleType::DEFAULT,
207 &[TestSaplingInput {
208 note_id: 0,
209 value: Zatoshis::const_from_u64(60000),
210 }][..],
211 &[SaplingPayment::new(Zatoshis::const_from_u64(40000))][..],
212 ),
213 #[cfg(feature = "orchard")]
214 &orchard_fees::EmptyBundleView,
215 #[cfg(feature = "orchard")]
216 &orchard_fees::EmptyBundleView,
217 None,
218 &all_change_receivers(),
219 );
220
221 assert_matches!(
222 result,
223 Ok(balance) if
224 balance.proposed_change() == [ChangeValue::sapling(Zatoshis::const_from_u64(10000), None)] &&
225 balance.fee_required() == MINIMUM_FEE
226 );
227 }
228
229 #[test]
230 fn dust_change() {
231 let fee_rule = FixedFeeRule::non_standard(MINIMUM_FEE);
232 let change_strategy = SingleOutputChangeStrategy::<MockWalletDb>::new(
233 fee_rule,
234 None,
235 ShieldedPool::Sapling,
236 DustOutputPolicy::default(),
237 );
238
239 let result = change_strategy.compute_balance(
241 &Network::TestNetwork,
242 Network::TestNetwork
243 .activation_height(NetworkUpgrade::Nu5)
244 .unwrap()
245 .into(),
246 BlockHeight::from_u32(1),
247 &PoolMigrationParams::new(
248 zcash_protocol::zip318::BlockSpacing::UNIFORM,
249 AnchorRetentionInterval::ZIP_318,
250 ),
251 &[] as &[TestTransparentInput],
252 &[] as &[TxOut],
253 &(
254 sapling::builder::BundleType::DEFAULT,
255 &[
256 TestSaplingInput {
257 note_id: 0,
258 value: Zatoshis::const_from_u64(40000),
259 },
260 TestSaplingInput {
262 note_id: 0,
263 value: Zatoshis::const_from_u64(10100),
264 },
265 ][..],
266 &[SaplingPayment::new(Zatoshis::const_from_u64(40000))][..],
267 ),
268 #[cfg(feature = "orchard")]
269 &orchard_fees::EmptyBundleView,
270 #[cfg(feature = "orchard")]
271 &orchard_fees::EmptyBundleView,
272 None,
273 &all_change_receivers(),
274 );
275
276 assert_matches!(
277 result,
278 Err(ChangeError::InsufficientFunds { available, required })
279 if available == Zatoshis::const_from_u64(50100) && required == Zatoshis::const_from_u64(60000)
280 );
281 }
282}