zcash_client_backend: Make it possible for change strategies to use wallet metadata.
In the process this modifies input selection to take the change strategy as an explicit argument, rather than being wrapped as part of the input selector.
This commit is contained in:
parent
e21fce4411
commit
6d5a6ac7ac
|
@ -12,10 +12,55 @@ and this library adheres to Rust's notion of
|
|||
- `Progress`
|
||||
- `WalletSummary::progress`
|
||||
- `WalletMeta`
|
||||
- `impl Default for wallet::input_selection::GreedyInputSelector`
|
||||
|
||||
### Changed
|
||||
- `zcash_client_backend::data_api`:
|
||||
- `InputSource` has an added method `get_wallet_metadata`
|
||||
- `error::Error` has additional variant `Error::Change`. This necessitates
|
||||
the addition of two type parameters to the `Error` type,
|
||||
`ChangeErrT` and `NoteRefT`.
|
||||
- The following methods each now take an additional `change_strategy`
|
||||
argument, along with an associated `ChangeT` type parameter:
|
||||
- `zcash_client_backend::data_api::wallet::spend`
|
||||
- `zcash_client_backend::data_api::wallet::propose_transfer`
|
||||
- `zcash_client_backend::data_api::wallet::propose_shielding`. This method
|
||||
also now takes an additional `to_account` argument.
|
||||
- `zcash_client_backend::data_api::wallet::shield_transparent_funds`. This
|
||||
method also now takes an additional `to_account` argument.
|
||||
- `wallet::input_selection::InputSelectionError` now has an additional `Change`
|
||||
variant. This necessitates the addition of two type parameters.
|
||||
- `wallet::input_selection::InputSelector::propose_transaction` takes an
|
||||
additional `change_strategy` argument, along with an associated `ChangeT`
|
||||
type parameter.
|
||||
- The `wallet::input_selection::InputSelector::FeeRule` associated type has
|
||||
been removed. The fee rule is now part of the change strategy passed to
|
||||
`propose_transaction`.
|
||||
- `wallet::input_selection::ShieldingSelector::propose_shielding` takes an
|
||||
additional `change_strategy` argument, along with an associated `ChangeT`
|
||||
type parameter. In addition, it also takes a new `to_account` argument
|
||||
that identifies the destination account for the shielded notes.
|
||||
- The `wallet::input_selection::ShieldingSelector::FeeRule` associated type
|
||||
has been removed. The fee rule is now part of the change strategy passed to
|
||||
`propose_shielding`.
|
||||
- The `Change` variant of `wallet::input_selection::GreedyInputSelectorError`
|
||||
has been removed, along with the additional type parameters it necessitated.
|
||||
- The arguments to `wallet::input_selection::GreedyInputSelector::new` have
|
||||
changed.
|
||||
- `zcash_client_backend::fees::ChangeStrategy` has changed. It has two new
|
||||
associated types, `MetaSource` and `WalletMeta`, and its `FeeRule` associated
|
||||
type now has an additional `Clone` bound. In addition, it defines a new
|
||||
`fetch_wallet_meta` method, and the arguments to `compute_balance` have
|
||||
changed.
|
||||
- `zcash_client_backend::fees::fixed::SingleOutputChangeStrategy::new`
|
||||
now takes an additional `DustOutputPolicy` argument. It also now carries
|
||||
an additional type parameter.
|
||||
- `zcash_client_backend::fees::standard::SingleOutputChangeStrategy::new`
|
||||
now takes an additional `DustOutputPolicy` argument. It also now carries
|
||||
an additional type parameter.
|
||||
- `zcash_client_backend::fees::zip317::SingleOutputChangeStrategy::new`
|
||||
now takes an additional `DustOutputPolicy` argument. It also now carries
|
||||
an additional type parameter.
|
||||
|
||||
### Changed
|
||||
- MSRV is now 1.77.0.
|
||||
|
@ -25,6 +70,8 @@ and this library adheres to Rust's notion of
|
|||
- `zcash_client_backend::data_api`:
|
||||
- `WalletSummary::scan_progress` and `WalletSummary::recovery_progress` have
|
||||
been removed. Use `WalletSummary::progress` instead.
|
||||
- `zcash_client_backend::fees`:
|
||||
- `impl From<BalanceError> for ChangeError<...>`
|
||||
|
||||
## [0.14.0] - 2024-10-04
|
||||
|
||||
|
|
|
@ -794,7 +794,7 @@ impl<NoteRef> SpendableNotes<NoteRef> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Metadata about the structure of the wallet for a particular account.
|
||||
/// Metadata about the structure of the wallet for a particular account.
|
||||
///
|
||||
/// At present this just contains counts of unspent outputs in each pool, but it may be extended in
|
||||
/// the future to contain note values or other more detailed information about wallet structure.
|
||||
|
|
|
@ -13,6 +13,7 @@ use zcash_primitives::transaction::{
|
|||
|
||||
use crate::address::UnifiedAddress;
|
||||
use crate::data_api::wallet::input_selection::InputSelectorError;
|
||||
use crate::fees::ChangeError;
|
||||
use crate::proposal::ProposalError;
|
||||
use crate::PoolType;
|
||||
|
||||
|
@ -23,7 +24,8 @@ use crate::wallet::NoteId;
|
|||
|
||||
/// Errors that can occur as a consequence of wallet operations.
|
||||
#[derive(Debug)]
|
||||
pub enum Error<DataSourceError, CommitmentTreeError, SelectionError, FeeError> {
|
||||
pub enum Error<DataSourceError, CommitmentTreeError, SelectionError, FeeError, ChangeErrT, NoteRefT>
|
||||
{
|
||||
/// An error occurred retrieving data from the underlying data source
|
||||
DataSource(DataSourceError),
|
||||
|
||||
|
@ -33,6 +35,9 @@ pub enum Error<DataSourceError, CommitmentTreeError, SelectionError, FeeError> {
|
|||
/// An error in note selection
|
||||
NoteSelection(SelectionError),
|
||||
|
||||
/// An error in change selection during transaction proposal construction
|
||||
Change(ChangeError<ChangeErrT, NoteRefT>),
|
||||
|
||||
/// An error in transaction proposal construction
|
||||
Proposal(ProposalError),
|
||||
|
||||
|
@ -98,12 +103,14 @@ pub enum Error<DataSourceError, CommitmentTreeError, SelectionError, FeeError> {
|
|||
PaysEphemeralTransparentAddress(String),
|
||||
}
|
||||
|
||||
impl<DE, CE, SE, FE> fmt::Display for Error<DE, CE, SE, FE>
|
||||
impl<DE, TE, SE, FE, CE, N> fmt::Display for Error<DE, TE, SE, FE, CE, N>
|
||||
where
|
||||
DE: fmt::Display,
|
||||
CE: fmt::Display,
|
||||
TE: fmt::Display,
|
||||
SE: fmt::Display,
|
||||
FE: fmt::Display,
|
||||
CE: fmt::Display,
|
||||
N: fmt::Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use fmt::Write;
|
||||
|
@ -122,6 +129,9 @@ where
|
|||
Error::NoteSelection(e) => {
|
||||
write!(f, "Note selection encountered the following error: {}", e)
|
||||
}
|
||||
Error::Change(e) => {
|
||||
write!(f, "Change output generation failed: {}", e)
|
||||
}
|
||||
Error::Proposal(e) => {
|
||||
write!(f, "Input selection attempted to construct an invalid proposal: {}", e)
|
||||
}
|
||||
|
@ -178,12 +188,14 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<DE, CE, SE, FE> error::Error for Error<DE, CE, SE, FE>
|
||||
impl<DE, TE, SE, FE, CE, N> error::Error for Error<DE, TE, SE, FE, CE, N>
|
||||
where
|
||||
DE: Debug + Display + error::Error + 'static,
|
||||
CE: Debug + Display + error::Error + 'static,
|
||||
TE: Debug + Display + error::Error + 'static,
|
||||
SE: Debug + Display + error::Error + 'static,
|
||||
FE: Debug + Display + 'static,
|
||||
CE: Debug + Display + error::Error + 'static,
|
||||
N: Debug + Display + 'static,
|
||||
{
|
||||
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
||||
match &self {
|
||||
|
@ -197,35 +209,38 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<DE, CE, SE, FE> From<builder::Error<FE>> for Error<DE, CE, SE, FE> {
|
||||
impl<DE, TE, SE, FE, CE, N> From<builder::Error<FE>> for Error<DE, TE, SE, FE, CE, N> {
|
||||
fn from(e: builder::Error<FE>) -> Self {
|
||||
Error::Builder(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl<DE, CE, SE, FE> From<ProposalError> for Error<DE, CE, SE, FE> {
|
||||
impl<DE, TE, SE, FE, CE, N> From<ProposalError> for Error<DE, TE, SE, FE, CE, N> {
|
||||
fn from(e: ProposalError) -> Self {
|
||||
Error::Proposal(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl<DE, CE, SE, FE> From<BalanceError> for Error<DE, CE, SE, FE> {
|
||||
impl<DE, TE, SE, FE, CE, N> From<BalanceError> for Error<DE, TE, SE, FE, CE, N> {
|
||||
fn from(e: BalanceError) -> Self {
|
||||
Error::BalanceError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl<DE, CE, SE, FE> From<ConversionError<&'static str>> for Error<DE, CE, SE, FE> {
|
||||
impl<DE, TE, SE, FE, CE, N> From<ConversionError<&'static str>> for Error<DE, TE, SE, FE, CE, N> {
|
||||
fn from(value: ConversionError<&'static str>) -> Self {
|
||||
Error::Address(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<DE, CE, SE, FE> From<InputSelectorError<DE, SE>> for Error<DE, CE, SE, FE> {
|
||||
fn from(e: InputSelectorError<DE, SE>) -> Self {
|
||||
impl<DE, TE, SE, FE, CE, N> From<InputSelectorError<DE, SE, CE, N>>
|
||||
for Error<DE, TE, SE, FE, CE, N>
|
||||
{
|
||||
fn from(e: InputSelectorError<DE, SE, CE, N>) -> Self {
|
||||
match e {
|
||||
InputSelectorError::DataSource(e) => Error::DataSource(e),
|
||||
InputSelectorError::Selection(e) => Error::NoteSelection(e),
|
||||
InputSelectorError::Change(e) => Error::Change(e),
|
||||
InputSelectorError::Proposal(e) => Error::Proposal(e),
|
||||
InputSelectorError::InsufficientFunds {
|
||||
available,
|
||||
|
@ -240,20 +255,20 @@ impl<DE, CE, SE, FE> From<InputSelectorError<DE, SE>> for Error<DE, CE, SE, FE>
|
|||
}
|
||||
}
|
||||
|
||||
impl<DE, CE, SE, FE> From<sapling::builder::Error> for Error<DE, CE, SE, FE> {
|
||||
impl<DE, TE, SE, FE, CE, N> From<sapling::builder::Error> for Error<DE, TE, SE, FE, CE, N> {
|
||||
fn from(e: sapling::builder::Error) -> Self {
|
||||
Error::Builder(builder::Error::SaplingBuild(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl<DE, CE, SE, FE> From<transparent::builder::Error> for Error<DE, CE, SE, FE> {
|
||||
impl<DE, TE, SE, FE, CE, N> From<transparent::builder::Error> for Error<DE, TE, SE, FE, CE, N> {
|
||||
fn from(e: transparent::builder::Error) -> Self {
|
||||
Error::Builder(builder::Error::TransparentBuild(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl<DE, CE, SE, FE> From<ShardTreeError<CE>> for Error<DE, CE, SE, FE> {
|
||||
fn from(e: ShardTreeError<CE>) -> Self {
|
||||
impl<DE, TE, SE, FE, CE, N> From<ShardTreeError<TE>> for Error<DE, TE, SE, FE, CE, N> {
|
||||
fn from(e: ShardTreeError<TE>) -> Self {
|
||||
Error::CommitmentTree(e)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ use zcash_primitives::{
|
|||
memo::Memo,
|
||||
transaction::{
|
||||
components::{amount::NonNegativeAmount, sapling::zip212_enforcement},
|
||||
fees::{zip317::FeeError as Zip317FeeError, FeeRule, StandardFeeRule},
|
||||
fees::{FeeRule, StandardFeeRule},
|
||||
Transaction, TxId,
|
||||
},
|
||||
};
|
||||
|
@ -46,7 +46,10 @@ use zip32::{fingerprint::SeedFingerprint, DiversifierIndex};
|
|||
|
||||
use crate::{
|
||||
address::UnifiedAddress,
|
||||
fees::{standard, DustOutputPolicy},
|
||||
fees::{
|
||||
standard::{self, SingleOutputChangeStrategy},
|
||||
ChangeStrategy, DustOutputPolicy,
|
||||
},
|
||||
keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey},
|
||||
proposal::Proposal,
|
||||
proto::compact_formats::{
|
||||
|
@ -62,7 +65,7 @@ use super::{
|
|||
scanning::ScanRange,
|
||||
wallet::{
|
||||
create_proposed_transactions, create_spend_to_address,
|
||||
input_selection::{GreedyInputSelector, GreedyInputSelectorError, InputSelector},
|
||||
input_selection::{GreedyInputSelector, InputSelector},
|
||||
propose_standard_transfer_to_address, propose_transfer, spend,
|
||||
},
|
||||
Account, AccountBalance, AccountBirthday, AccountPurpose, AccountSource, BlockMetadata,
|
||||
|
@ -874,12 +877,7 @@ where
|
|||
fallback_change_pool: ShieldedProtocol,
|
||||
) -> Result<
|
||||
NonEmpty<TxId>,
|
||||
super::error::Error<
|
||||
ErrT,
|
||||
<DbT as WalletCommitmentTrees>::Error,
|
||||
GreedyInputSelectorError<Zip317FeeError, <DbT as InputSource>::NoteRef>,
|
||||
Zip317FeeError,
|
||||
>,
|
||||
super::wallet::TransferErrT<DbT, GreedyInputSelector<DbT>, SingleOutputChangeStrategy<DbT>>,
|
||||
> {
|
||||
let prover = LocalTxProver::bundled();
|
||||
let network = self.network().clone();
|
||||
|
@ -901,24 +899,18 @@ where
|
|||
|
||||
/// Invokes [`spend`] with the given arguments.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn spend<InputsT>(
|
||||
pub fn spend<InputsT, ChangeT>(
|
||||
&mut self,
|
||||
input_selector: &InputsT,
|
||||
change_strategy: &ChangeT,
|
||||
usk: &UnifiedSpendingKey,
|
||||
request: zip321::TransactionRequest,
|
||||
ovk_policy: OvkPolicy,
|
||||
min_confirmations: NonZeroU32,
|
||||
) -> Result<
|
||||
NonEmpty<TxId>,
|
||||
super::error::Error<
|
||||
ErrT,
|
||||
<DbT as WalletCommitmentTrees>::Error,
|
||||
InputsT::Error,
|
||||
<InputsT::FeeRule as FeeRule>::Error,
|
||||
>,
|
||||
>
|
||||
) -> Result<NonEmpty<TxId>, super::wallet::TransferErrT<DbT, InputsT, ChangeT>>
|
||||
where
|
||||
InputsT: InputSelector<InputSource = DbT>,
|
||||
ChangeT: ChangeStrategy<MetaSource = DbT>,
|
||||
{
|
||||
#![allow(deprecated)]
|
||||
let prover = LocalTxProver::bundled();
|
||||
|
@ -929,6 +921,7 @@ where
|
|||
&prover,
|
||||
&prover,
|
||||
input_selector,
|
||||
change_strategy,
|
||||
usk,
|
||||
request,
|
||||
ovk_policy,
|
||||
|
@ -938,25 +931,28 @@ where
|
|||
|
||||
/// Invokes [`propose_transfer`] with the given arguments.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn propose_transfer<InputsT>(
|
||||
pub fn propose_transfer<InputsT, ChangeT>(
|
||||
&mut self,
|
||||
spend_from_account: <DbT as InputSource>::AccountId,
|
||||
input_selector: &InputsT,
|
||||
change_strategy: &ChangeT,
|
||||
request: zip321::TransactionRequest,
|
||||
min_confirmations: NonZeroU32,
|
||||
) -> Result<
|
||||
Proposal<InputsT::FeeRule, <DbT as InputSource>::NoteRef>,
|
||||
super::error::Error<ErrT, Infallible, InputsT::Error, <InputsT::FeeRule as FeeRule>::Error>,
|
||||
Proposal<ChangeT::FeeRule, <DbT as InputSource>::NoteRef>,
|
||||
super::wallet::ProposeTransferErrT<DbT, Infallible, InputsT, ChangeT>,
|
||||
>
|
||||
where
|
||||
InputsT: InputSelector<InputSource = DbT>,
|
||||
ChangeT: ChangeStrategy<MetaSource = DbT>,
|
||||
{
|
||||
let network = self.network().clone();
|
||||
propose_transfer::<_, _, _, Infallible>(
|
||||
propose_transfer::<_, _, _, _, Infallible>(
|
||||
self.wallet_mut(),
|
||||
&network,
|
||||
spend_from_account,
|
||||
input_selector,
|
||||
change_strategy,
|
||||
request,
|
||||
min_confirmations,
|
||||
)
|
||||
|
@ -977,11 +973,11 @@ where
|
|||
fallback_change_pool: ShieldedProtocol,
|
||||
) -> Result<
|
||||
Proposal<StandardFeeRule, <DbT as InputSource>::NoteRef>,
|
||||
super::error::Error<
|
||||
ErrT,
|
||||
super::wallet::ProposeTransferErrT<
|
||||
DbT,
|
||||
CommitmentTreeErrT,
|
||||
GreedyInputSelectorError<Zip317FeeError, <DbT as InputSource>::NoteRef>,
|
||||
Zip317FeeError,
|
||||
GreedyInputSelector<DbT>,
|
||||
SingleOutputChangeStrategy<DbT>,
|
||||
>,
|
||||
> {
|
||||
let network = self.network().clone();
|
||||
|
@ -1011,47 +1007,47 @@ where
|
|||
#[cfg(feature = "transparent-inputs")]
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[allow(dead_code)]
|
||||
pub fn propose_shielding<InputsT>(
|
||||
pub fn propose_shielding<InputsT, ChangeT>(
|
||||
&mut self,
|
||||
input_selector: &InputsT,
|
||||
change_strategy: &ChangeT,
|
||||
shielding_threshold: NonNegativeAmount,
|
||||
from_addrs: &[TransparentAddress],
|
||||
to_account: <InputsT::InputSource as InputSource>::AccountId,
|
||||
min_confirmations: u32,
|
||||
) -> Result<
|
||||
Proposal<InputsT::FeeRule, Infallible>,
|
||||
super::error::Error<ErrT, Infallible, InputsT::Error, <InputsT::FeeRule as FeeRule>::Error>,
|
||||
Proposal<ChangeT::FeeRule, Infallible>,
|
||||
super::wallet::ProposeShieldingErrT<DbT, Infallible, InputsT, ChangeT>,
|
||||
>
|
||||
where
|
||||
InputsT: ShieldingSelector<InputSource = DbT>,
|
||||
ChangeT: ChangeStrategy<MetaSource = DbT>,
|
||||
{
|
||||
use super::wallet::propose_shielding;
|
||||
|
||||
let network = self.network().clone();
|
||||
propose_shielding::<_, _, _, Infallible>(
|
||||
propose_shielding::<_, _, _, _, Infallible>(
|
||||
self.wallet_mut(),
|
||||
&network,
|
||||
input_selector,
|
||||
change_strategy,
|
||||
shielding_threshold,
|
||||
from_addrs,
|
||||
to_account,
|
||||
min_confirmations,
|
||||
)
|
||||
}
|
||||
|
||||
/// Invokes [`create_proposed_transactions`] with the given arguments.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn create_proposed_transactions<InputsErrT, FeeRuleT>(
|
||||
pub fn create_proposed_transactions<InputsErrT, FeeRuleT, ChangeErrT>(
|
||||
&mut self,
|
||||
usk: &UnifiedSpendingKey,
|
||||
ovk_policy: OvkPolicy,
|
||||
proposal: &Proposal<FeeRuleT, <DbT as InputSource>::NoteRef>,
|
||||
) -> Result<
|
||||
NonEmpty<TxId>,
|
||||
super::error::Error<
|
||||
ErrT,
|
||||
<DbT as WalletCommitmentTrees>::Error,
|
||||
InputsErrT,
|
||||
FeeRuleT::Error,
|
||||
>,
|
||||
super::wallet::CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, DbT::NoteRef>,
|
||||
>
|
||||
where
|
||||
FeeRuleT: FeeRule,
|
||||
|
@ -1074,24 +1070,20 @@ where
|
|||
/// [`shield_transparent_funds`]: crate::data_api::wallet::shield_transparent_funds
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn shield_transparent_funds<InputsT>(
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn shield_transparent_funds<InputsT, ChangeT>(
|
||||
&mut self,
|
||||
input_selector: &InputsT,
|
||||
change_strategy: &ChangeT,
|
||||
shielding_threshold: NonNegativeAmount,
|
||||
usk: &UnifiedSpendingKey,
|
||||
from_addrs: &[TransparentAddress],
|
||||
to_account: <DbT as InputSource>::AccountId,
|
||||
min_confirmations: u32,
|
||||
) -> Result<
|
||||
NonEmpty<TxId>,
|
||||
super::error::Error<
|
||||
ErrT,
|
||||
<DbT as WalletCommitmentTrees>::Error,
|
||||
InputsT::Error,
|
||||
<InputsT::FeeRule as FeeRule>::Error,
|
||||
>,
|
||||
>
|
||||
) -> Result<NonEmpty<TxId>, super::wallet::ShieldErrT<DbT, InputsT, ChangeT>>
|
||||
where
|
||||
InputsT: ShieldingSelector<InputSource = DbT>,
|
||||
ChangeT: ChangeStrategy<MetaSource = DbT>,
|
||||
{
|
||||
use crate::data_api::wallet::shield_transparent_funds;
|
||||
|
||||
|
@ -1103,9 +1095,11 @@ where
|
|||
&prover,
|
||||
&prover,
|
||||
input_selector,
|
||||
change_strategy,
|
||||
shielding_threshold,
|
||||
usk,
|
||||
from_addrs,
|
||||
to_account,
|
||||
min_confirmations,
|
||||
)
|
||||
}
|
||||
|
@ -1229,15 +1223,22 @@ impl<Cache, DbT: WalletRead + Reset> TestState<Cache, DbT, LocalNetwork> {
|
|||
|
||||
/// Helper method for constructing a [`GreedyInputSelector`] with a
|
||||
/// [`standard::SingleOutputChangeStrategy`].
|
||||
pub fn input_selector<DbT: InputSource>(
|
||||
pub fn input_selector<DbT: InputSource>() -> GreedyInputSelector<DbT> {
|
||||
GreedyInputSelector::<DbT>::new()
|
||||
}
|
||||
|
||||
pub fn single_output_change_strategy<DbT: InputSource>(
|
||||
fee_rule: StandardFeeRule,
|
||||
change_memo: Option<&str>,
|
||||
fallback_change_pool: ShieldedProtocol,
|
||||
) -> GreedyInputSelector<DbT, standard::SingleOutputChangeStrategy> {
|
||||
) -> standard::SingleOutputChangeStrategy<DbT> {
|
||||
let change_memo = change_memo.map(|m| MemoBytes::from(m.parse::<Memo>().unwrap()));
|
||||
let change_strategy =
|
||||
standard::SingleOutputChangeStrategy::new(fee_rule, change_memo, fallback_change_pool);
|
||||
GreedyInputSelector::new(change_strategy, DustOutputPolicy::default())
|
||||
standard::SingleOutputChangeStrategy::new(
|
||||
fee_rule,
|
||||
change_memo,
|
||||
fallback_change_pool,
|
||||
DustOutputPolicy::default(),
|
||||
)
|
||||
}
|
||||
|
||||
// Checks that a protobuf proposal serialized from the provided proposal value correctly parses to
|
||||
|
|
|
@ -17,9 +17,7 @@ use zcash_primitives::{
|
|||
legacy::TransparentAddress,
|
||||
transaction::{
|
||||
components::amount::NonNegativeAmount,
|
||||
fees::{
|
||||
fixed::FeeRule as FixedFeeRule, zip317::FeeError as Zip317FeeError, StandardFeeRule,
|
||||
},
|
||||
fees::{fixed::FeeRule as FixedFeeRule, StandardFeeRule},
|
||||
Transaction,
|
||||
},
|
||||
};
|
||||
|
@ -38,16 +36,22 @@ use crate::{
|
|||
self,
|
||||
chain::{self, ChainState, CommitmentTreeRoot, ScanSummary},
|
||||
error::Error,
|
||||
testing::{input_selector, AddressType, FakeCompactOutput, InitialChainState, TestBuilder},
|
||||
testing::{
|
||||
single_output_change_strategy, AddressType, FakeCompactOutput, InitialChainState,
|
||||
TestBuilder,
|
||||
},
|
||||
wallet::{
|
||||
decrypt_and_store_transaction,
|
||||
input_selection::{GreedyInputSelector, GreedyInputSelectorError},
|
||||
decrypt_and_store_transaction, input_selection::GreedyInputSelector, TransferErrT,
|
||||
},
|
||||
Account as _, AccountBirthday, DecryptedTransaction, InputSource, Ratio,
|
||||
WalletCommitmentTrees, WalletRead, WalletSummary, WalletTest, WalletWrite,
|
||||
},
|
||||
decrypt_transaction,
|
||||
fees::{fixed, standard, DustOutputPolicy},
|
||||
fees::{
|
||||
fixed,
|
||||
standard::{self, SingleOutputChangeStrategy},
|
||||
DustOutputPolicy,
|
||||
},
|
||||
scanning::ScanError,
|
||||
wallet::{Note, NoteId, OvkPolicy, ReceivedNote},
|
||||
};
|
||||
|
@ -216,19 +220,21 @@ pub fn send_single_step_proposed_transfer<T: ShieldedPoolTester>(
|
|||
fee_rule,
|
||||
Some(change_memo.clone().into()),
|
||||
T::SHIELDED_PROTOCOL,
|
||||
DustOutputPolicy::default(),
|
||||
);
|
||||
let input_selector = &GreedyInputSelector::new(change_strategy, DustOutputPolicy::default());
|
||||
let input_selector = GreedyInputSelector::new();
|
||||
|
||||
let proposal = st
|
||||
.propose_transfer(
|
||||
account.id(),
|
||||
input_selector,
|
||||
&input_selector,
|
||||
&change_strategy,
|
||||
request,
|
||||
NonZeroU32::new(1).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let create_proposed_result = st.create_proposed_transactions::<Infallible, _>(
|
||||
let create_proposed_result = st.create_proposed_transactions::<Infallible, _, Infallible>(
|
||||
account.usk(),
|
||||
OvkPolicy::Sender,
|
||||
&proposal,
|
||||
|
@ -399,7 +405,7 @@ pub fn send_multi_step_proposed_transfer<T: ShieldedPoolTester, DSF>(
|
|||
);
|
||||
assert_eq!(steps[1].balance().proposed_change(), []);
|
||||
|
||||
let create_proposed_result = st.create_proposed_transactions::<Infallible, _>(
|
||||
let create_proposed_result = st.create_proposed_transactions::<Infallible, _, Infallible>(
|
||||
account.usk(),
|
||||
OvkPolicy::Sender,
|
||||
&proposal,
|
||||
|
@ -499,7 +505,7 @@ pub fn send_multi_step_proposed_transfer<T: ShieldedPoolTester, DSF>(
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
let create_proposed_result = st.create_proposed_transactions::<Infallible, _>(
|
||||
let create_proposed_result = st.create_proposed_transactions::<Infallible, _, Infallible>(
|
||||
account.usk(),
|
||||
OvkPolicy::Sender,
|
||||
&proposal,
|
||||
|
@ -758,7 +764,7 @@ pub fn proposal_fails_if_not_all_ephemeral_outputs_consumed<T: ShieldedPoolTeste
|
|||
// This is somewhat redundant with `send_multi_step_proposed_transfer`,
|
||||
// but tests the case with no change memo and ensures we haven't messed
|
||||
// up the test setup.
|
||||
let create_proposed_result = st.create_proposed_transactions::<Infallible, _>(
|
||||
let create_proposed_result = st.create_proposed_transactions::<Infallible, _, Infallible>(
|
||||
account.usk(),
|
||||
OvkPolicy::Sender,
|
||||
&proposal,
|
||||
|
@ -774,7 +780,7 @@ pub fn proposal_fails_if_not_all_ephemeral_outputs_consumed<T: ShieldedPoolTeste
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
let create_proposed_result = st.create_proposed_transactions::<Infallible, _>(
|
||||
let create_proposed_result = st.create_proposed_transactions::<Infallible, _, Infallible>(
|
||||
account.usk(),
|
||||
OvkPolicy::Sender,
|
||||
&frobbed_proposal,
|
||||
|
@ -998,7 +1004,11 @@ pub fn spend_fails_on_unverified_notes<T: ShieldedPoolTester>(
|
|||
|
||||
// Executing the proposal should succeed
|
||||
let txid = st
|
||||
.create_proposed_transactions::<Infallible, _>(account.usk(), OvkPolicy::Sender, &proposal)
|
||||
.create_proposed_transactions::<Infallible, _, Infallible>(
|
||||
account.usk(),
|
||||
OvkPolicy::Sender,
|
||||
&proposal,
|
||||
)
|
||||
.unwrap()[0];
|
||||
|
||||
let (h, _) = st.generate_next_block_including(txid);
|
||||
|
@ -1057,7 +1067,7 @@ pub fn spend_fails_on_locked_notes<T: ShieldedPoolTester>(
|
|||
|
||||
// Executing the proposal should succeed
|
||||
assert_matches!(
|
||||
st.create_proposed_transactions::<Infallible, _>(account.usk(), OvkPolicy::Sender, &proposal,),
|
||||
st.create_proposed_transactions::<Infallible, _, Infallible>(account.usk(), OvkPolicy::Sender, &proposal,),
|
||||
Ok(txids) if txids.len() == 1
|
||||
);
|
||||
|
||||
|
@ -1139,7 +1149,11 @@ pub fn spend_fails_on_locked_notes<T: ShieldedPoolTester>(
|
|||
.unwrap();
|
||||
|
||||
let txid2 = st
|
||||
.create_proposed_transactions::<Infallible, _>(account.usk(), OvkPolicy::Sender, &proposal)
|
||||
.create_proposed_transactions::<Infallible, _, Infallible>(
|
||||
account.usk(),
|
||||
OvkPolicy::Sender,
|
||||
&proposal,
|
||||
)
|
||||
.unwrap()[0];
|
||||
|
||||
let (h, _) = st.generate_next_block_including(txid2);
|
||||
|
@ -1187,7 +1201,11 @@ pub fn ovk_policy_prevents_recovery_from_chain<T: ShieldedPoolTester, DSF>(
|
|||
ovk_policy|
|
||||
-> Result<
|
||||
Option<(Note, Address, MemoBytes)>,
|
||||
Error<_, _, GreedyInputSelectorError<Zip317FeeError, _>, Zip317FeeError>,
|
||||
TransferErrT<
|
||||
DSF::DataStore,
|
||||
GreedyInputSelector<DSF::DataStore>,
|
||||
SingleOutputChangeStrategy<DSF::DataStore>,
|
||||
>,
|
||||
> {
|
||||
let min_confirmations = NonZeroU32::new(1).unwrap();
|
||||
let proposal = st.propose_standard_transfer(
|
||||
|
@ -1283,7 +1301,7 @@ pub fn spend_succeeds_to_t_addr_zero_change<T: ShieldedPoolTester>(
|
|||
|
||||
// Executing the proposal should succeed
|
||||
assert_matches!(
|
||||
st.create_proposed_transactions::<Infallible, _>(account.usk(), OvkPolicy::Sender, &proposal),
|
||||
st.create_proposed_transactions::<Infallible, _, Infallible>(account.usk(), OvkPolicy::Sender, &proposal),
|
||||
Ok(txids) if txids.len() == 1
|
||||
);
|
||||
}
|
||||
|
@ -1346,7 +1364,7 @@ pub fn change_note_spends_succeed<T: ShieldedPoolTester>(
|
|||
|
||||
// Executing the proposal should succeed
|
||||
assert_matches!(
|
||||
st.create_proposed_transactions::<Infallible, _>(account.usk(), OvkPolicy::Sender, &proposal),
|
||||
st.create_proposed_transactions::<Infallible, _, Infallible>(account.usk(), OvkPolicy::Sender, &proposal),
|
||||
Ok(txids) if txids.len() == 1
|
||||
);
|
||||
}
|
||||
|
@ -1396,14 +1414,18 @@ pub fn external_address_change_spends_detected_in_restore_from_seed<T: ShieldedP
|
|||
|
||||
#[allow(deprecated)]
|
||||
let fee_rule = FixedFeeRule::standard();
|
||||
let input_selector = GreedyInputSelector::new(
|
||||
fixed::SingleOutputChangeStrategy::new(fee_rule, None, T::SHIELDED_PROTOCOL),
|
||||
let change_strategy = fixed::SingleOutputChangeStrategy::new(
|
||||
fee_rule,
|
||||
None,
|
||||
T::SHIELDED_PROTOCOL,
|
||||
DustOutputPolicy::default(),
|
||||
);
|
||||
let input_selector = GreedyInputSelector::new();
|
||||
|
||||
let txid = st
|
||||
.spend(
|
||||
&input_selector,
|
||||
&change_strategy,
|
||||
&usk,
|
||||
req,
|
||||
OvkPolicy::Sender,
|
||||
|
@ -1447,8 +1469,8 @@ pub fn external_address_change_spends_detected_in_restore_from_seed<T: ShieldedP
|
|||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn zip317_spend<T: ShieldedPoolTester>(
|
||||
ds_factory: impl DataStoreFactory,
|
||||
pub fn zip317_spend<T: ShieldedPoolTester, DSF: DataStoreFactory>(
|
||||
ds_factory: DSF,
|
||||
cache: impl TestCache,
|
||||
) {
|
||||
let mut st = TestBuilder::new()
|
||||
|
@ -1484,7 +1506,9 @@ pub fn zip317_spend<T: ShieldedPoolTester>(
|
|||
assert_eq!(st.get_total_balance(account_id), total);
|
||||
assert_eq!(st.get_spendable_balance(account_id, 1), total);
|
||||
|
||||
let input_selector = input_selector(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL);
|
||||
let input_selector = GreedyInputSelector::<DSF::DataStore>::new();
|
||||
let change_strategy =
|
||||
single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL);
|
||||
|
||||
// This first request will fail due to insufficient non-dust funds
|
||||
let req = TransactionRequest::new(vec![Payment::without_memo(
|
||||
|
@ -1496,6 +1520,7 @@ pub fn zip317_spend<T: ShieldedPoolTester>(
|
|||
assert_matches!(
|
||||
st.spend(
|
||||
&input_selector,
|
||||
&change_strategy,
|
||||
account.usk(),
|
||||
req,
|
||||
OvkPolicy::Sender,
|
||||
|
@ -1517,6 +1542,7 @@ pub fn zip317_spend<T: ShieldedPoolTester>(
|
|||
let txid = st
|
||||
.spend(
|
||||
&input_selector,
|
||||
&change_strategy,
|
||||
account.usk(),
|
||||
req,
|
||||
OvkPolicy::Sender,
|
||||
|
@ -1579,19 +1605,18 @@ where
|
|||
let res0 = st.wallet_mut().put_received_transparent_utxo(&utxo);
|
||||
assert_matches!(res0, Ok(_));
|
||||
|
||||
let fee_rule = StandardFeeRule::Zip317;
|
||||
|
||||
let input_selector = GreedyInputSelector::new(
|
||||
standard::SingleOutputChangeStrategy::new(fee_rule, None, T::SHIELDED_PROTOCOL),
|
||||
DustOutputPolicy::default(),
|
||||
);
|
||||
let input_selector = GreedyInputSelector::new();
|
||||
let change_strategy =
|
||||
single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL);
|
||||
|
||||
let txids = st
|
||||
.shield_transparent_funds(
|
||||
&input_selector,
|
||||
&change_strategy,
|
||||
NonNegativeAmount::from_u64(10000).unwrap(),
|
||||
account.usk(),
|
||||
&[*taddr],
|
||||
account.id(),
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
|
@ -1827,15 +1852,14 @@ pub fn pool_crossing_required<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
|
|||
)])
|
||||
.unwrap();
|
||||
|
||||
let fee_rule = StandardFeeRule::Zip317;
|
||||
let input_selector = GreedyInputSelector::new(
|
||||
standard::SingleOutputChangeStrategy::new(fee_rule, None, P1::SHIELDED_PROTOCOL),
|
||||
DustOutputPolicy::default(),
|
||||
);
|
||||
let input_selector = GreedyInputSelector::new();
|
||||
let change_strategy =
|
||||
single_output_change_strategy(StandardFeeRule::Zip317, None, P1::SHIELDED_PROTOCOL);
|
||||
let proposal0 = st
|
||||
.propose_transfer(
|
||||
account.id(),
|
||||
&input_selector,
|
||||
&change_strategy,
|
||||
p0_to_p1,
|
||||
NonZeroU32::new(1).unwrap(),
|
||||
)
|
||||
|
@ -1863,7 +1887,7 @@ pub fn pool_crossing_required<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
|
|||
);
|
||||
assert_eq!(change_output.value(), expected_change);
|
||||
|
||||
let create_proposed_result = st.create_proposed_transactions::<Infallible, _>(
|
||||
let create_proposed_result = st.create_proposed_transactions::<Infallible, _, Infallible>(
|
||||
account.usk(),
|
||||
OvkPolicy::Sender,
|
||||
&proposal0,
|
||||
|
@ -1918,17 +1942,16 @@ pub fn fully_funded_fully_private<P0: ShieldedPoolTester, P1: ShieldedPoolTester
|
|||
)])
|
||||
.unwrap();
|
||||
|
||||
let fee_rule = StandardFeeRule::Zip317;
|
||||
let input_selector = GreedyInputSelector::new(
|
||||
// We set the default change output pool to P0, because we want to verify later that
|
||||
// change is actually sent to P1 (as the transaction is fully fundable from P1).
|
||||
standard::SingleOutputChangeStrategy::new(fee_rule, None, P0::SHIELDED_PROTOCOL),
|
||||
DustOutputPolicy::default(),
|
||||
);
|
||||
let input_selector = GreedyInputSelector::new();
|
||||
// We set the default change output pool to P0, because we want to verify later that
|
||||
// change is actually sent to P1 (as the transaction is fully fundable from P1).
|
||||
let change_strategy =
|
||||
single_output_change_strategy(StandardFeeRule::Zip317, None, P0::SHIELDED_PROTOCOL);
|
||||
let proposal0 = st
|
||||
.propose_transfer(
|
||||
account.id(),
|
||||
&input_selector,
|
||||
&change_strategy,
|
||||
p0_to_p1,
|
||||
NonZeroU32::new(1).unwrap(),
|
||||
)
|
||||
|
@ -1955,7 +1978,7 @@ pub fn fully_funded_fully_private<P0: ShieldedPoolTester, P1: ShieldedPoolTester
|
|||
);
|
||||
assert_eq!(change_output.value(), expected_change);
|
||||
|
||||
let create_proposed_result = st.create_proposed_transactions::<Infallible, _>(
|
||||
let create_proposed_result = st.create_proposed_transactions::<Infallible, _, Infallible>(
|
||||
account.usk(),
|
||||
OvkPolicy::Sender,
|
||||
&proposal0,
|
||||
|
@ -2009,17 +2032,16 @@ pub fn fully_funded_send_to_t<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
|
|||
)])
|
||||
.unwrap();
|
||||
|
||||
let fee_rule = StandardFeeRule::Zip317;
|
||||
let input_selector = GreedyInputSelector::new(
|
||||
// We set the default change output pool to P0, because we want to verify later that
|
||||
// change is actually sent to P1 (as the transaction is fully fundable from P1).
|
||||
standard::SingleOutputChangeStrategy::new(fee_rule, None, P0::SHIELDED_PROTOCOL),
|
||||
DustOutputPolicy::default(),
|
||||
);
|
||||
let input_selector = GreedyInputSelector::new();
|
||||
// We set the default change output pool to P0, because we want to verify later that
|
||||
// change is actually sent to P1 (as the transaction is fully fundable from P1).
|
||||
let change_strategy =
|
||||
single_output_change_strategy(StandardFeeRule::Zip317, None, P0::SHIELDED_PROTOCOL);
|
||||
let proposal0 = st
|
||||
.propose_transfer(
|
||||
account.id(),
|
||||
&input_selector,
|
||||
&change_strategy,
|
||||
p0_to_p1,
|
||||
NonZeroU32::new(1).unwrap(),
|
||||
)
|
||||
|
@ -2043,7 +2065,7 @@ pub fn fully_funded_send_to_t<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
|
|||
assert_eq!(change_output.output_pool(), PoolType::SAPLING);
|
||||
assert_eq!(change_output.value(), expected_change);
|
||||
|
||||
let create_proposed_result = st.create_proposed_transactions::<Infallible, _>(
|
||||
let create_proposed_result = st.create_proposed_transactions::<Infallible, _, Infallible>(
|
||||
account.usk(),
|
||||
OvkPolicy::Sender,
|
||||
&proposal0,
|
||||
|
@ -2110,11 +2132,9 @@ pub fn multi_pool_checkpoint<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
|
|||
assert_eq!(st.get_spendable_balance(acct_id, 1), initial_balance);
|
||||
|
||||
// Set up the fee rule and input selector we'll use for all the transfers.
|
||||
let fee_rule = StandardFeeRule::Zip317;
|
||||
let input_selector = GreedyInputSelector::new(
|
||||
standard::SingleOutputChangeStrategy::new(fee_rule, None, P1::SHIELDED_PROTOCOL),
|
||||
DustOutputPolicy::default(),
|
||||
);
|
||||
let input_selector = GreedyInputSelector::new();
|
||||
let change_strategy =
|
||||
single_output_change_strategy(StandardFeeRule::Zip317, None, P1::SHIELDED_PROTOCOL);
|
||||
|
||||
// First, send funds just to P0
|
||||
let transfer_amount = NonNegativeAmount::const_from_u64(200000);
|
||||
|
@ -2126,6 +2146,7 @@ pub fn multi_pool_checkpoint<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
|
|||
let res = st
|
||||
.spend(
|
||||
&input_selector,
|
||||
&change_strategy,
|
||||
account.usk(),
|
||||
p0_transfer,
|
||||
OvkPolicy::Sender,
|
||||
|
@ -2157,6 +2178,7 @@ pub fn multi_pool_checkpoint<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
|
|||
let res = st
|
||||
.spend(
|
||||
&input_selector,
|
||||
&change_strategy,
|
||||
account.usk(),
|
||||
both_transfer,
|
||||
OvkPolicy::Sender,
|
||||
|
@ -2597,17 +2619,14 @@ pub fn scan_cached_blocks_allows_blocks_out_of_order<T: ShieldedPoolTester>(
|
|||
.unwrap();
|
||||
|
||||
#[allow(deprecated)]
|
||||
let input_selector = GreedyInputSelector::new(
|
||||
standard::SingleOutputChangeStrategy::new(
|
||||
StandardFeeRule::Zip317,
|
||||
None,
|
||||
T::SHIELDED_PROTOCOL,
|
||||
),
|
||||
DustOutputPolicy::default(),
|
||||
);
|
||||
let input_selector = GreedyInputSelector::new();
|
||||
let change_strategy =
|
||||
single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL);
|
||||
|
||||
assert_matches!(
|
||||
st.spend(
|
||||
&input_selector,
|
||||
&change_strategy,
|
||||
account.usk(),
|
||||
req,
|
||||
OvkPolicy::Sender,
|
||||
|
|
|
@ -197,16 +197,23 @@ where
|
|||
check_balance(&st, 0, value);
|
||||
|
||||
// Shield the output.
|
||||
let input_selector = GreedyInputSelector::new(
|
||||
fixed::SingleOutputChangeStrategy::new(
|
||||
FixedFeeRule::non_standard(NonNegativeAmount::ZERO),
|
||||
None,
|
||||
ShieldedProtocol::Sapling,
|
||||
),
|
||||
let input_selector = GreedyInputSelector::new();
|
||||
let change_strategy = fixed::SingleOutputChangeStrategy::new(
|
||||
FixedFeeRule::non_standard(NonNegativeAmount::ZERO),
|
||||
None,
|
||||
ShieldedProtocol::Sapling,
|
||||
DustOutputPolicy::default(),
|
||||
);
|
||||
let txid = st
|
||||
.shield_transparent_funds(&input_selector, value, account.usk(), &[*taddr], 1)
|
||||
.shield_transparent_funds(
|
||||
&input_selector,
|
||||
&change_strategy,
|
||||
value,
|
||||
account.usk(),
|
||||
&[*taddr],
|
||||
account.id(),
|
||||
1,
|
||||
)
|
||||
.unwrap()[0];
|
||||
|
||||
// The wallet should have zero transparent balance, because the shielding
|
||||
|
|
|
@ -50,7 +50,7 @@ use crate::{
|
|||
WalletRead, WalletWrite,
|
||||
},
|
||||
decrypt_transaction,
|
||||
fees::{self, DustOutputPolicy},
|
||||
fees::{standard::SingleOutputChangeStrategy, ChangeStrategy, DustOutputPolicy},
|
||||
keys::UnifiedSpendingKey,
|
||||
proposal::{Proposal, ProposalError, Step, StepOutputIndex},
|
||||
wallet::{Note, OvkPolicy, Recipient},
|
||||
|
@ -62,7 +62,7 @@ use zcash_primitives::{
|
|||
transaction::{
|
||||
builder::{BuildConfig, BuildResult, Builder},
|
||||
components::{amount::NonNegativeAmount, sapling::zip212_enforcement, OutPoint},
|
||||
fees::{zip317::FeeError as Zip317FeeError, FeeRule, StandardFeeRule},
|
||||
fees::{FeeRule, StandardFeeRule},
|
||||
Transaction, TxId,
|
||||
},
|
||||
};
|
||||
|
@ -83,9 +83,7 @@ use {
|
|||
};
|
||||
|
||||
pub mod input_selection;
|
||||
use input_selection::{
|
||||
GreedyInputSelector, GreedyInputSelectorError, InputSelector, InputSelectorError,
|
||||
};
|
||||
use input_selection::{GreedyInputSelector, InputSelector, InputSelectorError};
|
||||
|
||||
/// Scans a [`Transaction`] for any information that can be decrypted by the accounts in
|
||||
/// the wallet, and saves it to the wallet.
|
||||
|
@ -117,6 +115,53 @@ where
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub type ProposeTransferErrT<DbT, CommitmentTreeErrT, InputsT, ChangeT> = Error<
|
||||
<DbT as WalletRead>::Error,
|
||||
CommitmentTreeErrT,
|
||||
<InputsT as InputSelector>::Error,
|
||||
<<ChangeT as ChangeStrategy>::FeeRule as FeeRule>::Error,
|
||||
<ChangeT as ChangeStrategy>::Error,
|
||||
<<InputsT as InputSelector>::InputSource as InputSource>::NoteRef,
|
||||
>;
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
pub type ProposeShieldingErrT<DbT, CommitmentTreeErrT, InputsT, ChangeT> = Error<
|
||||
<DbT as WalletRead>::Error,
|
||||
CommitmentTreeErrT,
|
||||
<InputsT as ShieldingSelector>::Error,
|
||||
<<ChangeT as ChangeStrategy>::FeeRule as FeeRule>::Error,
|
||||
<ChangeT as ChangeStrategy>::Error,
|
||||
Infallible,
|
||||
>;
|
||||
|
||||
pub type CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N> = Error<
|
||||
<DbT as WalletRead>::Error,
|
||||
<DbT as WalletCommitmentTrees>::Error,
|
||||
InputsErrT,
|
||||
<FeeRuleT as FeeRule>::Error,
|
||||
ChangeErrT,
|
||||
N,
|
||||
>;
|
||||
|
||||
pub type TransferErrT<DbT, InputsT, ChangeT> = Error<
|
||||
<DbT as WalletRead>::Error,
|
||||
<DbT as WalletCommitmentTrees>::Error,
|
||||
<InputsT as InputSelector>::Error,
|
||||
<<ChangeT as ChangeStrategy>::FeeRule as FeeRule>::Error,
|
||||
<ChangeT as ChangeStrategy>::Error,
|
||||
<<InputsT as InputSelector>::InputSource as InputSource>::NoteRef,
|
||||
>;
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
pub type ShieldErrT<DbT, InputsT, ChangeT> = Error<
|
||||
<DbT as WalletRead>::Error,
|
||||
<DbT as WalletCommitmentTrees>::Error,
|
||||
<InputsT as ShieldingSelector>::Error,
|
||||
<<ChangeT as ChangeStrategy>::FeeRule as FeeRule>::Error,
|
||||
<ChangeT as ChangeStrategy>::Error,
|
||||
Infallible,
|
||||
>;
|
||||
|
||||
#[allow(clippy::needless_doctest_main)]
|
||||
/// Creates a transaction or series of transactions paying the specified address from
|
||||
/// the given account, and the [`TxId`] corresponding to each newly-created transaction.
|
||||
|
@ -251,12 +296,7 @@ pub fn create_spend_to_address<DbT, ParamsT>(
|
|||
fallback_change_pool: ShieldedProtocol,
|
||||
) -> Result<
|
||||
NonEmpty<TxId>,
|
||||
Error<
|
||||
<DbT as WalletRead>::Error,
|
||||
<DbT as WalletCommitmentTrees>::Error,
|
||||
GreedyInputSelectorError<Zip317FeeError, DbT::NoteRef>,
|
||||
Zip317FeeError,
|
||||
>,
|
||||
TransferErrT<DbT, GreedyInputSelector<DbT>, SingleOutputChangeStrategy<DbT>>,
|
||||
>
|
||||
where
|
||||
ParamsT: consensus::Parameters + Clone,
|
||||
|
@ -297,13 +337,6 @@ where
|
|||
)
|
||||
}
|
||||
|
||||
type ErrorT<DbT, InputsErrT, FeeRuleT> = Error<
|
||||
<DbT as WalletRead>::Error,
|
||||
<DbT as WalletCommitmentTrees>::Error,
|
||||
InputsErrT,
|
||||
<FeeRuleT as FeeRule>::Error,
|
||||
>;
|
||||
|
||||
/// Constructs a transaction or series of transactions that send funds as specified
|
||||
/// by the `request` argument, stores them to the wallet's "sent transactions" data
|
||||
/// store, and returns the [`TxId`] for each transaction constructed.
|
||||
|
@ -358,17 +391,18 @@ type ErrorT<DbT, InputsErrT, FeeRuleT> = Error<
|
|||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[deprecated(note = "Use `propose_transfer` and `create_proposed_transactions` instead.")]
|
||||
pub fn spend<DbT, ParamsT, InputsT>(
|
||||
pub fn spend<DbT, ParamsT, InputsT, ChangeT>(
|
||||
wallet_db: &mut DbT,
|
||||
params: &ParamsT,
|
||||
spend_prover: &impl SpendProver,
|
||||
output_prover: &impl OutputProver,
|
||||
input_selector: &InputsT,
|
||||
change_strategy: &ChangeT,
|
||||
usk: &UnifiedSpendingKey,
|
||||
request: zip321::TransactionRequest,
|
||||
ovk_policy: OvkPolicy,
|
||||
min_confirmations: NonZeroU32,
|
||||
) -> Result<NonEmpty<TxId>, ErrorT<DbT, InputsT::Error, InputsT::FeeRule>>
|
||||
) -> Result<NonEmpty<TxId>, TransferErrT<DbT, InputsT, ChangeT>>
|
||||
where
|
||||
DbT: InputSource,
|
||||
DbT: WalletWrite<
|
||||
|
@ -378,6 +412,7 @@ where
|
|||
DbT: WalletCommitmentTrees,
|
||||
ParamsT: consensus::Parameters + Clone,
|
||||
InputsT: InputSelector<InputSource = DbT>,
|
||||
ChangeT: ChangeStrategy<MetaSource = DbT>,
|
||||
{
|
||||
let account = wallet_db
|
||||
.get_account_for_ufvk(&usk.to_unified_full_viewing_key())
|
||||
|
@ -389,6 +424,7 @@ where
|
|||
params,
|
||||
account.id(),
|
||||
input_selector,
|
||||
change_strategy,
|
||||
request,
|
||||
min_confirmations,
|
||||
)?;
|
||||
|
@ -409,27 +445,24 @@ where
|
|||
/// [`create_proposed_transactions`].
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn propose_transfer<DbT, ParamsT, InputsT, CommitmentTreeErrT>(
|
||||
pub fn propose_transfer<DbT, ParamsT, InputsT, ChangeT, CommitmentTreeErrT>(
|
||||
wallet_db: &mut DbT,
|
||||
params: &ParamsT,
|
||||
spend_from_account: <DbT as InputSource>::AccountId,
|
||||
input_selector: &InputsT,
|
||||
change_strategy: &ChangeT,
|
||||
request: zip321::TransactionRequest,
|
||||
min_confirmations: NonZeroU32,
|
||||
) -> Result<
|
||||
Proposal<InputsT::FeeRule, <DbT as InputSource>::NoteRef>,
|
||||
Error<
|
||||
<DbT as WalletRead>::Error,
|
||||
CommitmentTreeErrT,
|
||||
InputsT::Error,
|
||||
<InputsT::FeeRule as FeeRule>::Error,
|
||||
>,
|
||||
Proposal<ChangeT::FeeRule, <DbT as InputSource>::NoteRef>,
|
||||
ProposeTransferErrT<DbT, CommitmentTreeErrT, InputsT, ChangeT>,
|
||||
>
|
||||
where
|
||||
DbT: WalletRead + InputSource<Error = <DbT as WalletRead>::Error>,
|
||||
<DbT as InputSource>::NoteRef: Copy + Eq + Ord,
|
||||
ParamsT: consensus::Parameters + Clone,
|
||||
InputsT: InputSelector<InputSource = DbT>,
|
||||
ChangeT: ChangeStrategy<MetaSource = DbT>,
|
||||
{
|
||||
let (target_height, anchor_height) = wallet_db
|
||||
.get_target_and_anchor_heights(min_confirmations)
|
||||
|
@ -444,6 +477,7 @@ where
|
|||
anchor_height,
|
||||
spend_from_account,
|
||||
request,
|
||||
change_strategy,
|
||||
)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
@ -488,11 +522,11 @@ pub fn propose_standard_transfer_to_address<DbT, ParamsT, CommitmentTreeErrT>(
|
|||
fallback_change_pool: ShieldedProtocol,
|
||||
) -> Result<
|
||||
Proposal<StandardFeeRule, DbT::NoteRef>,
|
||||
Error<
|
||||
<DbT as WalletRead>::Error,
|
||||
ProposeTransferErrT<
|
||||
DbT,
|
||||
CommitmentTreeErrT,
|
||||
GreedyInputSelectorError<Zip317FeeError, DbT::NoteRef>,
|
||||
Zip317FeeError,
|
||||
GreedyInputSelector<DbT>,
|
||||
SingleOutputChangeStrategy<DbT>,
|
||||
>,
|
||||
>
|
||||
where
|
||||
|
@ -517,19 +551,20 @@ where
|
|||
"It should not be possible for this to violate ZIP 321 request construction invariants.",
|
||||
);
|
||||
|
||||
let change_strategy = fees::standard::SingleOutputChangeStrategy::new(
|
||||
let input_selector = GreedyInputSelector::<DbT>::new();
|
||||
let change_strategy = SingleOutputChangeStrategy::<DbT>::new(
|
||||
fee_rule,
|
||||
change_memo,
|
||||
fallback_change_pool,
|
||||
DustOutputPolicy::default(),
|
||||
);
|
||||
let input_selector =
|
||||
GreedyInputSelector::<DbT, _>::new(change_strategy, DustOutputPolicy::default());
|
||||
|
||||
propose_transfer(
|
||||
wallet_db,
|
||||
params,
|
||||
spend_from_account,
|
||||
&input_selector,
|
||||
&change_strategy,
|
||||
request,
|
||||
min_confirmations,
|
||||
)
|
||||
|
@ -540,26 +575,24 @@ where
|
|||
#[cfg(feature = "transparent-inputs")]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn propose_shielding<DbT, ParamsT, InputsT, CommitmentTreeErrT>(
|
||||
pub fn propose_shielding<DbT, ParamsT, InputsT, ChangeT, CommitmentTreeErrT>(
|
||||
wallet_db: &mut DbT,
|
||||
params: &ParamsT,
|
||||
input_selector: &InputsT,
|
||||
change_strategy: &ChangeT,
|
||||
shielding_threshold: NonNegativeAmount,
|
||||
from_addrs: &[TransparentAddress],
|
||||
to_account: <DbT as InputSource>::AccountId,
|
||||
min_confirmations: u32,
|
||||
) -> Result<
|
||||
Proposal<InputsT::FeeRule, Infallible>,
|
||||
Error<
|
||||
<DbT as WalletRead>::Error,
|
||||
CommitmentTreeErrT,
|
||||
InputsT::Error,
|
||||
<InputsT::FeeRule as FeeRule>::Error,
|
||||
>,
|
||||
Proposal<ChangeT::FeeRule, Infallible>,
|
||||
ProposeShieldingErrT<DbT, CommitmentTreeErrT, InputsT, ChangeT>,
|
||||
>
|
||||
where
|
||||
ParamsT: consensus::Parameters,
|
||||
DbT: WalletRead + InputSource<Error = <DbT as WalletRead>::Error>,
|
||||
InputsT: ShieldingSelector<InputSource = DbT>,
|
||||
ChangeT: ChangeStrategy<MetaSource = DbT>,
|
||||
{
|
||||
let chain_tip_height = wallet_db
|
||||
.chain_height()
|
||||
|
@ -570,8 +603,10 @@ where
|
|||
.propose_shielding(
|
||||
params,
|
||||
wallet_db,
|
||||
change_strategy,
|
||||
shielding_threshold,
|
||||
from_addrs,
|
||||
to_account,
|
||||
chain_tip_height + 1,
|
||||
min_confirmations,
|
||||
)
|
||||
|
@ -599,7 +634,7 @@ struct StepResult<AccountId> {
|
|||
/// and therefore the required spend proofs for such notes cannot be constructed.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn create_proposed_transactions<DbT, ParamsT, InputsErrT, FeeRuleT, N>(
|
||||
pub fn create_proposed_transactions<DbT, ParamsT, InputsErrT, FeeRuleT, ChangeErrT, N>(
|
||||
wallet_db: &mut DbT,
|
||||
params: &ParamsT,
|
||||
spend_prover: &impl SpendProver,
|
||||
|
@ -607,7 +642,7 @@ pub fn create_proposed_transactions<DbT, ParamsT, InputsErrT, FeeRuleT, N>(
|
|||
usk: &UnifiedSpendingKey,
|
||||
ovk_policy: OvkPolicy,
|
||||
proposal: &Proposal<FeeRuleT, N>,
|
||||
) -> Result<NonEmpty<TxId>, ErrorT<DbT, InputsErrT, FeeRuleT>>
|
||||
) -> Result<NonEmpty<TxId>, CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>>
|
||||
where
|
||||
DbT: WalletWrite + WalletCommitmentTrees,
|
||||
ParamsT: consensus::Parameters + Clone,
|
||||
|
@ -691,7 +726,7 @@ where
|
|||
// `TransparentAddress` and `Outpoint`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn create_proposed_transaction<DbT, ParamsT, InputsErrT, FeeRuleT, N>(
|
||||
fn create_proposed_transaction<DbT, ParamsT, InputsErrT, FeeRuleT, ChangeErrT, N>(
|
||||
wallet_db: &mut DbT,
|
||||
params: &ParamsT,
|
||||
spend_prover: &impl SpendProver,
|
||||
|
@ -707,7 +742,10 @@ fn create_proposed_transaction<DbT, ParamsT, InputsErrT, FeeRuleT, N>(
|
|||
StepOutput,
|
||||
(TransparentAddress, OutPoint),
|
||||
>,
|
||||
) -> Result<StepResult<<DbT as WalletRead>::AccountId>, ErrorT<DbT, InputsErrT, FeeRuleT>>
|
||||
) -> Result<
|
||||
StepResult<<DbT as WalletRead>::AccountId>,
|
||||
CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>,
|
||||
>
|
||||
where
|
||||
DbT: WalletWrite + WalletCommitmentTrees,
|
||||
ParamsT: consensus::Parameters + Clone,
|
||||
|
@ -749,53 +787,54 @@ where
|
|||
return Err(Error::ProposalNotSupported);
|
||||
}
|
||||
|
||||
let (sapling_anchor, sapling_inputs) =
|
||||
if proposal_step.involves(PoolType::Shielded(ShieldedProtocol::Sapling)) {
|
||||
proposal_step.shielded_inputs().map_or_else(
|
||||
|| Ok((Some(sapling::Anchor::empty_tree()), vec![])),
|
||||
|inputs| {
|
||||
wallet_db.with_sapling_tree_mut::<_, _, Error<_, _, _, _>>(|sapling_tree| {
|
||||
let anchor = sapling_tree
|
||||
.root_at_checkpoint_id(&inputs.anchor_height())?
|
||||
.ok_or(ProposalError::AnchorNotFound(inputs.anchor_height()))?
|
||||
.into();
|
||||
let (sapling_anchor, sapling_inputs) = if proposal_step
|
||||
.involves(PoolType::Shielded(ShieldedProtocol::Sapling))
|
||||
{
|
||||
proposal_step.shielded_inputs().map_or_else(
|
||||
|| Ok((Some(sapling::Anchor::empty_tree()), vec![])),
|
||||
|inputs| {
|
||||
wallet_db.with_sapling_tree_mut::<_, _, Error<_, _, _, _, _, _>>(|sapling_tree| {
|
||||
let anchor = sapling_tree
|
||||
.root_at_checkpoint_id(&inputs.anchor_height())?
|
||||
.ok_or(ProposalError::AnchorNotFound(inputs.anchor_height()))?
|
||||
.into();
|
||||
|
||||
let sapling_inputs = inputs
|
||||
.notes()
|
||||
.iter()
|
||||
.filter_map(|selected| match selected.note() {
|
||||
Note::Sapling(note) => {
|
||||
let key = match selected.spending_key_scope() {
|
||||
Scope::External => usk.sapling().clone(),
|
||||
Scope::Internal => usk.sapling().derive_internal(),
|
||||
};
|
||||
let sapling_inputs = inputs
|
||||
.notes()
|
||||
.iter()
|
||||
.filter_map(|selected| match selected.note() {
|
||||
Note::Sapling(note) => {
|
||||
let key = match selected.spending_key_scope() {
|
||||
Scope::External => usk.sapling().clone(),
|
||||
Scope::Internal => usk.sapling().derive_internal(),
|
||||
};
|
||||
|
||||
sapling_tree
|
||||
.witness_at_checkpoint_id_caching(
|
||||
selected.note_commitment_tree_position(),
|
||||
&inputs.anchor_height(),
|
||||
)
|
||||
.and_then(|witness| {
|
||||
witness.ok_or(ShardTreeError::Query(
|
||||
QueryError::CheckpointPruned,
|
||||
))
|
||||
})
|
||||
.map(|merkle_path| Some((key, note, merkle_path)))
|
||||
.map_err(Error::from)
|
||||
.transpose()
|
||||
}
|
||||
#[cfg(feature = "orchard")]
|
||||
Note::Orchard(_) => None,
|
||||
})
|
||||
.collect::<Result<Vec<_>, Error<_, _, _, _>>>()?;
|
||||
sapling_tree
|
||||
.witness_at_checkpoint_id_caching(
|
||||
selected.note_commitment_tree_position(),
|
||||
&inputs.anchor_height(),
|
||||
)
|
||||
.and_then(|witness| {
|
||||
witness.ok_or(ShardTreeError::Query(
|
||||
QueryError::CheckpointPruned,
|
||||
))
|
||||
})
|
||||
.map(|merkle_path| Some((key, note, merkle_path)))
|
||||
.map_err(Error::from)
|
||||
.transpose()
|
||||
}
|
||||
#[cfg(feature = "orchard")]
|
||||
Note::Orchard(_) => None,
|
||||
})
|
||||
.collect::<Result<Vec<_>, Error<_, _, _, _, _, _>>>()?;
|
||||
|
||||
Ok((Some(anchor), sapling_inputs))
|
||||
})
|
||||
},
|
||||
)?
|
||||
} else {
|
||||
(None, vec![])
|
||||
};
|
||||
Ok((Some(anchor), sapling_inputs))
|
||||
})
|
||||
},
|
||||
)?
|
||||
} else {
|
||||
(None, vec![])
|
||||
};
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
let (orchard_anchor, orchard_inputs) = if proposal_step
|
||||
|
@ -804,7 +843,7 @@ where
|
|||
proposal_step.shielded_inputs().map_or_else(
|
||||
|| Ok((Some(orchard::Anchor::empty_tree()), vec![])),
|
||||
|inputs| {
|
||||
wallet_db.with_orchard_tree_mut::<_, _, Error<_, _, _, _>>(|orchard_tree| {
|
||||
wallet_db.with_orchard_tree_mut::<_, _, Error<_, _, _, _, _, _>>(|orchard_tree| {
|
||||
let anchor = orchard_tree
|
||||
.root_at_checkpoint_id(&inputs.anchor_height())?
|
||||
.ok_or(ProposalError::AnchorNotFound(inputs.anchor_height()))?
|
||||
|
@ -829,7 +868,7 @@ where
|
|||
.transpose(),
|
||||
Note::Sapling(_) => None,
|
||||
})
|
||||
.collect::<Result<Vec<_>, Error<_, _, _, _>>>()?;
|
||||
.collect::<Result<Vec<_>, Error<_, _, _, _, _, _>>>()?;
|
||||
|
||||
Ok((Some(anchor), orchard_inputs))
|
||||
})
|
||||
|
@ -872,7 +911,7 @@ where
|
|||
#[cfg(feature = "transparent-inputs")]
|
||||
let mut metadata_from_address = |addr: TransparentAddress| -> Result<
|
||||
TransparentAddressMetadata,
|
||||
ErrorT<DbT, InputsErrT, FeeRuleT>,
|
||||
CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>,
|
||||
> {
|
||||
match cache.get(&addr) {
|
||||
Some(result) => Ok(result.clone()),
|
||||
|
@ -898,22 +937,23 @@ where
|
|||
#[cfg(feature = "transparent-inputs")]
|
||||
let utxos_spent = {
|
||||
let mut utxos_spent: Vec<OutPoint> = vec![];
|
||||
let add_transparent_input = |builder: &mut Builder<_, _>,
|
||||
utxos_spent: &mut Vec<_>,
|
||||
address_metadata: &TransparentAddressMetadata,
|
||||
outpoint: OutPoint,
|
||||
txout: TxOut|
|
||||
-> Result<(), ErrorT<DbT, InputsErrT, FeeRuleT>> {
|
||||
let secret_key = usk
|
||||
.transparent()
|
||||
.derive_secret_key(address_metadata.scope(), address_metadata.address_index())
|
||||
.expect("spending key derivation should not fail");
|
||||
let add_transparent_input =
|
||||
|builder: &mut Builder<_, _>,
|
||||
utxos_spent: &mut Vec<_>,
|
||||
address_metadata: &TransparentAddressMetadata,
|
||||
outpoint: OutPoint,
|
||||
txout: TxOut|
|
||||
-> Result<(), CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>> {
|
||||
let secret_key = usk
|
||||
.transparent()
|
||||
.derive_secret_key(address_metadata.scope(), address_metadata.address_index())
|
||||
.expect("spending key derivation should not fail");
|
||||
|
||||
utxos_spent.push(outpoint.clone());
|
||||
builder.add_transparent_input(secret_key, outpoint, txout)?;
|
||||
utxos_spent.push(outpoint.clone());
|
||||
builder.add_transparent_input(secret_key, outpoint, txout)?;
|
||||
|
||||
Ok(())
|
||||
};
|
||||
Ok(())
|
||||
};
|
||||
|
||||
for utxo in proposal_step.transparent_inputs() {
|
||||
add_transparent_input(
|
||||
|
@ -1031,7 +1071,10 @@ where
|
|||
let add_sapling_output = |builder: &mut Builder<_, _>,
|
||||
sapling_output_meta: &mut Vec<_>,
|
||||
to: sapling::PaymentAddress|
|
||||
-> Result<(), ErrorT<DbT, InputsErrT, FeeRuleT>> {
|
||||
-> Result<
|
||||
(),
|
||||
CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>,
|
||||
> {
|
||||
let memo = payment.memo().map_or_else(MemoBytes::empty, |m| m.clone());
|
||||
builder.add_sapling_output(sapling_external_ovk, to, payment.amount(), memo.clone())?;
|
||||
sapling_output_meta.push((
|
||||
|
@ -1043,50 +1086,52 @@ where
|
|||
};
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
let add_orchard_output = |builder: &mut Builder<_, _>,
|
||||
orchard_output_meta: &mut Vec<_>,
|
||||
to: orchard::Address|
|
||||
-> Result<(), ErrorT<DbT, InputsErrT, FeeRuleT>> {
|
||||
let memo = payment.memo().map_or_else(MemoBytes::empty, |m| m.clone());
|
||||
builder.add_orchard_output(
|
||||
orchard_external_ovk.clone(),
|
||||
to,
|
||||
payment.amount().into(),
|
||||
memo.clone(),
|
||||
)?;
|
||||
orchard_output_meta.push((
|
||||
Recipient::External(recipient_address.clone(), PoolType::ORCHARD),
|
||||
payment.amount(),
|
||||
Some(memo),
|
||||
));
|
||||
Ok(())
|
||||
};
|
||||
let add_orchard_output =
|
||||
|builder: &mut Builder<_, _>,
|
||||
orchard_output_meta: &mut Vec<_>,
|
||||
to: orchard::Address|
|
||||
-> Result<(), CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>> {
|
||||
let memo = payment.memo().map_or_else(MemoBytes::empty, |m| m.clone());
|
||||
builder.add_orchard_output(
|
||||
orchard_external_ovk.clone(),
|
||||
to,
|
||||
payment.amount().into(),
|
||||
memo.clone(),
|
||||
)?;
|
||||
orchard_output_meta.push((
|
||||
Recipient::External(recipient_address.clone(), PoolType::ORCHARD),
|
||||
payment.amount(),
|
||||
Some(memo),
|
||||
));
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let add_transparent_output = |builder: &mut Builder<_, _>,
|
||||
transparent_output_meta: &mut Vec<_>,
|
||||
to: TransparentAddress|
|
||||
-> Result<(), ErrorT<DbT, InputsErrT, FeeRuleT>> {
|
||||
// Always reject sending to one of our known ephemeral addresses.
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
if wallet_db
|
||||
.find_account_for_ephemeral_address(&to)
|
||||
.map_err(Error::DataSource)?
|
||||
.is_some()
|
||||
{
|
||||
return Err(Error::PaysEphemeralTransparentAddress(to.encode(params)));
|
||||
}
|
||||
if payment.memo().is_some() {
|
||||
return Err(Error::MemoForbidden);
|
||||
}
|
||||
builder.add_transparent_output(&to, payment.amount())?;
|
||||
transparent_output_meta.push((
|
||||
Recipient::External(recipient_address.clone(), PoolType::TRANSPARENT),
|
||||
to,
|
||||
payment.amount(),
|
||||
StepOutputIndex::Payment(payment_index),
|
||||
));
|
||||
Ok(())
|
||||
};
|
||||
let add_transparent_output =
|
||||
|builder: &mut Builder<_, _>,
|
||||
transparent_output_meta: &mut Vec<_>,
|
||||
to: TransparentAddress|
|
||||
-> Result<(), CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>> {
|
||||
// Always reject sending to one of our known ephemeral addresses.
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
if wallet_db
|
||||
.find_account_for_ephemeral_address(&to)
|
||||
.map_err(Error::DataSource)?
|
||||
.is_some()
|
||||
{
|
||||
return Err(Error::PaysEphemeralTransparentAddress(to.encode(params)));
|
||||
}
|
||||
if payment.memo().is_some() {
|
||||
return Err(Error::MemoForbidden);
|
||||
}
|
||||
builder.add_transparent_output(&to, payment.amount())?;
|
||||
transparent_output_meta.push((
|
||||
Recipient::External(recipient_address.clone(), PoolType::TRANSPARENT),
|
||||
to,
|
||||
payment.amount(),
|
||||
StepOutputIndex::Payment(payment_index),
|
||||
));
|
||||
Ok(())
|
||||
};
|
||||
|
||||
match recipient_address
|
||||
.clone()
|
||||
|
@ -1371,36 +1416,33 @@ where
|
|||
#[cfg(feature = "transparent-inputs")]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn shield_transparent_funds<DbT, ParamsT, InputsT>(
|
||||
pub fn shield_transparent_funds<DbT, ParamsT, InputsT, ChangeT>(
|
||||
wallet_db: &mut DbT,
|
||||
params: &ParamsT,
|
||||
spend_prover: &impl SpendProver,
|
||||
output_prover: &impl OutputProver,
|
||||
input_selector: &InputsT,
|
||||
change_strategy: &ChangeT,
|
||||
shielding_threshold: NonNegativeAmount,
|
||||
usk: &UnifiedSpendingKey,
|
||||
from_addrs: &[TransparentAddress],
|
||||
to_account: <DbT as InputSource>::AccountId,
|
||||
min_confirmations: u32,
|
||||
) -> Result<
|
||||
NonEmpty<TxId>,
|
||||
Error<
|
||||
<DbT as WalletRead>::Error,
|
||||
<DbT as WalletCommitmentTrees>::Error,
|
||||
InputsT::Error,
|
||||
<InputsT::FeeRule as FeeRule>::Error,
|
||||
>,
|
||||
>
|
||||
) -> Result<NonEmpty<TxId>, ShieldErrT<DbT, InputsT, ChangeT>>
|
||||
where
|
||||
ParamsT: consensus::Parameters,
|
||||
DbT: WalletWrite + WalletCommitmentTrees + InputSource<Error = <DbT as WalletRead>::Error>,
|
||||
InputsT: ShieldingSelector<InputSource = DbT>,
|
||||
ChangeT: ChangeStrategy<MetaSource = DbT>,
|
||||
{
|
||||
let proposal = propose_shielding(
|
||||
wallet_db,
|
||||
params,
|
||||
input_selector,
|
||||
change_strategy,
|
||||
shielding_threshold,
|
||||
from_addrs,
|
||||
to_account,
|
||||
min_confirmations,
|
||||
)?;
|
||||
|
||||
|
|
|
@ -11,19 +11,16 @@ use nonempty::NonEmpty;
|
|||
use zcash_address::ConversionError;
|
||||
use zcash_primitives::{
|
||||
consensus::{self, BlockHeight},
|
||||
transaction::{
|
||||
components::{
|
||||
amount::{BalanceError, NonNegativeAmount},
|
||||
TxOut,
|
||||
},
|
||||
fees::FeeRule,
|
||||
transaction::components::{
|
||||
amount::{BalanceError, NonNegativeAmount},
|
||||
TxOut,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
address::{Address, UnifiedAddress},
|
||||
data_api::{InputSource, SimpleNoteRetention, SpendableNotes},
|
||||
fees::{sapling, ChangeError, ChangeStrategy, DustOutputPolicy},
|
||||
fees::{sapling, ChangeError, ChangeStrategy},
|
||||
proposal::{Proposal, ProposalError, ShieldedInputs},
|
||||
wallet::WalletTransparentOutput,
|
||||
zip321::TransactionRequest,
|
||||
|
@ -47,11 +44,13 @@ use crate::fees::orchard as orchard_fees;
|
|||
|
||||
/// The type of errors that may be produced in input selection.
|
||||
#[derive(Debug)]
|
||||
pub enum InputSelectorError<DbErrT, SelectorErrT> {
|
||||
pub enum InputSelectorError<DbErrT, SelectorErrT, ChangeErrT, N> {
|
||||
/// An error occurred accessing the underlying data store.
|
||||
DataSource(DbErrT),
|
||||
/// An error occurred specific to the provided input selector's selection rules.
|
||||
Selection(SelectorErrT),
|
||||
/// An error occurred in computing the change or fee for the proposed transfer.
|
||||
Change(ChangeError<ChangeErrT, N>),
|
||||
/// Input selection attempted to generate an invalid transaction proposal.
|
||||
Proposal(ProposalError),
|
||||
/// An error occurred parsing the address from a payment request.
|
||||
|
@ -67,13 +66,9 @@ pub enum InputSelectorError<DbErrT, SelectorErrT> {
|
|||
SyncRequired,
|
||||
}
|
||||
|
||||
impl<E, S> From<ConversionError<&'static str>> for InputSelectorError<E, S> {
|
||||
fn from(value: ConversionError<&'static str>) -> Self {
|
||||
InputSelectorError::Address(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<DE: fmt::Display, SE: fmt::Display> fmt::Display for InputSelectorError<DE, SE> {
|
||||
impl<DE: fmt::Display, SE: fmt::Display, CE: fmt::Display, N: fmt::Display> fmt::Display
|
||||
for InputSelectorError<DE, SE, CE, N>
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match &self {
|
||||
InputSelectorError::DataSource(e) => {
|
||||
|
@ -86,6 +81,11 @@ impl<DE: fmt::Display, SE: fmt::Display> fmt::Display for InputSelectorError<DE,
|
|||
InputSelectorError::Selection(e) => {
|
||||
write!(f, "Note selection encountered the following error: {}", e)
|
||||
}
|
||||
InputSelectorError::Change(e) => write!(
|
||||
f,
|
||||
"Proposal generation failed due to an error in computing change or transaction fees: {}",
|
||||
e
|
||||
),
|
||||
InputSelectorError::Proposal(e) => {
|
||||
write!(
|
||||
f,
|
||||
|
@ -116,21 +116,36 @@ impl<DE: fmt::Display, SE: fmt::Display> fmt::Display for InputSelectorError<DE,
|
|||
}
|
||||
}
|
||||
|
||||
impl<DE, SE> error::Error for InputSelectorError<DE, SE>
|
||||
impl<DE, SE, CE, N> error::Error for InputSelectorError<DE, SE, CE, N>
|
||||
where
|
||||
DE: Debug + Display + error::Error + 'static,
|
||||
SE: Debug + Display + error::Error + 'static,
|
||||
CE: Debug + Display + error::Error + 'static,
|
||||
N: Debug + Display + 'static,
|
||||
{
|
||||
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
||||
match &self {
|
||||
Self::DataSource(e) => Some(e),
|
||||
Self::Selection(e) => Some(e),
|
||||
Self::Change(e) => Some(e),
|
||||
Self::Proposal(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, S, F, N> From<ConversionError<&'static str>> for InputSelectorError<E, S, F, N> {
|
||||
fn from(value: ConversionError<&'static str>) -> Self {
|
||||
InputSelectorError::Address(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, S, C, N> From<ChangeError<C, N>> for InputSelectorError<E, S, C, N> {
|
||||
fn from(err: ChangeError<C, N>) -> Self {
|
||||
InputSelectorError::Change(err)
|
||||
}
|
||||
}
|
||||
|
||||
/// A strategy for selecting transaction inputs and proposing transaction outputs.
|
||||
///
|
||||
/// Proposals should include only economically useful inputs, as determined by `Self::FeeRule`;
|
||||
|
@ -139,14 +154,13 @@ where
|
|||
pub trait InputSelector {
|
||||
/// The type of errors that may be generated in input selection
|
||||
type Error;
|
||||
/// The type of data source that the input selector expects to access to obtain input Sapling
|
||||
/// notes. This associated type permits input selectors that may use specialized knowledge of
|
||||
/// the internals of a particular backing data store, if the generic API of
|
||||
/// `InputSource` does not provide sufficiently fine-grained operations for a particular
|
||||
/// backing store to optimally perform input selection.
|
||||
|
||||
/// The type of data source that the input selector expects to access to obtain input notes.
|
||||
/// This associated type permits input selectors that may use specialized knowledge of the
|
||||
/// internals of a particular backing data store, if the generic API of `InputSource` does not
|
||||
/// provide sufficiently fine-grained operations for a particular backing store to optimally
|
||||
/// perform input selection.
|
||||
type InputSource: InputSource;
|
||||
/// The type of the fee rule that this input selector uses when computing fees.
|
||||
type FeeRule: FeeRule;
|
||||
|
||||
/// Performs input selection and returns a proposal for transaction construction including
|
||||
/// change and fee outputs.
|
||||
|
@ -163,7 +177,8 @@ pub trait InputSelector {
|
|||
/// If insufficient funds are available to satisfy the required outputs for the shielding
|
||||
/// request, this operation must fail and return [`InputSelectorError::InsufficientFunds`].
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn propose_transaction<ParamsT>(
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn propose_transaction<ParamsT, ChangeT>(
|
||||
&self,
|
||||
params: &ParamsT,
|
||||
wallet_db: &Self::InputSource,
|
||||
|
@ -171,12 +186,19 @@ pub trait InputSelector {
|
|||
anchor_height: BlockHeight,
|
||||
account: <Self::InputSource as InputSource>::AccountId,
|
||||
transaction_request: TransactionRequest,
|
||||
change_strategy: &ChangeT,
|
||||
) -> Result<
|
||||
Proposal<Self::FeeRule, <Self::InputSource as InputSource>::NoteRef>,
|
||||
InputSelectorError<<Self::InputSource as InputSource>::Error, Self::Error>,
|
||||
Proposal<<ChangeT as ChangeStrategy>::FeeRule, <Self::InputSource as InputSource>::NoteRef>,
|
||||
InputSelectorError<
|
||||
<Self::InputSource as InputSource>::Error,
|
||||
Self::Error,
|
||||
ChangeT::Error,
|
||||
<Self::InputSource as InputSource>::NoteRef,
|
||||
>,
|
||||
>
|
||||
where
|
||||
ParamsT: consensus::Parameters;
|
||||
ParamsT: consensus::Parameters,
|
||||
ChangeT: ChangeStrategy<MetaSource = Self::InputSource>;
|
||||
}
|
||||
|
||||
/// A strategy for selecting transaction inputs and proposing transaction outputs
|
||||
|
@ -192,8 +214,6 @@ pub trait ShieldingSelector {
|
|||
/// [`InputSource`] does not provide sufficiently fine-grained operations for a
|
||||
/// particular backing store to optimally perform input selection.
|
||||
type InputSource: InputSource;
|
||||
/// The type of the fee rule that this input selector uses when computing fees.
|
||||
type FeeRule: FeeRule;
|
||||
|
||||
/// Performs input selection and returns a proposal for the construction of a shielding
|
||||
/// transaction.
|
||||
|
@ -204,36 +224,43 @@ pub trait ShieldingSelector {
|
|||
/// outputs for the shielding request, this operation must fail and return
|
||||
/// [`InputSelectorError::InsufficientFunds`].
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn propose_shielding<ParamsT>(
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn propose_shielding<ParamsT, ChangeT>(
|
||||
&self,
|
||||
params: &ParamsT,
|
||||
wallet_db: &Self::InputSource,
|
||||
change_strategy: &ChangeT,
|
||||
shielding_threshold: NonNegativeAmount,
|
||||
source_addrs: &[TransparentAddress],
|
||||
to_account: <Self::InputSource as InputSource>::AccountId,
|
||||
target_height: BlockHeight,
|
||||
min_confirmations: u32,
|
||||
) -> Result<
|
||||
Proposal<Self::FeeRule, Infallible>,
|
||||
InputSelectorError<<Self::InputSource as InputSource>::Error, Self::Error>,
|
||||
Proposal<<ChangeT as ChangeStrategy>::FeeRule, Infallible>,
|
||||
InputSelectorError<
|
||||
<Self::InputSource as InputSource>::Error,
|
||||
Self::Error,
|
||||
ChangeT::Error,
|
||||
Infallible,
|
||||
>,
|
||||
>
|
||||
where
|
||||
ParamsT: consensus::Parameters;
|
||||
ParamsT: consensus::Parameters,
|
||||
ChangeT: ChangeStrategy<MetaSource = Self::InputSource>;
|
||||
}
|
||||
|
||||
/// Errors that can occur as a consequence of greedy input selection.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum GreedyInputSelectorError<ChangeStrategyErrT, NoteRefT> {
|
||||
pub enum GreedyInputSelectorError {
|
||||
/// An intermediate value overflowed or underflowed the valid monetary range.
|
||||
Balance(BalanceError),
|
||||
/// A unified address did not contain a supported receiver.
|
||||
UnsupportedAddress(Box<UnifiedAddress>),
|
||||
/// Support for transparent-source-only (TEX) addresses requires the transparent-inputs feature.
|
||||
UnsupportedTexAddress,
|
||||
/// An error was encountered in change selection.
|
||||
Change(ChangeError<ChangeStrategyErrT, NoteRefT>),
|
||||
}
|
||||
|
||||
impl<CE: fmt::Display, N: fmt::Display> fmt::Display for GreedyInputSelectorError<CE, N> {
|
||||
impl fmt::Display for GreedyInputSelectorError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match &self {
|
||||
GreedyInputSelectorError::Balance(e) => write!(
|
||||
|
@ -249,32 +276,20 @@ impl<CE: fmt::Display, N: fmt::Display> fmt::Display for GreedyInputSelectorErro
|
|||
GreedyInputSelectorError::UnsupportedTexAddress => {
|
||||
write!(f, "Support for transparent-source-only (TEX) addresses requires the transparent-inputs feature.")
|
||||
}
|
||||
GreedyInputSelectorError::Change(err) => {
|
||||
write!(f, "An error occurred computing change and fees: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<DbErrT, ChangeStrategyErrT, NoteRefT>
|
||||
From<GreedyInputSelectorError<ChangeStrategyErrT, NoteRefT>>
|
||||
for InputSelectorError<DbErrT, GreedyInputSelectorError<ChangeStrategyErrT, NoteRefT>>
|
||||
impl<DbErrT, ChangeErrT, N> From<GreedyInputSelectorError>
|
||||
for InputSelectorError<DbErrT, GreedyInputSelectorError, ChangeErrT, N>
|
||||
{
|
||||
fn from(err: GreedyInputSelectorError<ChangeStrategyErrT, NoteRefT>) -> Self {
|
||||
fn from(err: GreedyInputSelectorError) -> Self {
|
||||
InputSelectorError::Selection(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl<DbErrT, ChangeStrategyErrT, NoteRefT> From<ChangeError<ChangeStrategyErrT, NoteRefT>>
|
||||
for InputSelectorError<DbErrT, GreedyInputSelectorError<ChangeStrategyErrT, NoteRefT>>
|
||||
{
|
||||
fn from(err: ChangeError<ChangeStrategyErrT, NoteRefT>) -> Self {
|
||||
InputSelectorError::Selection(GreedyInputSelectorError::Change(err))
|
||||
}
|
||||
}
|
||||
|
||||
impl<DbErrT, ChangeStrategyErrT, NoteRefT> From<BalanceError>
|
||||
for InputSelectorError<DbErrT, GreedyInputSelectorError<ChangeStrategyErrT, NoteRefT>>
|
||||
impl<DbErrT, ChangeErrT, N> From<BalanceError>
|
||||
for InputSelectorError<DbErrT, GreedyInputSelectorError, ChangeErrT, N>
|
||||
{
|
||||
fn from(err: BalanceError) -> Self {
|
||||
InputSelectorError::Selection(GreedyInputSelectorError::Balance(err))
|
||||
|
@ -319,13 +334,11 @@ impl orchard_fees::OutputView for OrchardPayment {
|
|||
///
|
||||
/// This implementation performs input selection using methods available via the
|
||||
/// [`InputSource`] interface.
|
||||
pub struct GreedyInputSelector<DbT, ChangeT> {
|
||||
change_strategy: ChangeT,
|
||||
dust_output_policy: DustOutputPolicy,
|
||||
pub struct GreedyInputSelector<DbT> {
|
||||
_ds_type: PhantomData<DbT>,
|
||||
}
|
||||
|
||||
impl<DbT, ChangeT: ChangeStrategy> GreedyInputSelector<DbT, ChangeT> {
|
||||
impl<DbT> GreedyInputSelector<DbT> {
|
||||
/// Constructs a new greedy input selector that uses the provided change strategy to determine
|
||||
/// change values and fee amounts.
|
||||
///
|
||||
|
@ -335,27 +348,25 @@ impl<DbT, ChangeT: ChangeStrategy> GreedyInputSelector<DbT, ChangeT> {
|
|||
/// attempting to construct a transaction proposal that requires such an output.
|
||||
///
|
||||
/// [`EphemeralBalance::Output`]: crate::fees::EphemeralBalance::Output
|
||||
pub fn new(change_strategy: ChangeT, dust_output_policy: DustOutputPolicy) -> Self {
|
||||
pub fn new() -> Self {
|
||||
GreedyInputSelector {
|
||||
change_strategy,
|
||||
dust_output_policy,
|
||||
_ds_type: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<DbT, ChangeT> InputSelector for GreedyInputSelector<DbT, ChangeT>
|
||||
where
|
||||
DbT: InputSource,
|
||||
ChangeT: ChangeStrategy,
|
||||
ChangeT::FeeRule: Clone,
|
||||
{
|
||||
type Error = GreedyInputSelectorError<ChangeT::Error, DbT::NoteRef>;
|
||||
impl<DbT> Default for GreedyInputSelector<DbT> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<DbT: InputSource> InputSelector for GreedyInputSelector<DbT> {
|
||||
type Error = GreedyInputSelectorError;
|
||||
type InputSource = DbT;
|
||||
type FeeRule = ChangeT::FeeRule;
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn propose_transaction<ParamsT>(
|
||||
fn propose_transaction<ParamsT, ChangeT>(
|
||||
&self,
|
||||
params: &ParamsT,
|
||||
wallet_db: &Self::InputSource,
|
||||
|
@ -363,13 +374,15 @@ where
|
|||
anchor_height: BlockHeight,
|
||||
account: <DbT as InputSource>::AccountId,
|
||||
transaction_request: TransactionRequest,
|
||||
change_strategy: &ChangeT,
|
||||
) -> Result<
|
||||
Proposal<Self::FeeRule, DbT::NoteRef>,
|
||||
InputSelectorError<<DbT as InputSource>::Error, Self::Error>,
|
||||
Proposal<<ChangeT as ChangeStrategy>::FeeRule, DbT::NoteRef>,
|
||||
InputSelectorError<<DbT as InputSource>::Error, Self::Error, ChangeT::Error, DbT::NoteRef>,
|
||||
>
|
||||
where
|
||||
ParamsT: consensus::Parameters,
|
||||
Self::InputSource: InputSource,
|
||||
ChangeT: ChangeStrategy<MetaSource = DbT>,
|
||||
{
|
||||
let mut transparent_outputs = vec![];
|
||||
let mut sapling_outputs = vec![];
|
||||
|
@ -484,8 +497,8 @@ where
|
|||
// catching the `InsufficientFunds` error to obtain the required amount
|
||||
// given the provided change strategy. Ignore the change memo in order
|
||||
// to avoid adding a change output.
|
||||
let tr1_required_input_value =
|
||||
match self.change_strategy.compute_balance::<_, DbT::NoteRef>(
|
||||
let tr1_required_input_value = match change_strategy
|
||||
.compute_balance::<_, DbT::NoteRef>(
|
||||
params,
|
||||
target_height,
|
||||
&[] as &[WalletTransparentOutput],
|
||||
|
@ -493,17 +506,18 @@ where
|
|||
&sapling::EmptyBundleView,
|
||||
#[cfg(feature = "orchard")]
|
||||
&orchard_fees::EmptyBundleView,
|
||||
&self.dust_output_policy,
|
||||
Some(&EphemeralBalance::Input(NonNegativeAmount::ZERO)),
|
||||
None,
|
||||
) {
|
||||
Err(ChangeError::InsufficientFunds { required, .. }) => required,
|
||||
Ok(_) => NonNegativeAmount::ZERO, // shouldn't happen
|
||||
Err(other) => return Err(other.into()),
|
||||
};
|
||||
Err(ChangeError::InsufficientFunds { required, .. }) => required,
|
||||
Err(ChangeError::DustInputs { .. }) => unreachable!("no inputs were supplied"),
|
||||
Err(other) => return Err(InputSelectorError::Change(other)),
|
||||
Ok(_) => NonNegativeAmount::ZERO, // shouldn't happen
|
||||
};
|
||||
|
||||
// Now recompute to obtain the `TransactionBalance` and verify that it
|
||||
// fully accounts for the required fees.
|
||||
let tr1_balance = self.change_strategy.compute_balance::<_, DbT::NoteRef>(
|
||||
let tr1_balance = change_strategy.compute_balance::<_, DbT::NoteRef>(
|
||||
params,
|
||||
target_height,
|
||||
&[] as &[WalletTransparentOutput],
|
||||
|
@ -511,8 +525,8 @@ where
|
|||
&sapling::EmptyBundleView,
|
||||
#[cfg(feature = "orchard")]
|
||||
&orchard_fees::EmptyBundleView,
|
||||
&self.dust_output_policy,
|
||||
Some(&EphemeralBalance::Input(tr1_required_input_value)),
|
||||
None,
|
||||
)?;
|
||||
assert_eq!(tr1_balance.total(), tr1_balance.fee_required());
|
||||
|
||||
|
@ -573,9 +587,20 @@ where
|
|||
vec![]
|
||||
};
|
||||
|
||||
let selected_input_ids = sapling_inputs.iter().map(|(id, _)| id);
|
||||
#[cfg(feature = "orchard")]
|
||||
let selected_input_ids =
|
||||
selected_input_ids.chain(orchard_inputs.iter().map(|(id, _)| id));
|
||||
|
||||
let selected_input_ids = selected_input_ids.cloned().collect::<Vec<_>>();
|
||||
|
||||
let wallet_meta = change_strategy
|
||||
.fetch_wallet_meta(wallet_db, account, &selected_input_ids)
|
||||
.map_err(InputSelectorError::DataSource)?;
|
||||
|
||||
// In the ZIP 320 case, this is the balance for transaction 0, taking into account
|
||||
// the ephemeral output.
|
||||
let balance = self.change_strategy.compute_balance(
|
||||
let balance = change_strategy.compute_balance(
|
||||
params,
|
||||
target_height,
|
||||
&[] as &[WalletTransparentOutput],
|
||||
|
@ -591,8 +616,8 @@ where
|
|||
&orchard_inputs[..],
|
||||
&orchard_outputs[..],
|
||||
),
|
||||
&self.dust_output_policy,
|
||||
ephemeral_balance.as_ref(),
|
||||
Some(&wallet_meta),
|
||||
);
|
||||
|
||||
match balance {
|
||||
|
@ -681,7 +706,7 @@ where
|
|||
);
|
||||
|
||||
return Proposal::multi_step(
|
||||
self.change_strategy.fee_rule().clone(),
|
||||
change_strategy.fee_rule().clone(),
|
||||
target_height,
|
||||
NonEmpty::from_vec(steps).expect("steps is known to be nonempty"),
|
||||
)
|
||||
|
@ -694,7 +719,7 @@ where
|
|||
vec![],
|
||||
shielded_inputs,
|
||||
balance,
|
||||
self.change_strategy.fee_rule().clone(),
|
||||
(*change_strategy.fee_rule()).clone(),
|
||||
target_height,
|
||||
false,
|
||||
)
|
||||
|
@ -713,7 +738,7 @@ where
|
|||
Err(ChangeError::InsufficientFunds { required, .. }) => {
|
||||
amount_required = required;
|
||||
}
|
||||
Err(other) => return Err(other.into()),
|
||||
Err(other) => return Err(InputSelectorError::Change(other)),
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "orchard"))]
|
||||
|
@ -747,31 +772,28 @@ where
|
|||
}
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
impl<DbT, ChangeT> ShieldingSelector for GreedyInputSelector<DbT, ChangeT>
|
||||
where
|
||||
DbT: InputSource,
|
||||
ChangeT: ChangeStrategy,
|
||||
ChangeT::FeeRule: Clone,
|
||||
{
|
||||
type Error = GreedyInputSelectorError<ChangeT::Error, Infallible>;
|
||||
impl<DbT: InputSource> ShieldingSelector for GreedyInputSelector<DbT> {
|
||||
type Error = GreedyInputSelectorError;
|
||||
type InputSource = DbT;
|
||||
type FeeRule = ChangeT::FeeRule;
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn propose_shielding<ParamsT>(
|
||||
fn propose_shielding<ParamsT, ChangeT>(
|
||||
&self,
|
||||
params: &ParamsT,
|
||||
wallet_db: &Self::InputSource,
|
||||
change_strategy: &ChangeT,
|
||||
shielding_threshold: NonNegativeAmount,
|
||||
source_addrs: &[TransparentAddress],
|
||||
to_account: <Self::InputSource as InputSource>::AccountId,
|
||||
target_height: BlockHeight,
|
||||
min_confirmations: u32,
|
||||
) -> Result<
|
||||
Proposal<Self::FeeRule, Infallible>,
|
||||
InputSelectorError<<DbT as InputSource>::Error, Self::Error>,
|
||||
Proposal<<ChangeT as ChangeStrategy>::FeeRule, Infallible>,
|
||||
InputSelectorError<<DbT as InputSource>::Error, Self::Error, ChangeT::Error, Infallible>,
|
||||
>
|
||||
where
|
||||
ParamsT: consensus::Parameters,
|
||||
ChangeT: ChangeStrategy<MetaSource = Self::InputSource>,
|
||||
{
|
||||
let mut transparent_inputs: Vec<WalletTransparentOutput> = source_addrs
|
||||
.iter()
|
||||
|
@ -784,7 +806,11 @@ where
|
|||
.flat_map(|v| v.into_iter())
|
||||
.collect();
|
||||
|
||||
let trial_balance = self.change_strategy.compute_balance(
|
||||
let wallet_meta = change_strategy
|
||||
.fetch_wallet_meta(wallet_db, to_account, &[])
|
||||
.map_err(InputSelectorError::DataSource)?;
|
||||
|
||||
let trial_balance = change_strategy.compute_balance(
|
||||
params,
|
||||
target_height,
|
||||
&transparent_inputs,
|
||||
|
@ -792,8 +818,8 @@ where
|
|||
&sapling::EmptyBundleView,
|
||||
#[cfg(feature = "orchard")]
|
||||
&orchard_fees::EmptyBundleView,
|
||||
&self.dust_output_policy,
|
||||
None,
|
||||
Some(&wallet_meta),
|
||||
);
|
||||
|
||||
let balance = match trial_balance {
|
||||
|
@ -802,7 +828,7 @@ where
|
|||
let exclusions: BTreeSet<OutPoint> = transparent.into_iter().collect();
|
||||
transparent_inputs.retain(|i| !exclusions.contains(i.outpoint()));
|
||||
|
||||
self.change_strategy.compute_balance(
|
||||
change_strategy.compute_balance(
|
||||
params,
|
||||
target_height,
|
||||
&transparent_inputs,
|
||||
|
@ -810,13 +836,11 @@ where
|
|||
&sapling::EmptyBundleView,
|
||||
#[cfg(feature = "orchard")]
|
||||
&orchard_fees::EmptyBundleView,
|
||||
&self.dust_output_policy,
|
||||
None,
|
||||
Some(&wallet_meta),
|
||||
)?
|
||||
}
|
||||
Err(other) => {
|
||||
return Err(other.into());
|
||||
}
|
||||
Err(other) => return Err(InputSelectorError::Change(other)),
|
||||
};
|
||||
|
||||
if balance.total() >= shielding_threshold {
|
||||
|
@ -826,7 +850,7 @@ where
|
|||
transparent_inputs,
|
||||
None,
|
||||
balance,
|
||||
(*self.change_strategy.fee_rule()).clone(),
|
||||
(*change_strategy.fee_rule()).clone(),
|
||||
target_height,
|
||||
true,
|
||||
)
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
use std::fmt;
|
||||
use std::fmt::{self, Debug, Display};
|
||||
|
||||
use zcash_primitives::{
|
||||
consensus::{self, BlockHeight},
|
||||
memo::MemoBytes,
|
||||
transaction::{
|
||||
components::{
|
||||
amount::{BalanceError, NonNegativeAmount},
|
||||
OutPoint,
|
||||
},
|
||||
components::{amount::NonNegativeAmount, OutPoint},
|
||||
fees::{transparent, FeeRule},
|
||||
},
|
||||
};
|
||||
use zcash_protocol::{PoolType, ShieldedProtocol};
|
||||
|
||||
use crate::data_api::InputSource;
|
||||
|
||||
pub(crate) mod common;
|
||||
pub mod fixed;
|
||||
#[cfg(feature = "orchard")]
|
||||
|
@ -273,9 +272,16 @@ impl<CE: fmt::Display, N: fmt::Display> fmt::Display for ChangeError<CE, N> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<NoteRefT> From<BalanceError> for ChangeError<BalanceError, NoteRefT> {
|
||||
fn from(err: BalanceError) -> ChangeError<BalanceError, NoteRefT> {
|
||||
ChangeError::StrategyError(err)
|
||||
impl<E, N> std::error::Error for ChangeError<E, N>
|
||||
where
|
||||
E: Debug + Display + std::error::Error + 'static,
|
||||
N: Debug + Display + 'static,
|
||||
{
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match &self {
|
||||
ChangeError::StrategyError(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -368,13 +374,30 @@ impl EphemeralBalance {
|
|||
/// A trait that represents the ability to compute the suggested change and fees that must be paid
|
||||
/// by a transaction having a specified set of inputs and outputs.
|
||||
pub trait ChangeStrategy {
|
||||
type FeeRule: FeeRule;
|
||||
type FeeRule: FeeRule + Clone;
|
||||
type Error;
|
||||
|
||||
/// The type of metadata source that this change strategy requires in order to be able to
|
||||
/// retrieve required wallet metadata. If more capabilities are required of the backend than
|
||||
/// are exposed in the [`InputSource`] trait, the implementer of this trait should define their
|
||||
/// own trait that descends from [`InputSource`] and adds the required capabilities there, and
|
||||
/// then implement that trait for their desired database backend.
|
||||
type MetaSource: InputSource;
|
||||
type WalletMeta;
|
||||
|
||||
/// Returns the fee rule that this change strategy will respect when performing
|
||||
/// balance computations.
|
||||
fn fee_rule(&self) -> &Self::FeeRule;
|
||||
|
||||
/// Uses the provided metadata source to obtain the wallet metadata required for change
|
||||
/// creation determinations.
|
||||
fn fetch_wallet_meta(
|
||||
&self,
|
||||
meta_source: &Self::MetaSource,
|
||||
account: <Self::MetaSource as InputSource>::AccountId,
|
||||
exclude: &[<Self::MetaSource as InputSource>::NoteRef],
|
||||
) -> Result<Self::WalletMeta, <Self::MetaSource as InputSource>::Error>;
|
||||
|
||||
/// Computes the totals of inputs, suggested change amounts, and fees given the
|
||||
/// provided inputs and outputs being used to construct a transaction.
|
||||
///
|
||||
|
@ -393,7 +416,11 @@ pub trait ChangeStrategy {
|
|||
/// - `ephemeral_balance`: if the transaction is to be constructed with either an
|
||||
/// ephemeral transparent input or an ephemeral transparent output this argument
|
||||
/// may be used to provide the value of that input or output. The value of this
|
||||
/// output should be `None` in the case that there are no such items.
|
||||
/// argument should be `None` in the case that there are no such items.
|
||||
/// - `wallet_meta`: Additional wallet metadata that the change strategy may use
|
||||
/// in determining how to construct change outputs. This wallet metadata value
|
||||
/// should be computed excluding the inputs provided in the `transparent_inputs`,
|
||||
/// `sapling`, and `orchard` arguments.
|
||||
///
|
||||
/// [ZIP 320]: https://zips.z.cash/zip-0320
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
@ -405,8 +432,8 @@ pub trait ChangeStrategy {
|
|||
transparent_outputs: &[impl transparent::OutputView],
|
||||
sapling: &impl sapling::BundleView<NoteRefT>,
|
||||
#[cfg(feature = "orchard")] orchard: &impl orchard::BundleView<NoteRefT>,
|
||||
dust_output_policy: &DustOutputPolicy,
|
||||
ephemeral_balance: Option<&EphemeralBalance>,
|
||||
wallet_meta: Option<&Self::WalletMeta>,
|
||||
) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>>;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
//! Change strategies designed for use with a fixed fee.
|
||||
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use zcash_primitives::{
|
||||
consensus::{self, BlockHeight},
|
||||
memo::MemoBytes,
|
||||
|
@ -9,7 +11,7 @@ use zcash_primitives::{
|
|||
},
|
||||
};
|
||||
|
||||
use crate::ShieldedProtocol;
|
||||
use crate::{data_api::InputSource, ShieldedProtocol};
|
||||
|
||||
use super::{
|
||||
common::single_change_output_balance, sapling as sapling_fees, ChangeError, ChangeStrategy,
|
||||
|
@ -23,13 +25,15 @@ use super::orchard as orchard_fees;
|
|||
/// as the most current pool that avoids unnecessary pool-crossing (with a specified
|
||||
/// fallback when the transaction has no shielded inputs). Fee calculation is delegated
|
||||
/// to the provided fee rule.
|
||||
pub struct SingleOutputChangeStrategy {
|
||||
pub struct SingleOutputChangeStrategy<I> {
|
||||
fee_rule: FixedFeeRule,
|
||||
change_memo: Option<MemoBytes>,
|
||||
fallback_change_pool: ShieldedProtocol,
|
||||
dust_output_policy: DustOutputPolicy,
|
||||
meta_source: PhantomData<I>,
|
||||
}
|
||||
|
||||
impl SingleOutputChangeStrategy {
|
||||
impl<I> SingleOutputChangeStrategy<I> {
|
||||
/// Constructs a new [`SingleOutputChangeStrategy`] with the specified fee rule
|
||||
/// and change memo.
|
||||
///
|
||||
|
@ -39,23 +43,37 @@ impl SingleOutputChangeStrategy {
|
|||
fee_rule: FixedFeeRule,
|
||||
change_memo: Option<MemoBytes>,
|
||||
fallback_change_pool: ShieldedProtocol,
|
||||
dust_output_policy: DustOutputPolicy,
|
||||
) -> Self {
|
||||
Self {
|
||||
fee_rule,
|
||||
change_memo,
|
||||
fallback_change_pool,
|
||||
dust_output_policy,
|
||||
meta_source: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChangeStrategy for SingleOutputChangeStrategy {
|
||||
impl<I: InputSource> ChangeStrategy for SingleOutputChangeStrategy<I> {
|
||||
type FeeRule = FixedFeeRule;
|
||||
type Error = BalanceError;
|
||||
type MetaSource = I;
|
||||
type WalletMeta = ();
|
||||
|
||||
fn fee_rule(&self) -> &Self::FeeRule {
|
||||
&self.fee_rule
|
||||
}
|
||||
|
||||
fn fetch_wallet_meta(
|
||||
&self,
|
||||
_meta_source: &Self::MetaSource,
|
||||
_account: <Self::MetaSource as InputSource>::AccountId,
|
||||
_exclude: &[<Self::MetaSource as crate::data_api::InputSource>::NoteRef],
|
||||
) -> Result<Self::WalletMeta, <Self::MetaSource as crate::data_api::InputSource>::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn compute_balance<P: consensus::Parameters, NoteRefT: Clone>(
|
||||
&self,
|
||||
params: &P,
|
||||
|
@ -64,8 +82,8 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
|||
transparent_outputs: &[impl transparent::OutputView],
|
||||
sapling: &impl sapling_fees::BundleView<NoteRefT>,
|
||||
#[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
|
||||
dust_output_policy: &DustOutputPolicy,
|
||||
ephemeral_balance: Option<&EphemeralBalance>,
|
||||
_wallet_meta: Option<&Self::WalletMeta>,
|
||||
) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
|
||||
single_change_output_balance(
|
||||
params,
|
||||
|
@ -76,7 +94,7 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
|||
sapling,
|
||||
#[cfg(feature = "orchard")]
|
||||
orchard,
|
||||
dust_output_policy,
|
||||
&self.dust_output_policy,
|
||||
self.fee_rule.fixed_fee(),
|
||||
self.change_memo.as_ref(),
|
||||
self.fallback_change_pool,
|
||||
|
@ -99,7 +117,7 @@ mod tests {
|
|||
|
||||
use super::SingleOutputChangeStrategy;
|
||||
use crate::{
|
||||
data_api::wallet::input_selection::SaplingPayment,
|
||||
data_api::{testing::MockWalletDb, wallet::input_selection::SaplingPayment},
|
||||
fees::{
|
||||
tests::{TestSaplingInput, TestTransparentInput},
|
||||
ChangeError, ChangeStrategy, ChangeValue, DustOutputPolicy,
|
||||
|
@ -114,8 +132,12 @@ mod tests {
|
|||
fn change_without_dust() {
|
||||
#[allow(deprecated)]
|
||||
let fee_rule = FixedFeeRule::standard();
|
||||
let change_strategy =
|
||||
SingleOutputChangeStrategy::new(fee_rule, None, ShieldedProtocol::Sapling);
|
||||
let change_strategy = SingleOutputChangeStrategy::<MockWalletDb>::new(
|
||||
fee_rule,
|
||||
None,
|
||||
ShieldedProtocol::Sapling,
|
||||
DustOutputPolicy::default(),
|
||||
);
|
||||
|
||||
// spend a single Sapling note that is sufficient to pay the fee
|
||||
let result = change_strategy.compute_balance(
|
||||
|
@ -137,7 +159,7 @@ mod tests {
|
|||
),
|
||||
#[cfg(feature = "orchard")]
|
||||
&orchard_fees::EmptyBundleView,
|
||||
&DustOutputPolicy::default(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
|
@ -153,8 +175,12 @@ mod tests {
|
|||
fn dust_change() {
|
||||
#[allow(deprecated)]
|
||||
let fee_rule = FixedFeeRule::standard();
|
||||
let change_strategy =
|
||||
SingleOutputChangeStrategy::new(fee_rule, None, ShieldedProtocol::Sapling);
|
||||
let change_strategy = SingleOutputChangeStrategy::<MockWalletDb>::new(
|
||||
fee_rule,
|
||||
None,
|
||||
ShieldedProtocol::Sapling,
|
||||
DustOutputPolicy::default(),
|
||||
);
|
||||
|
||||
// spend a single Sapling note that is sufficient to pay the fee
|
||||
let result = change_strategy.compute_balance(
|
||||
|
@ -183,7 +209,7 @@ mod tests {
|
|||
),
|
||||
#[cfg(feature = "orchard")]
|
||||
&orchard_fees::EmptyBundleView,
|
||||
&DustOutputPolicy::default(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
//! Change strategies designed for use with a standard fee.
|
||||
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use zcash_primitives::{
|
||||
consensus::{self, BlockHeight},
|
||||
memo::MemoBytes,
|
||||
|
@ -14,7 +16,7 @@ use zcash_primitives::{
|
|||
},
|
||||
};
|
||||
|
||||
use crate::ShieldedProtocol;
|
||||
use crate::{data_api::InputSource, ShieldedProtocol};
|
||||
|
||||
use super::{
|
||||
fixed, sapling as sapling_fees, zip317, ChangeError, ChangeStrategy, DustOutputPolicy,
|
||||
|
@ -28,13 +30,15 @@ use super::orchard as orchard_fees;
|
|||
/// as the most current pool that avoids unnecessary pool-crossing (with a specified
|
||||
/// fallback when the transaction has no shielded inputs). Fee calculation is delegated
|
||||
/// to the provided fee rule.
|
||||
pub struct SingleOutputChangeStrategy {
|
||||
pub struct SingleOutputChangeStrategy<I> {
|
||||
fee_rule: StandardFeeRule,
|
||||
change_memo: Option<MemoBytes>,
|
||||
fallback_change_pool: ShieldedProtocol,
|
||||
dust_output_policy: DustOutputPolicy,
|
||||
meta_source: PhantomData<I>,
|
||||
}
|
||||
|
||||
impl SingleOutputChangeStrategy {
|
||||
impl<I> SingleOutputChangeStrategy<I> {
|
||||
/// Constructs a new [`SingleOutputChangeStrategy`] with the specified ZIP 317
|
||||
/// fee parameters.
|
||||
///
|
||||
|
@ -44,23 +48,37 @@ impl SingleOutputChangeStrategy {
|
|||
fee_rule: StandardFeeRule,
|
||||
change_memo: Option<MemoBytes>,
|
||||
fallback_change_pool: ShieldedProtocol,
|
||||
dust_output_policy: DustOutputPolicy,
|
||||
) -> Self {
|
||||
Self {
|
||||
fee_rule,
|
||||
change_memo,
|
||||
fallback_change_pool,
|
||||
dust_output_policy,
|
||||
meta_source: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChangeStrategy for SingleOutputChangeStrategy {
|
||||
impl<I: InputSource> ChangeStrategy for SingleOutputChangeStrategy<I> {
|
||||
type FeeRule = StandardFeeRule;
|
||||
type Error = Zip317FeeError;
|
||||
type MetaSource = I;
|
||||
type WalletMeta = ();
|
||||
|
||||
fn fee_rule(&self) -> &Self::FeeRule {
|
||||
&self.fee_rule
|
||||
}
|
||||
|
||||
fn fetch_wallet_meta(
|
||||
&self,
|
||||
_meta_source: &Self::MetaSource,
|
||||
_account: <Self::MetaSource as InputSource>::AccountId,
|
||||
_exclude: &[<Self::MetaSource as crate::data_api::InputSource>::NoteRef],
|
||||
) -> Result<Self::WalletMeta, <Self::MetaSource as crate::data_api::InputSource>::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn compute_balance<P: consensus::Parameters, NoteRefT: Clone>(
|
||||
&self,
|
||||
params: &P,
|
||||
|
@ -69,15 +87,16 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
|||
transparent_outputs: &[impl transparent::OutputView],
|
||||
sapling: &impl sapling_fees::BundleView<NoteRefT>,
|
||||
#[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
|
||||
dust_output_policy: &DustOutputPolicy,
|
||||
ephemeral_balance: Option<&EphemeralBalance>,
|
||||
wallet_meta: Option<&Self::WalletMeta>,
|
||||
) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
|
||||
#[allow(deprecated)]
|
||||
match self.fee_rule() {
|
||||
StandardFeeRule::PreZip313 => fixed::SingleOutputChangeStrategy::new(
|
||||
StandardFeeRule::PreZip313 => fixed::SingleOutputChangeStrategy::<I>::new(
|
||||
FixedFeeRule::non_standard(NonNegativeAmount::const_from_u64(10000)),
|
||||
self.change_memo.clone(),
|
||||
self.fallback_change_pool,
|
||||
self.dust_output_policy,
|
||||
)
|
||||
.compute_balance(
|
||||
params,
|
||||
|
@ -87,14 +106,15 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
|||
sapling,
|
||||
#[cfg(feature = "orchard")]
|
||||
orchard,
|
||||
dust_output_policy,
|
||||
ephemeral_balance,
|
||||
wallet_meta,
|
||||
)
|
||||
.map_err(|e| e.map(Zip317FeeError::Balance)),
|
||||
StandardFeeRule::Zip313 => fixed::SingleOutputChangeStrategy::new(
|
||||
StandardFeeRule::Zip313 => fixed::SingleOutputChangeStrategy::<I>::new(
|
||||
FixedFeeRule::non_standard(NonNegativeAmount::const_from_u64(1000)),
|
||||
self.change_memo.clone(),
|
||||
self.fallback_change_pool,
|
||||
self.dust_output_policy,
|
||||
)
|
||||
.compute_balance(
|
||||
params,
|
||||
|
@ -104,14 +124,15 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
|||
sapling,
|
||||
#[cfg(feature = "orchard")]
|
||||
orchard,
|
||||
dust_output_policy,
|
||||
ephemeral_balance,
|
||||
wallet_meta,
|
||||
)
|
||||
.map_err(|e| e.map(Zip317FeeError::Balance)),
|
||||
StandardFeeRule::Zip317 => zip317::SingleOutputChangeStrategy::new(
|
||||
StandardFeeRule::Zip317 => zip317::SingleOutputChangeStrategy::<I>::new(
|
||||
Zip317FeeRule::standard(),
|
||||
self.change_memo.clone(),
|
||||
self.fallback_change_pool,
|
||||
self.dust_output_policy,
|
||||
)
|
||||
.compute_balance(
|
||||
params,
|
||||
|
@ -121,8 +142,8 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
|||
sapling,
|
||||
#[cfg(feature = "orchard")]
|
||||
orchard,
|
||||
dust_output_policy,
|
||||
ephemeral_balance,
|
||||
wallet_meta,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
//! to ensure that inputs added to a transaction do not cause fees to rise by
|
||||
//! an amount greater than their value.
|
||||
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use zcash_primitives::{
|
||||
consensus::{self, BlockHeight},
|
||||
memo::MemoBytes,
|
||||
|
@ -13,7 +15,7 @@ use zcash_primitives::{
|
|||
},
|
||||
};
|
||||
|
||||
use crate::ShieldedProtocol;
|
||||
use crate::{data_api::InputSource, ShieldedProtocol};
|
||||
|
||||
use super::{
|
||||
common::single_change_output_balance, sapling as sapling_fees, ChangeError, ChangeStrategy,
|
||||
|
@ -27,13 +29,15 @@ use super::orchard as orchard_fees;
|
|||
/// as the most current pool that avoids unnecessary pool-crossing (with a specified
|
||||
/// fallback when the transaction has no shielded inputs). Fee calculation is delegated
|
||||
/// to the provided fee rule.
|
||||
pub struct SingleOutputChangeStrategy {
|
||||
pub struct SingleOutputChangeStrategy<I> {
|
||||
fee_rule: Zip317FeeRule,
|
||||
change_memo: Option<MemoBytes>,
|
||||
fallback_change_pool: ShieldedProtocol,
|
||||
dust_output_policy: DustOutputPolicy,
|
||||
meta_source: PhantomData<I>,
|
||||
}
|
||||
|
||||
impl SingleOutputChangeStrategy {
|
||||
impl<I> SingleOutputChangeStrategy<I> {
|
||||
/// Constructs a new [`SingleOutputChangeStrategy`] with the specified ZIP 317
|
||||
/// fee parameters and change memo.
|
||||
///
|
||||
|
@ -43,23 +47,37 @@ impl SingleOutputChangeStrategy {
|
|||
fee_rule: Zip317FeeRule,
|
||||
change_memo: Option<MemoBytes>,
|
||||
fallback_change_pool: ShieldedProtocol,
|
||||
dust_output_policy: DustOutputPolicy,
|
||||
) -> Self {
|
||||
Self {
|
||||
fee_rule,
|
||||
change_memo,
|
||||
fallback_change_pool,
|
||||
dust_output_policy,
|
||||
meta_source: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChangeStrategy for SingleOutputChangeStrategy {
|
||||
impl<I: InputSource> ChangeStrategy for SingleOutputChangeStrategy<I> {
|
||||
type FeeRule = Zip317FeeRule;
|
||||
type Error = Zip317FeeError;
|
||||
type MetaSource = I;
|
||||
type WalletMeta = ();
|
||||
|
||||
fn fee_rule(&self) -> &Self::FeeRule {
|
||||
&self.fee_rule
|
||||
}
|
||||
|
||||
fn fetch_wallet_meta(
|
||||
&self,
|
||||
_meta_source: &Self::MetaSource,
|
||||
_account: <Self::MetaSource as InputSource>::AccountId,
|
||||
_exclude: &[<Self::MetaSource as InputSource>::NoteRef],
|
||||
) -> Result<Self::WalletMeta, <Self::MetaSource as InputSource>::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn compute_balance<P: consensus::Parameters, NoteRefT: Clone>(
|
||||
&self,
|
||||
params: &P,
|
||||
|
@ -68,8 +86,8 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
|||
transparent_outputs: &[impl transparent::OutputView],
|
||||
sapling: &impl sapling_fees::BundleView<NoteRefT>,
|
||||
#[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
|
||||
dust_output_policy: &DustOutputPolicy,
|
||||
ephemeral_balance: Option<&EphemeralBalance>,
|
||||
_wallet_meta: Option<&Self::WalletMeta>,
|
||||
) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
|
||||
single_change_output_balance(
|
||||
params,
|
||||
|
@ -80,7 +98,7 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
|||
sapling,
|
||||
#[cfg(feature = "orchard")]
|
||||
orchard,
|
||||
dust_output_policy,
|
||||
&self.dust_output_policy,
|
||||
self.fee_rule.marginal_fee(),
|
||||
self.change_memo.as_ref(),
|
||||
self.fallback_change_pool,
|
||||
|
@ -106,7 +124,7 @@ mod tests {
|
|||
|
||||
use super::SingleOutputChangeStrategy;
|
||||
use crate::{
|
||||
data_api::wallet::input_selection::SaplingPayment,
|
||||
data_api::{testing::MockWalletDb, wallet::input_selection::SaplingPayment},
|
||||
fees::{
|
||||
tests::{TestSaplingInput, TestTransparentInput},
|
||||
ChangeError, ChangeStrategy, ChangeValue, DustAction, DustOutputPolicy,
|
||||
|
@ -122,10 +140,11 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn change_without_dust() {
|
||||
let change_strategy = SingleOutputChangeStrategy::new(
|
||||
let change_strategy = SingleOutputChangeStrategy::<MockWalletDb>::new(
|
||||
Zip317FeeRule::standard(),
|
||||
None,
|
||||
ShieldedProtocol::Sapling,
|
||||
DustOutputPolicy::default(),
|
||||
);
|
||||
|
||||
// spend a single Sapling note that is sufficient to pay the fee
|
||||
|
@ -148,7 +167,7 @@ mod tests {
|
|||
),
|
||||
#[cfg(feature = "orchard")]
|
||||
&orchard_fees::EmptyBundleView,
|
||||
&DustOutputPolicy::default(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
|
@ -163,10 +182,11 @@ mod tests {
|
|||
#[test]
|
||||
#[cfg(feature = "orchard")]
|
||||
fn cross_pool_change_without_dust() {
|
||||
let change_strategy = SingleOutputChangeStrategy::new(
|
||||
let change_strategy = SingleOutputChangeStrategy::<MockWalletDb>::new(
|
||||
Zip317FeeRule::standard(),
|
||||
None,
|
||||
ShieldedProtocol::Orchard,
|
||||
DustOutputPolicy::default(),
|
||||
);
|
||||
|
||||
// spend a single Sapling note that is sufficient to pay the fee
|
||||
|
@ -192,7 +212,7 @@ mod tests {
|
|||
30000,
|
||||
))][..],
|
||||
),
|
||||
&DustOutputPolicy::default(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
|
@ -206,22 +226,23 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn change_with_transparent_payments_implicitly_allowing_zero_change() {
|
||||
change_with_transparent_payments(&DustOutputPolicy::default())
|
||||
change_with_transparent_payments(DustOutputPolicy::default())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_with_transparent_payments_explicitly_allowing_zero_change() {
|
||||
change_with_transparent_payments(&DustOutputPolicy::new(
|
||||
change_with_transparent_payments(DustOutputPolicy::new(
|
||||
DustAction::AllowDustChange,
|
||||
Some(NonNegativeAmount::ZERO),
|
||||
))
|
||||
}
|
||||
|
||||
fn change_with_transparent_payments(dust_output_policy: &DustOutputPolicy) {
|
||||
let change_strategy = SingleOutputChangeStrategy::new(
|
||||
fn change_with_transparent_payments(dust_output_policy: DustOutputPolicy) {
|
||||
let change_strategy = SingleOutputChangeStrategy::<MockWalletDb>::new(
|
||||
Zip317FeeRule::standard(),
|
||||
None,
|
||||
ShieldedProtocol::Sapling,
|
||||
dust_output_policy,
|
||||
);
|
||||
|
||||
// spend a single Sapling note that is sufficient to pay the fee
|
||||
|
@ -245,7 +266,7 @@ mod tests {
|
|||
),
|
||||
#[cfg(feature = "orchard")]
|
||||
&orchard_fees::EmptyBundleView,
|
||||
dust_output_policy,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
|
@ -263,10 +284,11 @@ mod tests {
|
|||
use crate::fees::sapling as sapling_fees;
|
||||
use zcash_primitives::{legacy::TransparentAddress, transaction::components::OutPoint};
|
||||
|
||||
let change_strategy = SingleOutputChangeStrategy::new(
|
||||
let change_strategy = SingleOutputChangeStrategy::<MockWalletDb>::new(
|
||||
Zip317FeeRule::standard(),
|
||||
None,
|
||||
ShieldedProtocol::Sapling,
|
||||
DustOutputPolicy::default(),
|
||||
);
|
||||
|
||||
// Spend a single transparent UTXO that is exactly sufficient to pay the fee.
|
||||
|
@ -289,7 +311,7 @@ mod tests {
|
|||
&sapling_fees::EmptyBundleView,
|
||||
#[cfg(feature = "orchard")]
|
||||
&orchard_fees::EmptyBundleView,
|
||||
&DustOutputPolicy::default(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
|
@ -307,10 +329,11 @@ mod tests {
|
|||
use crate::fees::sapling as sapling_fees;
|
||||
use zcash_primitives::{legacy::TransparentAddress, transaction::components::OutPoint};
|
||||
|
||||
let change_strategy = SingleOutputChangeStrategy::new(
|
||||
let change_strategy = SingleOutputChangeStrategy::<MockWalletDb>::new(
|
||||
Zip317FeeRule::standard(),
|
||||
None,
|
||||
ShieldedProtocol::Sapling,
|
||||
DustOutputPolicy::default(),
|
||||
);
|
||||
|
||||
// Spend a single transparent UTXO that is sufficient to pay the fee.
|
||||
|
@ -333,7 +356,7 @@ mod tests {
|
|||
&sapling_fees::EmptyBundleView,
|
||||
#[cfg(feature = "orchard")]
|
||||
&orchard_fees::EmptyBundleView,
|
||||
&DustOutputPolicy::default(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
|
@ -351,10 +374,14 @@ mod tests {
|
|||
use crate::fees::sapling as sapling_fees;
|
||||
use zcash_primitives::{legacy::TransparentAddress, transaction::components::OutPoint};
|
||||
|
||||
let change_strategy = SingleOutputChangeStrategy::new(
|
||||
let change_strategy = SingleOutputChangeStrategy::<MockWalletDb>::new(
|
||||
Zip317FeeRule::standard(),
|
||||
None,
|
||||
ShieldedProtocol::Sapling,
|
||||
DustOutputPolicy::new(
|
||||
DustAction::AllowDustChange,
|
||||
Some(NonNegativeAmount::const_from_u64(1000)),
|
||||
),
|
||||
);
|
||||
|
||||
// Spend a single transparent UTXO that is sufficient to pay the fee.
|
||||
|
@ -380,10 +407,7 @@ mod tests {
|
|||
&sapling_fees::EmptyBundleView,
|
||||
#[cfg(feature = "orchard")]
|
||||
&orchard_fees::EmptyBundleView,
|
||||
&DustOutputPolicy::new(
|
||||
DustAction::AllowDustChange,
|
||||
Some(NonNegativeAmount::const_from_u64(1000)),
|
||||
),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
|
@ -397,22 +421,23 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn change_with_allowable_dust_implicitly_allowing_zero_change() {
|
||||
change_with_allowable_dust(&DustOutputPolicy::default())
|
||||
change_with_allowable_dust(DustOutputPolicy::default())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_with_allowable_dust_explicitly_allowing_zero_change() {
|
||||
change_with_allowable_dust(&DustOutputPolicy::new(
|
||||
change_with_allowable_dust(DustOutputPolicy::new(
|
||||
DustAction::AllowDustChange,
|
||||
Some(NonNegativeAmount::ZERO),
|
||||
))
|
||||
}
|
||||
|
||||
fn change_with_allowable_dust(dust_output_policy: &DustOutputPolicy) {
|
||||
let change_strategy = SingleOutputChangeStrategy::new(
|
||||
fn change_with_allowable_dust(dust_output_policy: DustOutputPolicy) {
|
||||
let change_strategy = SingleOutputChangeStrategy::<MockWalletDb>::new(
|
||||
Zip317FeeRule::standard(),
|
||||
None,
|
||||
ShieldedProtocol::Sapling,
|
||||
dust_output_policy,
|
||||
);
|
||||
|
||||
// Spend two Sapling notes, one of them dust. There is sufficient to
|
||||
|
@ -444,7 +469,7 @@ mod tests {
|
|||
),
|
||||
#[cfg(feature = "orchard")]
|
||||
&orchard_fees::EmptyBundleView,
|
||||
dust_output_policy,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
|
@ -458,10 +483,11 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn change_with_disallowed_dust() {
|
||||
let change_strategy = SingleOutputChangeStrategy::new(
|
||||
let change_strategy = SingleOutputChangeStrategy::<MockWalletDb>::new(
|
||||
Zip317FeeRule::standard(),
|
||||
None,
|
||||
ShieldedProtocol::Sapling,
|
||||
DustOutputPolicy::default(),
|
||||
);
|
||||
|
||||
// Attempt to spend three Sapling notes, one of them dust. Adding the third
|
||||
|
@ -495,7 +521,7 @@ mod tests {
|
|||
),
|
||||
#[cfg(feature = "orchard")]
|
||||
&orchard_fees::EmptyBundleView,
|
||||
&DustOutputPolicy::default(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
|
|
|
@ -126,7 +126,7 @@ impl Display for ProposalError {
|
|||
#[cfg(feature = "transparent-inputs")]
|
||||
ProposalError::EphemeralOutputsInvalid => write!(
|
||||
f,
|
||||
"The change strategy provided to input selection failed to correctly generate an ephemeral change output when needed for sending to a TEX address."
|
||||
"The proposal generator failed to correctly generate an ephemeral change output when needed for sending to a TEX address."
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -118,7 +118,7 @@ pub(crate) fn external_address_change_spends_detected_in_restore_from_seed<
|
|||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn zip317_spend<T: ShieldedPoolTester>() {
|
||||
zcash_client_backend::data_api::testing::pool::zip317_spend::<T>(
|
||||
zcash_client_backend::data_api::testing::pool::zip317_spend::<T, TestDbFactory>(
|
||||
TestDbFactory,
|
||||
BlockCache::new(),
|
||||
)
|
||||
|
|
|
@ -65,6 +65,7 @@
|
|||
//! - `memo` the shielded memo associated with the output, if any.
|
||||
|
||||
use incrementalmerkletree::{Marking, Retention};
|
||||
|
||||
use rusqlite::{self, named_params, params, OptionalExtension};
|
||||
use secrecy::{ExposeSecret, SecretVec};
|
||||
use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree};
|
||||
|
@ -78,6 +79,7 @@ use std::convert::TryFrom;
|
|||
use std::io::{self, Cursor};
|
||||
use std::num::NonZeroU32;
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use zcash_address::ZcashAddress;
|
||||
|
|
|
@ -1779,20 +1779,21 @@ pub(crate) mod tests {
|
|||
fee_rule,
|
||||
Some(change_memo.into()),
|
||||
OrchardPoolTester::SHIELDED_PROTOCOL,
|
||||
DustOutputPolicy::default(),
|
||||
);
|
||||
let input_selector =
|
||||
&GreedyInputSelector::new(change_strategy, DustOutputPolicy::default());
|
||||
let input_selector = GreedyInputSelector::new();
|
||||
|
||||
let proposal = st
|
||||
.propose_transfer(
|
||||
account.id(),
|
||||
input_selector,
|
||||
&input_selector,
|
||||
&change_strategy,
|
||||
request,
|
||||
NonZeroU32::new(10).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let create_proposed_result = st.create_proposed_transactions::<Infallible, _>(
|
||||
let create_proposed_result = st.create_proposed_transactions::<Infallible, _, Infallible>(
|
||||
account.usk(),
|
||||
OvkPolicy::Sender,
|
||||
&proposal,
|
||||
|
@ -1867,13 +1868,14 @@ pub(crate) mod tests {
|
|||
fee_rule,
|
||||
Some(change_memo.into()),
|
||||
OrchardPoolTester::SHIELDED_PROTOCOL,
|
||||
DustOutputPolicy::default(),
|
||||
);
|
||||
let input_selector =
|
||||
&GreedyInputSelector::new(change_strategy, DustOutputPolicy::default());
|
||||
let input_selector = GreedyInputSelector::new();
|
||||
|
||||
let proposal = st.propose_transfer(
|
||||
account.id(),
|
||||
input_selector,
|
||||
&input_selector,
|
||||
&change_strategy,
|
||||
request.clone(),
|
||||
NonZeroU32::new(10).unwrap(),
|
||||
);
|
||||
|
@ -1886,7 +1888,8 @@ pub(crate) mod tests {
|
|||
// Verify that it's now possible to create the proposal
|
||||
let proposal = st.propose_transfer(
|
||||
account.id(),
|
||||
input_selector,
|
||||
&input_selector,
|
||||
&change_strategy,
|
||||
request,
|
||||
NonZeroU32::new(10).unwrap(),
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue