zcash_client_backend: Allow proposer to specify fallback change pool.

In the event that the pool to which change should be sent cannot
automatically be determined based upon the inputs and outputs of a
transaction, it is up to the caller to specify where change should
be sent.
This commit is contained in:
Kris Nuttycombe 2024-02-07 18:19:02 -07:00
parent daf88a12e5
commit 4e3d99f1d0
16 changed files with 252 additions and 100 deletions

View File

@ -154,8 +154,9 @@ and this library adheres to Rust's notion of
the database identifiers for its contained notes by universally quantifying the database identifiers for its contained notes by universally quantifying
the `NoteRef` type parameter. the `NoteRef` type parameter.
- It returns a `NonEmpty<TxId>` instead of a single `TxId` value. - It returns a `NonEmpty<TxId>` instead of a single `TxId` value.
- `wallet::create_spend_to_address` now takes an additional `change_memo` - `wallet::create_spend_to_address` now takes additional `change_memo` and
argument. It also returns its result as a `NonEmpty<TxId>` instead of a `fallback_change_pool` arguments. It also returns its result as a
`NonEmpty<TxId>` instead of a
single `TxId`. single `TxId`.
- `wallet::spend` returns its result as a `NonEmpty<TxId>` instead of a - `wallet::spend` returns its result as a `NonEmpty<TxId>` instead of a
single `TxId`. single `TxId`.
@ -226,9 +227,10 @@ and this library adheres to Rust's notion of
now also provides the output pool to which change should be sent and an now also provides the output pool to which change should be sent and an
optional memo to be associated with the change output. optional memo to be associated with the change output.
- `ChangeError` has a new `BundleError` variant. - `ChangeError` has a new `BundleError` variant.
- `fixed::SingleOutputChangeStrategy::new` and - `fixed::SingleOutputChangeStrategy::new`,
`zip317::SingleOutputChangeStrategy::new` each now accept an additional `zip317::SingleOutputChangeStrategy::new`, and
`change_memo` argument. `standard::SingleOutputChangeStrategy::new` each now accept additional
`change_memo` and `fallback_change_pool` arguments.
- `zcash_client_backend::wallet`: - `zcash_client_backend::wallet`:
- The fields of `ReceivedSaplingNote` are now private. Use - The fields of `ReceivedSaplingNote` are now private. Use
`ReceivedSaplingNote::from_parts` for construction instead. Accessor methods `ReceivedSaplingNote::from_parts` for construction instead. Accessor methods

View File

@ -27,8 +27,7 @@ use crate::{
decrypt_transaction, decrypt_transaction,
fees::{self, DustOutputPolicy}, fees::{self, DustOutputPolicy},
keys::UnifiedSpendingKey, keys::UnifiedSpendingKey,
proposal::ProposalError, proposal::{self, Proposal, ProposalError},
proposal::{self, Proposal},
wallet::{Note, OvkPolicy, Recipient}, wallet::{Note, OvkPolicy, Recipient},
zip321::{self, Payment}, zip321::{self, Payment},
PoolType, ShieldedProtocol, PoolType, ShieldedProtocol,
@ -210,6 +209,7 @@ pub fn create_spend_to_address<DbT, ParamsT>(
ovk_policy: OvkPolicy, ovk_policy: OvkPolicy,
min_confirmations: NonZeroU32, min_confirmations: NonZeroU32,
change_memo: Option<MemoBytes>, change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
) -> Result< ) -> Result<
NonEmpty<TxId>, NonEmpty<TxId>,
Error< Error<
@ -240,6 +240,7 @@ where
amount, amount,
memo, memo,
change_memo, change_memo,
fallback_change_pool,
)?; )?;
create_proposed_transactions( create_proposed_transactions(
@ -423,6 +424,8 @@ where
/// * `amount`: The amount to send. /// * `amount`: The amount to send.
/// * `memo`: A memo to be included in the output to the recipient. /// * `memo`: A memo to be included in the output to the recipient.
/// * `change_memo`: A memo to be included in any change output that is created. /// * `change_memo`: A memo to be included in any change output that is created.
/// * `fallback_change_pool`: The shielded pool to which change should be sent if
/// automatic change pool determination fails.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
pub fn propose_standard_transfer_to_address<DbT, ParamsT, CommitmentTreeErrT>( pub fn propose_standard_transfer_to_address<DbT, ParamsT, CommitmentTreeErrT>(
@ -435,6 +438,7 @@ pub fn propose_standard_transfer_to_address<DbT, ParamsT, CommitmentTreeErrT>(
amount: NonNegativeAmount, amount: NonNegativeAmount,
memo: Option<MemoBytes>, memo: Option<MemoBytes>,
change_memo: Option<MemoBytes>, change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
) -> Result< ) -> Result<
Proposal<StandardFeeRule, DbT::NoteRef>, Proposal<StandardFeeRule, DbT::NoteRef>,
Error< Error<
@ -461,7 +465,11 @@ where
"It should not be possible for this to violate ZIP 321 request construction invariants.", "It should not be possible for this to violate ZIP 321 request construction invariants.",
); );
let change_strategy = fees::standard::SingleOutputChangeStrategy::new(fee_rule, change_memo); let change_strategy = fees::standard::SingleOutputChangeStrategy::new(
fee_rule,
change_memo,
fallback_change_pool,
);
let input_selector = let input_selector =
GreedyInputSelector::<DbT, _>::new(change_strategy, DustOutputPolicy::default()); GreedyInputSelector::<DbT, _>::new(change_strategy, DustOutputPolicy::default());
@ -644,8 +652,10 @@ where
.map_err(Error::DataSource)? .map_err(Error::DataSource)?
.ok_or(Error::KeyNotRecognized)?; .ok_or(Error::KeyNotRecognized)?;
let (sapling_anchor, sapling_inputs) = proposal_step.shielded_inputs().map_or_else( let (sapling_anchor, sapling_inputs) =
|| Ok((sapling::Anchor::empty_tree(), vec![])), if proposal_step.involves(PoolType::Shielded(ShieldedProtocol::Sapling)) {
proposal_step.shielded_inputs().map_or_else(
|| Ok((Some(sapling::Anchor::empty_tree()), vec![])),
|inputs| { |inputs| {
wallet_db.with_sapling_tree_mut::<_, _, Error<_, _, _, _>>(|sapling_tree| { wallet_db.with_sapling_tree_mut::<_, _, Error<_, _, _, _>>(|sapling_tree| {
let anchor = sapling_tree let anchor = sapling_tree
@ -676,13 +686,18 @@ where
}) })
.collect::<Result<Vec<_>, Error<_, _, _, _>>>()?; .collect::<Result<Vec<_>, Error<_, _, _, _>>>()?;
Ok((anchor, sapling_inputs)) Ok((Some(anchor), sapling_inputs))
}) })
}, },
)?; )?
} else {
(None, vec![])
};
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
let (orchard_anchor, orchard_inputs) = proposal_step.shielded_inputs().map_or_else( let (orchard_anchor, orchard_inputs) =
if proposal_step.involves(PoolType::Shielded(ShieldedProtocol::Orchard)) {
proposal_step.shielded_inputs().map_or_else(
|| Ok((Some(orchard::Anchor::empty_tree()), vec![])), || Ok((Some(orchard::Anchor::empty_tree()), vec![])),
|inputs| { |inputs| {
wallet_db.with_orchard_tree_mut::<_, _, Error<_, _, _, _>>(|orchard_tree| { wallet_db.with_orchard_tree_mut::<_, _, Error<_, _, _, _>>(|orchard_tree| {
@ -710,7 +725,10 @@ where
Ok((Some(anchor), orchard_inputs)) Ok((Some(anchor), orchard_inputs))
}) })
}, },
)?; )?
} else {
(None, vec![])
};
#[cfg(not(feature = "orchard"))] #[cfg(not(feature = "orchard"))]
let orchard_anchor = None; let orchard_anchor = None;
@ -720,7 +738,7 @@ where
params.clone(), params.clone(),
min_target_height, min_target_height,
BuildConfig::Standard { BuildConfig::Standard {
sapling_anchor: Some(sapling_anchor), sapling_anchor,
orchard_anchor, orchard_anchor,
}, },
); );
@ -984,7 +1002,6 @@ where
Some(memo), Some(memo),
)) ))
} }
#[cfg(zcash_unstable = "orchard")]
ShieldedProtocol::Orchard => { ShieldedProtocol::Orchard => {
#[cfg(not(feature = "orchard"))] #[cfg(not(feature = "orchard"))]
return Err(Error::UnsupportedChangeType(PoolType::Shielded( return Err(Error::UnsupportedChangeType(PoolType::Shielded(

View File

@ -455,9 +455,9 @@ where
Err(other) => return Err(other.into()), Err(other) => return Err(other.into()),
} }
#[cfg(not(zcash_unstable = "orchard"))] #[cfg(not(feature = "orchard"))]
let selectable_pools = &[ShieldedProtocol::Sapling]; let selectable_pools = &[ShieldedProtocol::Sapling];
#[cfg(zcash_unstable = "orchard")] #[cfg(feature = "orchard")]
let selectable_pools = &[ShieldedProtocol::Sapling, ShieldedProtocol::Orchard]; let selectable_pools = &[ShieldedProtocol::Sapling, ShieldedProtocol::Orchard];
shielded_inputs = wallet_db shielded_inputs = wallet_db

View File

@ -34,6 +34,7 @@ pub(crate) fn single_change_output_balance<
dust_output_policy: &DustOutputPolicy, dust_output_policy: &DustOutputPolicy,
default_dust_threshold: NonNegativeAmount, default_dust_threshold: NonNegativeAmount,
change_memo: Option<MemoBytes>, change_memo: Option<MemoBytes>,
_fallback_change_pool: ShieldedProtocol,
) -> Result<TransactionBalance, ChangeError<E, NoteRefT>> ) -> Result<TransactionBalance, ChangeError<E, NoteRefT>>
where where
E: From<F::Error> + From<BalanceError>, E: From<F::Error> + From<BalanceError>,
@ -86,7 +87,6 @@ where
// TODO: implement a less naive strategy for selecting the pool to which change will be sent. // TODO: implement a less naive strategy for selecting the pool to which change will be sent.
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
#[allow(clippy::if_same_then_else)]
let (change_pool, sapling_change, orchard_change) = let (change_pool, sapling_change, orchard_change) =
if orchard_in.is_positive() || orchard_out.is_positive() { if orchard_in.is_positive() || orchard_out.is_positive() {
// Send change to Orchard if we're spending any Orchard inputs or creating any Orchard outputs // Send change to Orchard if we're spending any Orchard inputs or creating any Orchard outputs
@ -96,8 +96,12 @@ where
// Sapling outputs, so that we avoid pool-crossing. // Sapling outputs, so that we avoid pool-crossing.
(ShieldedProtocol::Sapling, 1, 0) (ShieldedProtocol::Sapling, 1, 0)
} else { } else {
// For all other transactions, send change to Orchard. // This is a fully-transparent transaction, so the caller gets to decide
(ShieldedProtocol::Orchard, 0, 1) // where to shield change.
match _fallback_change_pool {
ShieldedProtocol::Orchard => (_fallback_change_pool, 0, 1),
ShieldedProtocol::Sapling => (_fallback_change_pool, 1, 0),
}
}; };
#[cfg(not(feature = "orchard"))] #[cfg(not(feature = "orchard"))]
let (change_pool, sapling_change) = (ShieldedProtocol::Sapling, 1); let (change_pool, sapling_change) = (ShieldedProtocol::Sapling, 1);

View File

@ -9,6 +9,8 @@ use zcash_primitives::{
}, },
}; };
use crate::ShieldedProtocol;
use super::{ use super::{
common::single_change_output_balance, sapling as sapling_fees, ChangeError, ChangeStrategy, common::single_change_output_balance, sapling as sapling_fees, ChangeError, ChangeStrategy,
DustOutputPolicy, TransactionBalance, DustOutputPolicy, TransactionBalance,
@ -22,15 +24,21 @@ use super::orchard as orchard_fees;
pub struct SingleOutputChangeStrategy { pub struct SingleOutputChangeStrategy {
fee_rule: FixedFeeRule, fee_rule: FixedFeeRule,
change_memo: Option<MemoBytes>, change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
} }
impl SingleOutputChangeStrategy { impl SingleOutputChangeStrategy {
/// Constructs a new [`SingleOutputChangeStrategy`] with the specified fee rule /// Constructs a new [`SingleOutputChangeStrategy`] with the specified fee rule
/// and change memo. /// and change memo.
pub fn new(fee_rule: FixedFeeRule, change_memo: Option<MemoBytes>) -> Self { pub fn new(
fee_rule: FixedFeeRule,
change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
) -> Self {
Self { Self {
fee_rule, fee_rule,
change_memo, change_memo,
fallback_change_pool,
} }
} }
} }
@ -65,6 +73,7 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
dust_output_policy, dust_output_policy,
self.fee_rule().fixed_fee(), self.fee_rule().fixed_fee(),
self.change_memo.clone(), self.change_memo.clone(),
self.fallback_change_pool,
) )
} }
} }
@ -89,13 +98,15 @@ mod tests {
tests::{TestSaplingInput, TestTransparentInput}, tests::{TestSaplingInput, TestTransparentInput},
ChangeError, ChangeStrategy, ChangeValue, DustOutputPolicy, ChangeError, ChangeStrategy, ChangeValue, DustOutputPolicy,
}, },
ShieldedProtocol,
}; };
#[test] #[test]
fn change_without_dust() { fn change_without_dust() {
#[allow(deprecated)] #[allow(deprecated)]
let fee_rule = FixedFeeRule::standard(); let fee_rule = FixedFeeRule::standard();
let change_strategy = SingleOutputChangeStrategy::new(fee_rule, None); let change_strategy =
SingleOutputChangeStrategy::new(fee_rule, None, ShieldedProtocol::Sapling);
// spend a single Sapling note that is sufficient to pay the fee // spend a single Sapling note that is sufficient to pay the fee
let result = change_strategy.compute_balance( let result = change_strategy.compute_balance(
@ -136,7 +147,8 @@ mod tests {
fn dust_change() { fn dust_change() {
#[allow(deprecated)] #[allow(deprecated)]
let fee_rule = FixedFeeRule::standard(); let fee_rule = FixedFeeRule::standard();
let change_strategy = SingleOutputChangeStrategy::new(fee_rule, None); let change_strategy =
SingleOutputChangeStrategy::new(fee_rule, None, ShieldedProtocol::Sapling);
// spend a single Sapling note that is sufficient to pay the fee // spend a single Sapling note that is sufficient to pay the fee
let result = change_strategy.compute_balance( let result = change_strategy.compute_balance(

View File

@ -14,6 +14,8 @@ use zcash_primitives::{
}, },
}; };
use crate::ShieldedProtocol;
use super::{ use super::{
fixed, sapling as sapling_fees, zip317, ChangeError, ChangeStrategy, DustOutputPolicy, fixed, sapling as sapling_fees, zip317, ChangeError, ChangeStrategy, DustOutputPolicy,
TransactionBalance, TransactionBalance,
@ -27,15 +29,21 @@ use super::orchard as orchard_fees;
pub struct SingleOutputChangeStrategy { pub struct SingleOutputChangeStrategy {
fee_rule: StandardFeeRule, fee_rule: StandardFeeRule,
change_memo: Option<MemoBytes>, change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
} }
impl SingleOutputChangeStrategy { impl SingleOutputChangeStrategy {
/// Constructs a new [`SingleOutputChangeStrategy`] with the specified ZIP 317 /// Constructs a new [`SingleOutputChangeStrategy`] with the specified ZIP 317
/// fee parameters. /// fee parameters.
pub fn new(fee_rule: StandardFeeRule, change_memo: Option<MemoBytes>) -> Self { pub fn new(
fee_rule: StandardFeeRule,
change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
) -> Self {
Self { Self {
fee_rule, fee_rule,
change_memo, change_memo,
fallback_change_pool,
} }
} }
} }
@ -63,6 +71,7 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
StandardFeeRule::PreZip313 => fixed::SingleOutputChangeStrategy::new( StandardFeeRule::PreZip313 => fixed::SingleOutputChangeStrategy::new(
FixedFeeRule::non_standard(NonNegativeAmount::const_from_u64(10000)), FixedFeeRule::non_standard(NonNegativeAmount::const_from_u64(10000)),
self.change_memo.clone(), self.change_memo.clone(),
self.fallback_change_pool,
) )
.compute_balance( .compute_balance(
params, params,
@ -78,6 +87,7 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
StandardFeeRule::Zip313 => fixed::SingleOutputChangeStrategy::new( StandardFeeRule::Zip313 => fixed::SingleOutputChangeStrategy::new(
FixedFeeRule::non_standard(NonNegativeAmount::const_from_u64(1000)), FixedFeeRule::non_standard(NonNegativeAmount::const_from_u64(1000)),
self.change_memo.clone(), self.change_memo.clone(),
self.fallback_change_pool,
) )
.compute_balance( .compute_balance(
params, params,
@ -93,6 +103,7 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
StandardFeeRule::Zip317 => zip317::SingleOutputChangeStrategy::new( StandardFeeRule::Zip317 => zip317::SingleOutputChangeStrategy::new(
Zip317FeeRule::standard(), Zip317FeeRule::standard(),
self.change_memo.clone(), self.change_memo.clone(),
self.fallback_change_pool,
) )
.compute_balance( .compute_balance(
params, params,

View File

@ -13,6 +13,8 @@ use zcash_primitives::{
}, },
}; };
use crate::ShieldedProtocol;
use super::{ use super::{
common::single_change_output_balance, sapling as sapling_fees, ChangeError, ChangeStrategy, common::single_change_output_balance, sapling as sapling_fees, ChangeError, ChangeStrategy,
DustOutputPolicy, TransactionBalance, DustOutputPolicy, TransactionBalance,
@ -26,15 +28,21 @@ use super::orchard as orchard_fees;
pub struct SingleOutputChangeStrategy { pub struct SingleOutputChangeStrategy {
fee_rule: Zip317FeeRule, fee_rule: Zip317FeeRule,
change_memo: Option<MemoBytes>, change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
} }
impl SingleOutputChangeStrategy { impl SingleOutputChangeStrategy {
/// Constructs a new [`SingleOutputChangeStrategy`] with the specified ZIP 317 /// Constructs a new [`SingleOutputChangeStrategy`] with the specified ZIP 317
/// fee parameters and change memo. /// fee parameters and change memo.
pub fn new(fee_rule: Zip317FeeRule, change_memo: Option<MemoBytes>) -> Self { pub fn new(
fee_rule: Zip317FeeRule,
change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
) -> Self {
Self { Self {
fee_rule, fee_rule,
change_memo, change_memo,
fallback_change_pool,
} }
} }
} }
@ -145,6 +153,7 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
dust_output_policy, dust_output_policy,
self.fee_rule.marginal_fee(), self.fee_rule.marginal_fee(),
self.change_memo.clone(), self.change_memo.clone(),
self.fallback_change_pool,
) )
} }
} }
@ -170,11 +179,16 @@ mod tests {
tests::{TestSaplingInput, TestTransparentInput}, tests::{TestSaplingInput, TestTransparentInput},
ChangeError, ChangeStrategy, ChangeValue, DustOutputPolicy, ChangeError, ChangeStrategy, ChangeValue, DustOutputPolicy,
}, },
ShieldedProtocol,
}; };
#[test] #[test]
fn change_without_dust() { fn change_without_dust() {
let change_strategy = SingleOutputChangeStrategy::new(Zip317FeeRule::standard(), None); let change_strategy = SingleOutputChangeStrategy::new(
Zip317FeeRule::standard(),
None,
ShieldedProtocol::Sapling,
);
// spend a single Sapling note that is sufficient to pay the fee // spend a single Sapling note that is sufficient to pay the fee
let result = change_strategy.compute_balance( let result = change_strategy.compute_balance(
@ -213,7 +227,11 @@ mod tests {
#[test] #[test]
fn change_with_transparent_payments() { fn change_with_transparent_payments() {
let change_strategy = SingleOutputChangeStrategy::new(Zip317FeeRule::standard(), None); let change_strategy = SingleOutputChangeStrategy::new(
Zip317FeeRule::standard(),
None,
ShieldedProtocol::Sapling,
);
// spend a single Sapling note that is sufficient to pay the fee // spend a single Sapling note that is sufficient to pay the fee
let result = change_strategy.compute_balance( let result = change_strategy.compute_balance(
@ -252,7 +270,11 @@ mod tests {
#[test] #[test]
fn change_with_allowable_dust() { fn change_with_allowable_dust() {
let change_strategy = SingleOutputChangeStrategy::new(Zip317FeeRule::standard(), None); let change_strategy = SingleOutputChangeStrategy::new(
Zip317FeeRule::standard(),
None,
ShieldedProtocol::Sapling,
);
// spend a single Sapling note that is sufficient to pay the fee // spend a single Sapling note that is sufficient to pay the fee
let result = change_strategy.compute_balance( let result = change_strategy.compute_balance(
@ -296,7 +318,11 @@ mod tests {
#[test] #[test]
fn change_with_disallowed_dust() { fn change_with_disallowed_dust() {
let change_strategy = SingleOutputChangeStrategy::new(Zip317FeeRule::standard(), None); let change_strategy = SingleOutputChangeStrategy::new(
Zip317FeeRule::standard(),
None,
ShieldedProtocol::Sapling,
);
// spend a single Sapling note that is sufficient to pay the fee // spend a single Sapling note that is sufficient to pay the fee
let result = change_strategy.compute_balance( let result = change_strategy.compute_balance(

View File

@ -97,7 +97,6 @@ pub enum ShieldedProtocol {
/// The Sapling protocol /// The Sapling protocol
Sapling, Sapling,
/// The Orchard protocol /// The Orchard protocol
#[cfg(zcash_unstable = "orchard")]
Orchard, Orchard,
} }
@ -118,7 +117,6 @@ impl PoolType {
Address::Unified(ua) => match self { Address::Unified(ua) => match self {
PoolType::Transparent => ua.transparent().is_some(), PoolType::Transparent => ua.transparent().is_some(),
PoolType::Shielded(ShieldedProtocol::Sapling) => ua.sapling().is_some(), PoolType::Shielded(ShieldedProtocol::Sapling) => ua.sapling().is_some(),
#[cfg(zcash_unstable = "orchard")]
PoolType::Shielded(ShieldedProtocol::Orchard) => { PoolType::Shielded(ShieldedProtocol::Orchard) => {
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
return ua.orchard().is_some(); return ua.orchard().is_some();
@ -136,7 +134,6 @@ impl fmt::Display for PoolType {
match self { match self {
PoolType::Transparent => f.write_str("Transparent"), PoolType::Transparent => f.write_str("Transparent"),
PoolType::Shielded(ShieldedProtocol::Sapling) => f.write_str("Sapling"), PoolType::Shielded(ShieldedProtocol::Sapling) => f.write_str("Sapling"),
#[cfg(zcash_unstable = "orchard")]
PoolType::Shielded(ShieldedProtocol::Orchard) => f.write_str("Orchard"), PoolType::Shielded(ShieldedProtocol::Orchard) => f.write_str("Orchard"),
} }
} }

View File

@ -490,6 +490,59 @@ impl<NoteRef> Step<NoteRef> {
pub fn is_shielding(&self) -> bool { pub fn is_shielding(&self) -> bool {
self.is_shielding self.is_shielding
} }
/// Returns whether or not this proposal requires interaction with the specified pool
pub fn involves(&self, pool_type: PoolType) -> bool {
match pool_type {
PoolType::Transparent => {
self.is_shielding
|| !self.transparent_inputs.is_empty()
|| self
.payment_pools()
.values()
.any(|pool| matches!(pool, PoolType::Transparent))
}
PoolType::Shielded(ShieldedProtocol::Sapling) => {
let sapling_in = self.shielded_inputs.iter().any(|s_in| {
s_in.notes()
.iter()
.any(|note| matches!(note.note(), Note::Sapling(_)))
});
let sapling_out = self
.payment_pools()
.values()
.any(|pool| matches!(pool, PoolType::Shielded(ShieldedProtocol::Sapling)));
let sapling_change = self
.balance
.proposed_change()
.iter()
.any(|c| c.output_pool() == ShieldedProtocol::Sapling);
sapling_in || sapling_out || sapling_change
}
#[cfg(not(feature = "orchard"))]
PoolType::Shielded(ShieldedProtocol::Orchard) => false,
#[cfg(feature = "orchard")]
PoolType::Shielded(ShieldedProtocol::Orchard) => {
let orchard_in = self.shielded_inputs.iter().any(|s_in| {
s_in.notes()
.iter()
.any(|note| matches!(note.note(), Note::Orchard(_)))
});
let orchard_out = self
.payment_pools()
.values()
.any(|pool| matches!(pool, PoolType::Shielded(ShieldedProtocol::Orchard)));
let orchard_change = self
.balance
.proposed_change()
.iter()
.any(|c| c.output_pool() == ShieldedProtocol::Orchard);
orchard_in || orchard_out || orchard_change
}
}
}
} }
impl<NoteRef> Debug for Step<NoteRef> { impl<NoteRef> Debug for Step<NoteRef> {
@ -501,6 +554,7 @@ impl<NoteRef> Debug for Step<NoteRef> {
"shielded_inputs", "shielded_inputs",
&self.shielded_inputs().map(|i| i.notes.len()), &self.shielded_inputs().map(|i| i.notes.len()),
) )
.field("prior_step_inputs", &self.prior_step_inputs)
.field( .field(
"anchor_height", "anchor_height",
&self.shielded_inputs().map(|i| i.anchor_height), &self.shielded_inputs().map(|i| i.anchor_height),

View File

@ -319,7 +319,6 @@ fn pool_type<T>(pool_id: i32) -> Result<PoolType, ProposalDecodingError<T>> {
match proposal::ValuePool::try_from(pool_id) { match proposal::ValuePool::try_from(pool_id) {
Ok(proposal::ValuePool::Transparent) => Ok(PoolType::Transparent), Ok(proposal::ValuePool::Transparent) => Ok(PoolType::Transparent),
Ok(proposal::ValuePool::Sapling) => Ok(PoolType::Shielded(ShieldedProtocol::Sapling)), Ok(proposal::ValuePool::Sapling) => Ok(PoolType::Shielded(ShieldedProtocol::Sapling)),
#[cfg(zcash_unstable = "orchard")]
Ok(proposal::ValuePool::Orchard) => Ok(PoolType::Shielded(ShieldedProtocol::Orchard)), Ok(proposal::ValuePool::Orchard) => Ok(PoolType::Shielded(ShieldedProtocol::Orchard)),
_ => Err(ProposalDecodingError::ValuePoolNotSupported(pool_id)), _ => Err(ProposalDecodingError::ValuePoolNotSupported(pool_id)),
} }
@ -354,7 +353,6 @@ impl From<ShieldedProtocol> for proposal::ValuePool {
fn from(value: ShieldedProtocol) -> Self { fn from(value: ShieldedProtocol) -> Self {
match value { match value {
ShieldedProtocol::Sapling => proposal::ValuePool::Sapling, ShieldedProtocol::Sapling => proposal::ValuePool::Sapling,
#[cfg(zcash_unstable = "orchard")]
ShieldedProtocol::Orchard => proposal::ValuePool::Orchard, ShieldedProtocol::Orchard => proposal::ValuePool::Orchard,
} }
} }

View File

@ -340,6 +340,7 @@ mod tests {
scanning::ScanError, scanning::ScanError,
wallet::OvkPolicy, wallet::OvkPolicy,
zip321::{Payment, TransactionRequest}, zip321::{Payment, TransactionRequest},
ShieldedProtocol,
}; };
use crate::{ use crate::{
@ -529,7 +530,7 @@ mod tests {
}]) }])
.unwrap(); .unwrap();
let input_selector = GreedyInputSelector::new( let input_selector = GreedyInputSelector::new(
SingleOutputChangeStrategy::new(FeeRule::standard(), None), SingleOutputChangeStrategy::new(FeeRule::standard(), None, ShieldedProtocol::Sapling),
DustOutputPolicy::default(), DustOutputPolicy::default(),
); );
assert_matches!( assert_matches!(

View File

@ -22,7 +22,6 @@ use sapling::{
zip32::DiversifiableFullViewingKey, zip32::DiversifiableFullViewingKey,
Note, Nullifier, PaymentAddress, Note, Nullifier, PaymentAddress,
}; };
use zcash_client_backend::fees::{standard, DustOutputPolicy};
#[allow(deprecated)] #[allow(deprecated)]
use zcash_client_backend::{ use zcash_client_backend::{
address::Address, address::Address,
@ -45,6 +44,10 @@ use zcash_client_backend::{
wallet::OvkPolicy, wallet::OvkPolicy,
zip321, zip321,
}; };
use zcash_client_backend::{
fees::{standard, DustOutputPolicy},
ShieldedProtocol,
};
use zcash_note_encryption::Domain; use zcash_note_encryption::Domain;
use zcash_primitives::{ use zcash_primitives::{
block::BlockHash, block::BlockHash,
@ -465,6 +468,7 @@ impl<Cache> TestState<Cache> {
ovk_policy, ovk_policy,
min_confirmations, min_confirmations,
change_memo, change_memo,
ShieldedProtocol::Sapling,
) )
} }
@ -567,6 +571,7 @@ impl<Cache> TestState<Cache> {
amount, amount,
memo, memo,
change_memo, change_memo,
ShieldedProtocol::Sapling,
); );
if let Ok(proposal) = &result { if let Ok(proposal) = &result {
@ -1058,7 +1063,8 @@ pub(crate) fn input_selector(
standard::SingleOutputChangeStrategy, standard::SingleOutputChangeStrategy,
> { > {
let change_memo = change_memo.map(|m| MemoBytes::from(m.parse::<Memo>().unwrap())); let change_memo = change_memo.map(|m| MemoBytes::from(m.parse::<Memo>().unwrap()));
let change_strategy = standard::SingleOutputChangeStrategy::new(fee_rule, change_memo); let change_strategy =
standard::SingleOutputChangeStrategy::new(fee_rule, change_memo, ShieldedProtocol::Sapling);
GreedyInputSelector::new(change_strategy, DustOutputPolicy::default()) GreedyInputSelector::new(change_strategy, DustOutputPolicy::default())
} }

View File

@ -137,7 +137,6 @@ pub(crate) fn pool_code(pool_type: PoolType) -> i64 {
match pool_type { match pool_type {
PoolType::Transparent => 0i64, PoolType::Transparent => 0i64,
PoolType::Shielded(ShieldedProtocol::Sapling) => 2i64, PoolType::Shielded(ShieldedProtocol::Sapling) => 2i64,
#[cfg(zcash_unstable = "orchard")]
PoolType::Shielded(ShieldedProtocol::Orchard) => 3i64, PoolType::Shielded(ShieldedProtocol::Orchard) => 3i64,
} }
} }
@ -818,7 +817,6 @@ pub(crate) fn get_received_memo(
) )
.optional()? .optional()?
.flatten(), .flatten(),
#[cfg(zcash_unstable = "orchard")]
_ => { _ => {
return Err(SqliteClientError::UnsupportedPoolType(PoolType::Shielded( return Err(SqliteClientError::UnsupportedPoolType(PoolType::Shielded(
note_id.protocol(), note_id.protocol(),
@ -2232,6 +2230,8 @@ mod tests {
#[test] #[test]
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
fn transparent_balance_across_shielding() { fn transparent_balance_across_shielding() {
use zcash_client_backend::ShieldedProtocol;
let mut st = TestBuilder::new() let mut st = TestBuilder::new()
.with_block_cache() .with_block_cache()
.with_test_account(AccountBirthday::from_sapling_activation) .with_test_account(AccountBirthday::from_sapling_activation)
@ -2316,6 +2316,7 @@ mod tests {
fixed::SingleOutputChangeStrategy::new( fixed::SingleOutputChangeStrategy::new(
FixedFeeRule::non_standard(NonNegativeAmount::ZERO), FixedFeeRule::non_standard(NonNegativeAmount::ZERO),
None, None,
ShieldedProtocol::Sapling,
), ),
DustOutputPolicy::default(), DustOutputPolicy::default(),
); );

View File

@ -579,8 +579,11 @@ pub(crate) mod tests {
let fee_rule = StandardFeeRule::PreZip313; let fee_rule = StandardFeeRule::PreZip313;
let change_memo = "Test change memo".parse::<Memo>().unwrap(); let change_memo = "Test change memo".parse::<Memo>().unwrap();
let change_strategy = let change_strategy = standard::SingleOutputChangeStrategy::new(
standard::SingleOutputChangeStrategy::new(fee_rule, Some(change_memo.clone().into())); fee_rule,
Some(change_memo.clone().into()),
ShieldedProtocol::Sapling,
);
let input_selector = let input_selector =
&GreedyInputSelector::new(change_strategy, DustOutputPolicy::default()); &GreedyInputSelector::new(change_strategy, DustOutputPolicy::default());
@ -731,7 +734,7 @@ pub(crate) mod tests {
let fee_rule = StandardFeeRule::Zip317; let fee_rule = StandardFeeRule::Zip317;
let input_selector = GreedyInputSelector::new( let input_selector = GreedyInputSelector::new(
standard::SingleOutputChangeStrategy::new(fee_rule, None), standard::SingleOutputChangeStrategy::new(fee_rule, None, ShieldedProtocol::Sapling),
DustOutputPolicy::default(), DustOutputPolicy::default(),
); );
let proposal0 = st let proposal0 = st
@ -1455,7 +1458,7 @@ pub(crate) mod tests {
#[allow(deprecated)] #[allow(deprecated)]
let fee_rule = FixedFeeRule::standard(); let fee_rule = FixedFeeRule::standard();
let input_selector = GreedyInputSelector::new( let input_selector = GreedyInputSelector::new(
fixed::SingleOutputChangeStrategy::new(fee_rule, None), fixed::SingleOutputChangeStrategy::new(fee_rule, None, ShieldedProtocol::Sapling),
DustOutputPolicy::default(), DustOutputPolicy::default(),
); );
@ -1657,7 +1660,7 @@ pub(crate) mod tests {
let fee_rule = StandardFeeRule::PreZip313; let fee_rule = StandardFeeRule::PreZip313;
let input_selector = GreedyInputSelector::new( let input_selector = GreedyInputSelector::new(
standard::SingleOutputChangeStrategy::new(fee_rule, None), standard::SingleOutputChangeStrategy::new(fee_rule, None, ShieldedProtocol::Sapling),
DustOutputPolicy::default(), DustOutputPolicy::default(),
); );

View File

@ -14,7 +14,7 @@ The entries below are relative to the `zcash_client_backend` crate as of
- `address` - `address`
- `encoding` - `encoding`
- `keys` - `keys`
- `zcash_keys::address::UnifiedAddress::unknown`: - `zcash_keys::address::UnifiedAddress::{unknown, has_orchard, has_sapling, has_transparent}`:
- `zcash_keys::keys`: - `zcash_keys::keys`:
- `AddressGenerationError` - `AddressGenerationError`
- `UnifiedAddressRequest` - `UnifiedAddressRequest`

View File

@ -108,17 +108,37 @@ impl UnifiedAddress {
} }
} }
/// Returns whether this address has an Orchard receiver.
///
/// This method is available irrespective of whether the `orchard` feature flag is enabled.
pub fn has_orchard(&self) -> bool {
#[cfg(not(feature = "orchard"))]
return false;
#[cfg(feature = "orchard")]
return self.orchard.is_some();
}
/// Returns the Orchard receiver within this Unified Address, if any. /// Returns the Orchard receiver within this Unified Address, if any.
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
pub fn orchard(&self) -> Option<&orchard::Address> { pub fn orchard(&self) -> Option<&orchard::Address> {
self.orchard.as_ref() self.orchard.as_ref()
} }
/// Returns whether this address has a Sapling receiver.
pub fn has_sapling(&self) -> bool {
self.sapling.is_some()
}
/// Returns the Sapling receiver within this Unified Address, if any. /// Returns the Sapling receiver within this Unified Address, if any.
pub fn sapling(&self) -> Option<&PaymentAddress> { pub fn sapling(&self) -> Option<&PaymentAddress> {
self.sapling.as_ref() self.sapling.as_ref()
} }
/// Returns whether this address has a Transparent receiver.
pub fn has_transparent(&self) -> bool {
self.transparent.is_some()
}
/// Returns the transparent receiver within this Unified Address, if any. /// Returns the transparent receiver within this Unified Address, if any.
pub fn transparent(&self) -> Option<&TransparentAddress> { pub fn transparent(&self) -> Option<&TransparentAddress> {
self.transparent.as_ref() self.transparent.as_ref()