use core::marker::PhantomData;
use zcash_primitives::transaction::fees::{fixed::FeeRule as FixedFeeRule, transparent};
use zcash_protocol::{
ShieldedPool,
consensus::{self, BlockHeight},
memo::MemoBytes,
value::{BalanceError, Zatoshis},
};
use crate::data_api::anchor_retention::PoolMigrationParams;
use crate::data_api::{InputSource, wallet::TargetHeight};
use super::{
ChangeError, ChangeStrategy, DustOutputPolicy, EphemeralBalance, SplitPolicy,
TransactionBalance,
common::{SinglePoolBalanceConfig, single_pool_output_balance},
sapling as sapling_fees,
};
#[cfg(feature = "transparent-inputs")]
use super::TransparentChangePolicy;
#[cfg(feature = "orchard")]
use super::orchard as orchard_fees;
#[cfg(feature = "orchard")]
use zcash_primitives::transaction::builder::BundlePadding;
pub struct SingleOutputChangeStrategy<I> {
fee_rule: FixedFeeRule,
change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedPool,
dust_output_policy: DustOutputPolicy,
#[cfg(feature = "transparent-inputs")]
transparent_change_policy: TransparentChangePolicy,
meta_source: PhantomData<I>,
}
impl<I> SingleOutputChangeStrategy<I> {
pub fn new(
fee_rule: FixedFeeRule,
change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedPool,
dust_output_policy: DustOutputPolicy,
) -> Self {
Self {
fee_rule,
change_memo,
fallback_change_pool,
dust_output_policy,
#[cfg(feature = "transparent-inputs")]
transparent_change_policy: TransparentChangePolicy::ShieldChange,
meta_source: PhantomData,
}
}
#[cfg(feature = "transparent-inputs")]
pub fn with_transparent_change_policy(
mut self,
transparent_change_policy: TransparentChangePolicy,
) -> Self {
self.transparent_change_policy = transparent_change_policy;
self
}
}
impl<I: InputSource> ChangeStrategy for SingleOutputChangeStrategy<I> {
type FeeRule = FixedFeeRule;
type Error = BalanceError;
type MetaSource = I;
type AccountMetaT = ();
fn fee_rule(&self) -> &Self::FeeRule {
&self.fee_rule
}
fn fetch_wallet_meta(
&self,
_meta_source: &Self::MetaSource,
_account: <Self::MetaSource as InputSource>::AccountId,
_target_height: TargetHeight,
_exclude: &[<Self::MetaSource as crate::data_api::InputSource>::NoteRef],
) -> Result<Self::AccountMetaT, <Self::MetaSource as crate::data_api::InputSource>::Error> {
Ok(())
}
fn compute_balance<P: consensus::Parameters, NoteRefT: Clone>(
&self,
params: &P,
target_height: TargetHeight,
anchor_height: BlockHeight,
zip318: &PoolMigrationParams,
transparent_inputs: &[impl transparent::InputView],
transparent_outputs: &[impl transparent::OutputView],
sapling: &impl sapling_fees::BundleView<NoteRefT>,
#[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
#[cfg(feature = "orchard")] ironwood: &impl orchard_fees::BundleView<NoteRefT>,
ephemeral_balance: Option<EphemeralBalance>,
_wallet_meta: &Self::AccountMetaT,
) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
let split_policy = SplitPolicy::single_output();
let cfg = SinglePoolBalanceConfig::new(
params,
&self.fee_rule,
&self.dust_output_policy,
self.fee_rule.fixed_fee(),
&split_policy,
self.fallback_change_pool,
#[cfg(feature = "transparent-inputs")]
self.transparent_change_policy,
Zatoshis::ZERO,
0,
);
single_pool_output_balance(
cfg,
None,
target_height,
transparent_inputs,
transparent_outputs,
sapling,
#[cfg(feature = "orchard")]
orchard,
#[cfg(feature = "orchard")]
ironwood,
#[cfg(feature = "orchard")]
BundlePadding::DEFAULT,
anchor_height,
zip318,
self.change_memo.as_ref(),
ephemeral_balance,
)
}
}
#[cfg(test)]
mod tests {
use crate::data_api::anchor_retention::{AnchorRetentionInterval, PoolMigrationParams};
use ::transparent::bundle::TxOut;
use zcash_primitives::transaction::fees::{
fixed::FeeRule as FixedFeeRule, zip317::MINIMUM_FEE,
};
use zcash_protocol::consensus::BlockHeight;
use zcash_protocol::{
ShieldedPool,
consensus::{Network, NetworkUpgrade, Parameters},
value::Zatoshis,
};
use super::SingleOutputChangeStrategy;
use crate::{
data_api::{testing::MockWalletDb, wallet::input_selection::SaplingPayment},
fees::{
ChangeError, ChangeStrategy, ChangeValue, DustOutputPolicy,
tests::{TestSaplingInput, TestTransparentInput},
},
};
#[cfg(feature = "orchard")]
use crate::fees::orchard as orchard_fees;
#[test]
fn change_without_dust() {
let fee_rule = FixedFeeRule::non_standard(MINIMUM_FEE);
let change_strategy = SingleOutputChangeStrategy::<MockWalletDb>::new(
fee_rule,
None,
ShieldedPool::Sapling,
DustOutputPolicy::default(),
);
let result = change_strategy.compute_balance(
&Network::TestNetwork,
Network::TestNetwork
.activation_height(NetworkUpgrade::Nu5)
.unwrap()
.into(),
BlockHeight::from_u32(1),
&PoolMigrationParams::new(AnchorRetentionInterval::ZIP_318),
&[] as &[TestTransparentInput],
&[] as &[TxOut],
&(
sapling::builder::BundleType::DEFAULT,
&[TestSaplingInput {
note_id: 0,
value: Zatoshis::const_from_u64(60000),
}][..],
&[SaplingPayment::new(Zatoshis::const_from_u64(40000))][..],
),
#[cfg(feature = "orchard")]
&orchard_fees::EmptyBundleView,
#[cfg(feature = "orchard")]
&orchard_fees::EmptyBundleView,
None,
&(),
);
assert_matches!(
result,
Ok(balance) if
balance.proposed_change() == [ChangeValue::sapling(Zatoshis::const_from_u64(10000), None)] &&
balance.fee_required() == MINIMUM_FEE
);
}
#[test]
fn dust_change() {
let fee_rule = FixedFeeRule::non_standard(MINIMUM_FEE);
let change_strategy = SingleOutputChangeStrategy::<MockWalletDb>::new(
fee_rule,
None,
ShieldedPool::Sapling,
DustOutputPolicy::default(),
);
let result = change_strategy.compute_balance(
&Network::TestNetwork,
Network::TestNetwork
.activation_height(NetworkUpgrade::Nu5)
.unwrap()
.into(),
BlockHeight::from_u32(1),
&PoolMigrationParams::new(AnchorRetentionInterval::ZIP_318),
&[] as &[TestTransparentInput],
&[] as &[TxOut],
&(
sapling::builder::BundleType::DEFAULT,
&[
TestSaplingInput {
note_id: 0,
value: Zatoshis::const_from_u64(40000),
},
TestSaplingInput {
note_id: 0,
value: Zatoshis::const_from_u64(10100),
},
][..],
&[SaplingPayment::new(Zatoshis::const_from_u64(40000))][..],
),
#[cfg(feature = "orchard")]
&orchard_fees::EmptyBundleView,
#[cfg(feature = "orchard")]
&orchard_fees::EmptyBundleView,
None,
&(),
);
assert_matches!(
result,
Err(ChangeError::InsufficientFunds { available, required })
if available == Zatoshis::const_from_u64(50100) && required == Zatoshis::const_from_u64(60000)
);
}
}