zcash_client_backend: Move the `Proposal` types to a `proposal` module.
This separation is in preparation for modifying the `Proposal` type to wrap a vector of proposal steps.
This commit is contained in:
parent
6e0d9a9420
commit
1db3109cb4
|
@ -47,7 +47,6 @@ and this library adheres to Rust's notion of
|
|||
}`
|
||||
- `WalletSummary::next_sapling_subtree_index`
|
||||
- `wallet::propose_standard_transfer_to_address`
|
||||
- `wallet::input_selection::Proposal::{from_parts, shielded_inputs, payment_pools}`
|
||||
- `wallet::input_selection::ShieldedInputs`
|
||||
- `wallet::input_selection::ShieldingSelector` has been
|
||||
factored out from the `InputSelector` trait to separate out transparent
|
||||
|
@ -59,6 +58,10 @@ and this library adheres to Rust's notion of
|
|||
- `ReceivedNote`
|
||||
- `WalletSaplingOutput::recipient_key_scope`
|
||||
- `wallet::TransparentAddressMetadata` (which replaces `zcash_keys::address::AddressMetadata`).
|
||||
- `zcash_client_backend::zip321::TransactionRequest::total`
|
||||
- `zcash_client_backend::zip321::parse::Param::name`
|
||||
- `zcash_client_backend::proposal`
|
||||
- `Proposal::{from_parts, shielded_inputs, payment_pools}`
|
||||
- `zcash_client_backend::proto::`
|
||||
- `PROPOSAL_SER_V1`
|
||||
- `ProposalDecodingError`
|
||||
|
@ -66,12 +69,12 @@ and this library adheres to Rust's notion of
|
|||
- `impl Clone for zcash_client_backend::{
|
||||
zip321::{Payment, TransactionRequest, Zip321Error, parse::Param, parse::IndexedParam},
|
||||
wallet::{ReceivedSaplingNote, WalletTransparentOutput},
|
||||
wallet::input_selection::{Proposal, SaplingInputs},
|
||||
proposal::{Proposal, SaplingInputs},
|
||||
}`
|
||||
- `impl {PartialEq, Eq} for zcash_client_backend::{
|
||||
zip321::{Zip321Error, parse::Param, parse::IndexedParam},
|
||||
wallet::{ReceivedSaplingNote, WalletTransparentOutput},
|
||||
wallet::input_selection::{Proposal, SaplingInputs},
|
||||
proposal::{Proposal, SaplingInputs},
|
||||
}`
|
||||
- `zcash_client_backend::zip321
|
||||
` `TransactionRequest::{total, from_indexed}`
|
||||
|
@ -85,6 +88,8 @@ and this library adheres to Rust's notion of
|
|||
- `ScannedBlock::{sapling_tree_size, sapling_nullifier_map, sapling_commitments}`
|
||||
have been moved to `ScannedBlockSapling` and in that context are now
|
||||
named `{tree_size, nullifier_map, commitments}` respectively.
|
||||
- `zcash_client_backend::::wallet::input_selection::{Proposal, ShieldedInputs, ProposalError}`
|
||||
have been moved to `zcash_client_backend::proposal`.
|
||||
|
||||
### Changed
|
||||
- `zcash_client_backend::data_api`:
|
||||
|
@ -115,6 +120,7 @@ and this library adheres to Rust's notion of
|
|||
- A new variant `UnsupportedPoolType` has been added.
|
||||
- A new variant `NoSupportedReceivers` has been added.
|
||||
- A new variant `NoSpendingKey` has been added.
|
||||
- A new variant `Proposal` has been added.
|
||||
- Variant `ChildIndexOutOfRange` has been removed.
|
||||
- `wallet::shield_transparent_funds` no longer takes a `memo` argument;
|
||||
instead, memos to be associated with the shielded outputs should be
|
||||
|
@ -148,6 +154,7 @@ and this library adheres to Rust's notion of
|
|||
`min_confirmations` argument as `u32` instead of `NonZeroU32`.
|
||||
- The `wallet::input_selection::InputSelector::DataSource`
|
||||
associated type has been renamed to `InputSource`.
|
||||
- `wallet::input_selection::InputSelectorError` has added variant `Proposal`
|
||||
- The signature of `wallet:input_selection::InputSelector::propose_transaction`
|
||||
has been altered such that it longer takes `min_confirmations` as an
|
||||
argument, instead taking explicit `target_height` and `anchor_height`
|
||||
|
@ -175,12 +182,15 @@ and this library adheres to Rust's notion of
|
|||
- `wallet::create_proposed_transaction` now forces implementations to ignore
|
||||
the database identifiers for its contained notes by universally quantifying
|
||||
the `NoteRef` type parameter.
|
||||
- Arguments to `wallet::input_selection::Proposal::from_parts` have changed.
|
||||
- `wallet::input_selection::Proposal::min_anchor_height` has been removed in
|
||||
favor of storing this value in `SaplingInputs`.
|
||||
- `wallet::input_selection::GreedyInputSelector` now has relaxed requirements
|
||||
for its `InputSource` associated type.
|
||||
|
||||
- `zcash_client_backend::proposal`:
|
||||
- Arguments to `Proposal::from_parts` have changed.
|
||||
- `Proposal::min_anchor_height` has been removed in favor of storing this
|
||||
value in `SaplingInputs`.
|
||||
- `Proposal::sapling_inputs` has been replaced by `Proposal::shielded_inputs`
|
||||
|
||||
- `zcash_client_backend::fees`:
|
||||
- `ChangeStrategy::compute_balance` arguments have changed.
|
||||
- `ChangeValue` is now a struct. In addition to the existing change value, it
|
||||
|
@ -230,8 +240,7 @@ and this library adheres to Rust's notion of
|
|||
### Removed
|
||||
- `zcash_client_backend::wallet::ReceivedSaplingNote` has been replaced by
|
||||
`zcash_client_backend::ReceivedNote`.
|
||||
- `zcash_client_backend::wallet::input_selection::Proposal::sapling_inputs` has
|
||||
been replaced by `Proposal::shielded_inputs`
|
||||
- `zcash_client_backend::wallet::input_selection::Proposal`
|
||||
- `zcash_client_backend::data_api`
|
||||
- `zcash_client_backend::data_api::ScannedBlock::from_parts` has been made crate-private.
|
||||
- `zcash_client_backend::data_api::ScannedBlock::into_sapling_commitments` has been
|
||||
|
|
|
@ -14,6 +14,7 @@ use zcash_primitives::{
|
|||
};
|
||||
|
||||
use crate::data_api::wallet::input_selection::InputSelectorError;
|
||||
use crate::proposal::ProposalError;
|
||||
use crate::PoolType;
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
|
@ -33,6 +34,9 @@ pub enum Error<DataSourceError, CommitmentTreeError, SelectionError, FeeError> {
|
|||
/// An error in note selection
|
||||
NoteSelection(SelectionError),
|
||||
|
||||
/// An error in transaction proposal construction
|
||||
Proposal(ProposalError),
|
||||
|
||||
/// No account could be found corresponding to a provided spending key.
|
||||
KeyNotRecognized,
|
||||
|
||||
|
@ -100,6 +104,9 @@ where
|
|||
Error::NoteSelection(e) => {
|
||||
write!(f, "Note selection encountered the following error: {}", e)
|
||||
}
|
||||
Error::Proposal(e) => {
|
||||
write!(f, "Input selection attempted to construct an invalid proposal: {}", e)
|
||||
}
|
||||
Error::KeyNotRecognized => {
|
||||
write!(
|
||||
f,
|
||||
|
@ -148,6 +155,7 @@ where
|
|||
Error::DataSource(e) => Some(e),
|
||||
Error::CommitmentTree(e) => Some(e),
|
||||
Error::NoteSelection(e) => Some(e),
|
||||
Error::Proposal(e) => Some(e),
|
||||
Error::Builder(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
|
@ -171,6 +179,7 @@ impl<DE, CE, SE, FE> From<InputSelectorError<DE, SE>> for Error<DE, CE, SE, FE>
|
|||
match e {
|
||||
InputSelectorError::DataSource(e) => Error::DataSource(e),
|
||||
InputSelectorError::Selection(e) => Error::NoteSelection(e),
|
||||
InputSelectorError::Proposal(e) => Error::Proposal(e),
|
||||
InputSelectorError::InsufficientFunds {
|
||||
available,
|
||||
required,
|
||||
|
|
|
@ -20,12 +20,13 @@ use zcash_primitives::{
|
|||
use crate::{
|
||||
address::Address,
|
||||
data_api::{
|
||||
error::Error, wallet::input_selection::Proposal, DecryptedTransaction, SentTransaction,
|
||||
SentTransactionOutput, WalletCommitmentTrees, WalletRead, WalletWrite,
|
||||
error::Error, DecryptedTransaction, SentTransaction, SentTransactionOutput,
|
||||
WalletCommitmentTrees, WalletRead, WalletWrite,
|
||||
},
|
||||
decrypt_transaction,
|
||||
fees::{self, DustOutputPolicy},
|
||||
keys::UnifiedSpendingKey,
|
||||
proposal::Proposal,
|
||||
wallet::{Note, OvkPolicy, Recipient},
|
||||
zip321::{self, Payment},
|
||||
PoolType, ShieldedProtocol,
|
||||
|
@ -710,7 +711,7 @@ where
|
|||
} else {
|
||||
builder.add_transparent_output(to, payment.amount)?;
|
||||
}
|
||||
transparent_output_meta.push((*to, payment.amount));
|
||||
transparent_output_meta.push((to, payment.amount));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -805,7 +806,7 @@ where
|
|||
|
||||
SentTransactionOutput::from_parts(
|
||||
output_index,
|
||||
Recipient::Transparent(addr),
|
||||
Recipient::Transparent(*addr),
|
||||
value,
|
||||
None,
|
||||
None,
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
use core::marker::PhantomData;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
error,
|
||||
fmt::{self, Debug, Display},
|
||||
};
|
||||
|
||||
|
@ -23,7 +24,8 @@ use zcash_primitives::{
|
|||
use crate::{
|
||||
address::{Address, UnifiedAddress},
|
||||
data_api::InputSource,
|
||||
fees::{sapling, ChangeError, ChangeStrategy, DustOutputPolicy, TransactionBalance},
|
||||
fees::{sapling, ChangeError, ChangeStrategy, DustOutputPolicy},
|
||||
proposal::{Proposal, ProposalError, ShieldedInputs},
|
||||
wallet::{Note, ReceivedNote, WalletTransparentOutput},
|
||||
zip321::TransactionRequest,
|
||||
PoolType, ShieldedProtocol,
|
||||
|
@ -39,11 +41,14 @@ use {std::collections::BTreeSet, zcash_primitives::transaction::components::OutP
|
|||
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> {
|
||||
/// An error occurred accessing the underlying data store.
|
||||
DataSource(DbErrT),
|
||||
/// An error occurred specific to the provided input selector's selection rules.
|
||||
Selection(SelectorErrT),
|
||||
/// Input selection attempted to generate an invalid transaction proposal.
|
||||
Proposal(ProposalError),
|
||||
/// Insufficient funds were available to satisfy the payment request that inputs were being
|
||||
/// selected to attempt to satisfy.
|
||||
InsufficientFunds {
|
||||
|
@ -68,6 +73,13 @@ 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::Proposal(e) => {
|
||||
write!(
|
||||
f,
|
||||
"Input selection attempted to generate an invalid proposal: {}",
|
||||
e
|
||||
)
|
||||
}
|
||||
InputSelectorError::InsufficientFunds {
|
||||
available,
|
||||
required,
|
||||
|
@ -84,238 +96,21 @@ impl<DE: fmt::Display, SE: fmt::Display> fmt::Display for InputSelectorError<DE,
|
|||
}
|
||||
}
|
||||
|
||||
/// The inputs to be consumed and outputs to be produced in a proposed transaction.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct Proposal<FeeRuleT, NoteRef> {
|
||||
transaction_request: TransactionRequest,
|
||||
payment_pools: BTreeMap<usize, PoolType>,
|
||||
transparent_inputs: Vec<WalletTransparentOutput>,
|
||||
shielded_inputs: Option<ShieldedInputs<NoteRef>>,
|
||||
balance: TransactionBalance,
|
||||
fee_rule: FeeRuleT,
|
||||
min_target_height: BlockHeight,
|
||||
is_shielding: bool,
|
||||
}
|
||||
|
||||
/// Errors that can occur in construction of a [`Proposal`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ProposalError {
|
||||
/// The total output value of the transaction request is not a valid Zcash amount.
|
||||
RequestTotalInvalid,
|
||||
/// The total of transaction inputs overflows the valid range of Zcash values.
|
||||
Overflow,
|
||||
/// The input total and output total of the payment request are not equal to one another. The
|
||||
/// sum of transaction outputs, change, and fees is required to be exactly equal to the value
|
||||
/// of provided inputs.
|
||||
BalanceError {
|
||||
input_total: NonNegativeAmount,
|
||||
output_total: NonNegativeAmount,
|
||||
},
|
||||
/// The `is_shielding` flag may only be set to `true` under the following conditions:
|
||||
/// * The total of transparent inputs is nonzero
|
||||
/// * There exist no Sapling inputs
|
||||
/// * There provided transaction request is empty; i.e. the only output values specified
|
||||
/// are change and fee amounts.
|
||||
ShieldingInvalid,
|
||||
}
|
||||
|
||||
impl Display for ProposalError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ProposalError::RequestTotalInvalid => write!(
|
||||
f,
|
||||
"The total requested output value is not a valid Zcash amount."
|
||||
),
|
||||
ProposalError::Overflow => write!(
|
||||
f,
|
||||
"The total of transaction inputs overflows the valid range of Zcash values."
|
||||
),
|
||||
ProposalError::BalanceError {
|
||||
input_total,
|
||||
output_total,
|
||||
} => write!(
|
||||
f,
|
||||
"Balance error: the output total {} was not equal to the input total {}",
|
||||
u64::from(*output_total),
|
||||
u64::from(*input_total)
|
||||
),
|
||||
ProposalError::ShieldingInvalid => write!(
|
||||
f,
|
||||
"The proposal violates the rules for a shielding transaction."
|
||||
),
|
||||
impl<DE, SE> error::Error for InputSelectorError<DE, SE>
|
||||
where
|
||||
DE: Debug + Display + error::Error + 'static,
|
||||
SE: Debug + Display + error::Error + 'static,
|
||||
{
|
||||
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
||||
match &self {
|
||||
Self::DataSource(e) => Some(e),
|
||||
Self::Selection(e) => Some(e),
|
||||
Self::Proposal(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ProposalError {}
|
||||
|
||||
impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
|
||||
/// Constructs a validated [`Proposal`] from its constituent parts.
|
||||
///
|
||||
/// This operation validates the proposal for balance consistency and agreement between
|
||||
/// the `is_shielding` flag and the structure of the proposal.
|
||||
///
|
||||
/// Parameters:
|
||||
/// * `transaction_request`: The ZIP 321 transaction request describing the payments
|
||||
/// to be made.
|
||||
/// * `payment_pools`: A map from payment index to pool type.
|
||||
/// * `transparent_inputs`: The set of previous transparent outputs to be spent.
|
||||
/// * `shielded_inputs`: The sets of previous shielded outputs to be spent.
|
||||
/// * `balance`: The change outputs to be added the transaction and the fee to be paid.
|
||||
/// * `fee_rule`: The fee rule observed by the proposed transaction.
|
||||
/// * `min_target_height`: The minimum block height at which the transaction may be created.
|
||||
/// * `is_shielding`: A flag that identifies whether this is a wallet-internal shielding
|
||||
/// transaction.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn from_parts(
|
||||
transaction_request: TransactionRequest,
|
||||
payment_pools: BTreeMap<usize, PoolType>,
|
||||
transparent_inputs: Vec<WalletTransparentOutput>,
|
||||
shielded_inputs: Option<ShieldedInputs<NoteRef>>,
|
||||
balance: TransactionBalance,
|
||||
fee_rule: FeeRuleT,
|
||||
min_target_height: BlockHeight,
|
||||
is_shielding: bool,
|
||||
) -> Result<Self, ProposalError> {
|
||||
let transparent_input_total = transparent_inputs
|
||||
.iter()
|
||||
.map(|out| out.txout().value)
|
||||
.fold(Ok(NonNegativeAmount::ZERO), |acc, a| {
|
||||
(acc? + a).ok_or(ProposalError::Overflow)
|
||||
})?;
|
||||
let shielded_input_total = shielded_inputs
|
||||
.iter()
|
||||
.flat_map(|s_in| s_in.notes().iter())
|
||||
.map(|out| out.note().value())
|
||||
.fold(Some(NonNegativeAmount::ZERO), |acc, a| (acc? + a))
|
||||
.ok_or(ProposalError::Overflow)?;
|
||||
let input_total =
|
||||
(transparent_input_total + shielded_input_total).ok_or(ProposalError::Overflow)?;
|
||||
|
||||
let request_total = transaction_request
|
||||
.total()
|
||||
.map_err(|_| ProposalError::RequestTotalInvalid)?;
|
||||
let output_total = (request_total + balance.total()).ok_or(ProposalError::Overflow)?;
|
||||
|
||||
if is_shielding
|
||||
&& (transparent_input_total == NonNegativeAmount::ZERO
|
||||
|| shielded_input_total > NonNegativeAmount::ZERO
|
||||
|| request_total > NonNegativeAmount::ZERO)
|
||||
{
|
||||
return Err(ProposalError::ShieldingInvalid);
|
||||
}
|
||||
|
||||
if input_total == output_total {
|
||||
Ok(Self {
|
||||
transaction_request,
|
||||
payment_pools,
|
||||
transparent_inputs,
|
||||
shielded_inputs,
|
||||
balance,
|
||||
fee_rule,
|
||||
min_target_height,
|
||||
is_shielding,
|
||||
})
|
||||
} else {
|
||||
Err(ProposalError::BalanceError {
|
||||
input_total,
|
||||
output_total,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the transaction request that describes the payments to be made.
|
||||
pub fn transaction_request(&self) -> &TransactionRequest {
|
||||
&self.transaction_request
|
||||
}
|
||||
/// Returns the map from payment index to the pool that has been selected
|
||||
/// for the output that will fulfill that payment.
|
||||
pub fn payment_pools(&self) -> &BTreeMap<usize, PoolType> {
|
||||
&self.payment_pools
|
||||
}
|
||||
/// Returns the transparent inputs that have been selected to fund the transaction.
|
||||
pub fn transparent_inputs(&self) -> &[WalletTransparentOutput] {
|
||||
&self.transparent_inputs
|
||||
}
|
||||
/// Returns the Sapling inputs that have been selected to fund the transaction.
|
||||
pub fn shielded_inputs(&self) -> Option<&ShieldedInputs<NoteRef>> {
|
||||
self.shielded_inputs.as_ref()
|
||||
}
|
||||
/// Returns the change outputs to be added to the transaction and the fee to be paid.
|
||||
pub fn balance(&self) -> &TransactionBalance {
|
||||
&self.balance
|
||||
}
|
||||
/// Returns the fee rule to be used by the transaction builder.
|
||||
pub fn fee_rule(&self) -> &FeeRuleT {
|
||||
&self.fee_rule
|
||||
}
|
||||
/// Returns the target height for which the proposal was prepared.
|
||||
///
|
||||
/// The chain must contain at least this many blocks in order for the proposal to
|
||||
/// be executed.
|
||||
pub fn min_target_height(&self) -> BlockHeight {
|
||||
self.min_target_height
|
||||
}
|
||||
/// Returns a flag indicating whether or not the proposed transaction
|
||||
/// is exclusively wallet-internal (if it does not involve any external
|
||||
/// recipients).
|
||||
pub fn is_shielding(&self) -> bool {
|
||||
self.is_shielding
|
||||
}
|
||||
}
|
||||
|
||||
impl<FeeRuleT, NoteRef> Debug for Proposal<FeeRuleT, NoteRef> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Proposal")
|
||||
.field("transaction_request", &self.transaction_request)
|
||||
.field("transparent_inputs", &self.transparent_inputs)
|
||||
.field(
|
||||
"shielded_inputs",
|
||||
&self.shielded_inputs().map(|i| i.notes.len()),
|
||||
)
|
||||
.field(
|
||||
"anchor_height",
|
||||
&self.shielded_inputs().map(|i| i.anchor_height),
|
||||
)
|
||||
.field("balance", &self.balance)
|
||||
//.field("fee_rule", &self.fee_rule)
|
||||
.field("min_target_height", &self.min_target_height)
|
||||
.field("is_shielding", &self.is_shielding)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
/// The Sapling inputs to a proposed transaction.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct ShieldedInputs<NoteRef> {
|
||||
anchor_height: BlockHeight,
|
||||
notes: NonEmpty<ReceivedNote<NoteRef, Note>>,
|
||||
}
|
||||
|
||||
impl<NoteRef> ShieldedInputs<NoteRef> {
|
||||
/// Constructs a [`ShieldedInputs`] from its constituent parts.
|
||||
pub fn from_parts(
|
||||
anchor_height: BlockHeight,
|
||||
notes: NonEmpty<ReceivedNote<NoteRef, Note>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
anchor_height,
|
||||
notes,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the anchor height for Sapling inputs that should be used when constructing the
|
||||
/// proposed transaction.
|
||||
pub fn anchor_height(&self) -> BlockHeight {
|
||||
self.anchor_height
|
||||
}
|
||||
|
||||
/// Returns the list of Sapling notes to be used as inputs to the proposed transaction.
|
||||
pub fn notes(&self) -> &NonEmpty<ReceivedNote<NoteRef, Note>> {
|
||||
&self.notes
|
||||
}
|
||||
}
|
||||
|
||||
/// A strategy for selecting transaction inputs and proposing transaction outputs.
|
||||
///
|
||||
/// Proposals should include only economically useful inputs, as determined by `Self::FeeRule`;
|
||||
|
@ -638,21 +433,18 @@ where
|
|||
|
||||
match balance {
|
||||
Ok(balance) => {
|
||||
return Ok(Proposal {
|
||||
return Proposal::from_parts(
|
||||
transaction_request,
|
||||
payment_pools,
|
||||
transparent_inputs: vec![],
|
||||
shielded_inputs: NonEmpty::from_vec(shielded_inputs).map(|notes| {
|
||||
ShieldedInputs {
|
||||
anchor_height,
|
||||
notes,
|
||||
}
|
||||
}),
|
||||
vec![],
|
||||
NonEmpty::from_vec(shielded_inputs)
|
||||
.map(|notes| ShieldedInputs::from_parts(anchor_height, notes)),
|
||||
balance,
|
||||
fee_rule: (*self.change_strategy.fee_rule()).clone(),
|
||||
min_target_height: target_height,
|
||||
is_shielding: false,
|
||||
});
|
||||
(*self.change_strategy.fee_rule()).clone(),
|
||||
target_height,
|
||||
false,
|
||||
)
|
||||
.map_err(InputSelectorError::Proposal);
|
||||
}
|
||||
Err(ChangeError::DustInputs { mut sapling, .. }) => {
|
||||
exclude.append(&mut sapling);
|
||||
|
@ -790,16 +582,17 @@ where
|
|||
};
|
||||
|
||||
if balance.total() >= shielding_threshold {
|
||||
Ok(Proposal {
|
||||
transaction_request: TransactionRequest::empty(),
|
||||
payment_pools: BTreeMap::new(),
|
||||
Proposal::from_parts(
|
||||
TransactionRequest::empty(),
|
||||
BTreeMap::new(),
|
||||
transparent_inputs,
|
||||
shielded_inputs: None,
|
||||
None,
|
||||
balance,
|
||||
fee_rule: (*self.change_strategy.fee_rule()).clone(),
|
||||
min_target_height: target_height,
|
||||
is_shielding: true,
|
||||
})
|
||||
(*self.change_strategy.fee_rule()).clone(),
|
||||
target_height,
|
||||
true,
|
||||
)
|
||||
.map_err(InputSelectorError::Proposal)
|
||||
} else {
|
||||
Err(InputSelectorError::InsufficientFunds {
|
||||
available: balance.total(),
|
||||
|
|
|
@ -67,6 +67,7 @@ mod decrypt;
|
|||
pub use zcash_keys::encoding;
|
||||
pub mod fees;
|
||||
pub use zcash_keys::keys;
|
||||
pub mod proposal;
|
||||
pub mod proto;
|
||||
pub mod scan;
|
||||
pub mod scanning;
|
||||
|
|
|
@ -0,0 +1,248 @@
|
|||
use std::{
|
||||
collections::BTreeMap,
|
||||
fmt::{self, Debug, Display},
|
||||
};
|
||||
|
||||
use nonempty::NonEmpty;
|
||||
use zcash_primitives::{
|
||||
consensus::BlockHeight, transaction::components::amount::NonNegativeAmount,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
fees::TransactionBalance,
|
||||
wallet::{Note, ReceivedNote, WalletTransparentOutput},
|
||||
zip321::TransactionRequest,
|
||||
PoolType,
|
||||
};
|
||||
|
||||
/// Errors that can occur in construction of a [`Proposal`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ProposalError {
|
||||
/// The total output value of the transaction request is not a valid Zcash amount.
|
||||
RequestTotalInvalid,
|
||||
/// The total of transaction inputs overflows the valid range of Zcash values.
|
||||
Overflow,
|
||||
/// The input total and output total of the payment request are not equal to one another. The
|
||||
/// sum of transaction outputs, change, and fees is required to be exactly equal to the value
|
||||
/// of provided inputs.
|
||||
BalanceError {
|
||||
input_total: NonNegativeAmount,
|
||||
output_total: NonNegativeAmount,
|
||||
},
|
||||
/// The `is_shielding` flag may only be set to `true` under the following conditions:
|
||||
/// * The total of transparent inputs is nonzero
|
||||
/// * There exist no Sapling inputs
|
||||
/// * There provided transaction request is empty; i.e. the only output values specified
|
||||
/// are change and fee amounts.
|
||||
ShieldingInvalid,
|
||||
}
|
||||
|
||||
impl Display for ProposalError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ProposalError::RequestTotalInvalid => write!(
|
||||
f,
|
||||
"The total requested output value is not a valid Zcash amount."
|
||||
),
|
||||
ProposalError::Overflow => write!(
|
||||
f,
|
||||
"The total of transaction inputs overflows the valid range of Zcash values."
|
||||
),
|
||||
ProposalError::BalanceError {
|
||||
input_total,
|
||||
output_total,
|
||||
} => write!(
|
||||
f,
|
||||
"Balance error: the output total {} was not equal to the input total {}",
|
||||
u64::from(*output_total),
|
||||
u64::from(*input_total)
|
||||
),
|
||||
ProposalError::ShieldingInvalid => write!(
|
||||
f,
|
||||
"The proposal violates the rules for a shielding transaction."
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ProposalError {}
|
||||
|
||||
/// The Sapling inputs to a proposed transaction.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct ShieldedInputs<NoteRef> {
|
||||
anchor_height: BlockHeight,
|
||||
notes: NonEmpty<ReceivedNote<NoteRef, Note>>,
|
||||
}
|
||||
|
||||
impl<NoteRef> ShieldedInputs<NoteRef> {
|
||||
/// Constructs a [`ShieldedInputs`] from its constituent parts.
|
||||
pub fn from_parts(
|
||||
anchor_height: BlockHeight,
|
||||
notes: NonEmpty<ReceivedNote<NoteRef, Note>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
anchor_height,
|
||||
notes,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the anchor height for Sapling inputs that should be used when constructing the
|
||||
/// proposed transaction.
|
||||
pub fn anchor_height(&self) -> BlockHeight {
|
||||
self.anchor_height
|
||||
}
|
||||
|
||||
/// Returns the list of Sapling notes to be used as inputs to the proposed transaction.
|
||||
pub fn notes(&self) -> &NonEmpty<ReceivedNote<NoteRef, Note>> {
|
||||
&self.notes
|
||||
}
|
||||
}
|
||||
|
||||
/// The inputs to be consumed and outputs to be produced in a proposed transaction.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct Proposal<FeeRuleT, NoteRef> {
|
||||
transaction_request: TransactionRequest,
|
||||
payment_pools: BTreeMap<usize, PoolType>,
|
||||
transparent_inputs: Vec<WalletTransparentOutput>,
|
||||
shielded_inputs: Option<ShieldedInputs<NoteRef>>,
|
||||
balance: TransactionBalance,
|
||||
fee_rule: FeeRuleT,
|
||||
min_target_height: BlockHeight,
|
||||
is_shielding: bool,
|
||||
}
|
||||
|
||||
impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
|
||||
/// Constructs a validated [`Proposal`] from its constituent parts.
|
||||
///
|
||||
/// This operation validates the proposal for balance consistency and agreement between
|
||||
/// the `is_shielding` flag and the structure of the proposal.
|
||||
///
|
||||
/// Parameters:
|
||||
/// * `transaction_request`: The ZIP 321 transaction request describing the payments
|
||||
/// to be made.
|
||||
/// * `payment_pools`: A map from payment index to pool type.
|
||||
/// * `transparent_inputs`: The set of previous transparent outputs to be spent.
|
||||
/// * `shielded_inputs`: The sets of previous shielded outputs to be spent.
|
||||
/// * `balance`: The change outputs to be added the transaction and the fee to be paid.
|
||||
/// * `fee_rule`: The fee rule observed by the proposed transaction.
|
||||
/// * `min_target_height`: The minimum block height at which the transaction may be created.
|
||||
/// * `is_shielding`: A flag that identifies whether this is a wallet-internal shielding
|
||||
/// transaction.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn from_parts(
|
||||
transaction_request: TransactionRequest,
|
||||
payment_pools: BTreeMap<usize, PoolType>,
|
||||
transparent_inputs: Vec<WalletTransparentOutput>,
|
||||
shielded_inputs: Option<ShieldedInputs<NoteRef>>,
|
||||
balance: TransactionBalance,
|
||||
fee_rule: FeeRuleT,
|
||||
min_target_height: BlockHeight,
|
||||
is_shielding: bool,
|
||||
) -> Result<Self, ProposalError> {
|
||||
let transparent_input_total = transparent_inputs
|
||||
.iter()
|
||||
.map(|out| out.txout().value)
|
||||
.fold(Ok(NonNegativeAmount::ZERO), |acc, a| {
|
||||
(acc? + a).ok_or(ProposalError::Overflow)
|
||||
})?;
|
||||
let shielded_input_total = shielded_inputs
|
||||
.iter()
|
||||
.flat_map(|s_in| s_in.notes().iter())
|
||||
.map(|out| out.note().value())
|
||||
.fold(Some(NonNegativeAmount::ZERO), |acc, a| (acc? + a))
|
||||
.ok_or(ProposalError::Overflow)?;
|
||||
let input_total =
|
||||
(transparent_input_total + shielded_input_total).ok_or(ProposalError::Overflow)?;
|
||||
|
||||
let request_total = transaction_request
|
||||
.total()
|
||||
.map_err(|_| ProposalError::RequestTotalInvalid)?;
|
||||
let output_total = (request_total + balance.total()).ok_or(ProposalError::Overflow)?;
|
||||
|
||||
if is_shielding
|
||||
&& (transparent_input_total == NonNegativeAmount::ZERO
|
||||
|| shielded_input_total > NonNegativeAmount::ZERO
|
||||
|| request_total > NonNegativeAmount::ZERO)
|
||||
{
|
||||
return Err(ProposalError::ShieldingInvalid);
|
||||
}
|
||||
|
||||
if input_total == output_total {
|
||||
Ok(Self {
|
||||
transaction_request,
|
||||
payment_pools,
|
||||
transparent_inputs,
|
||||
shielded_inputs,
|
||||
balance,
|
||||
fee_rule,
|
||||
min_target_height,
|
||||
is_shielding,
|
||||
})
|
||||
} else {
|
||||
Err(ProposalError::BalanceError {
|
||||
input_total,
|
||||
output_total,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the transaction request that describes the payments to be made.
|
||||
pub fn transaction_request(&self) -> &TransactionRequest {
|
||||
&self.transaction_request
|
||||
}
|
||||
/// Returns the map from payment index to the pool that has been selected
|
||||
/// for the output that will fulfill that payment.
|
||||
pub fn payment_pools(&self) -> &BTreeMap<usize, PoolType> {
|
||||
&self.payment_pools
|
||||
}
|
||||
/// Returns the transparent inputs that have been selected to fund the transaction.
|
||||
pub fn transparent_inputs(&self) -> &[WalletTransparentOutput] {
|
||||
&self.transparent_inputs
|
||||
}
|
||||
/// Returns the Sapling inputs that have been selected to fund the transaction.
|
||||
pub fn shielded_inputs(&self) -> Option<&ShieldedInputs<NoteRef>> {
|
||||
self.shielded_inputs.as_ref()
|
||||
}
|
||||
/// Returns the change outputs to be added to the transaction and the fee to be paid.
|
||||
pub fn balance(&self) -> &TransactionBalance {
|
||||
&self.balance
|
||||
}
|
||||
/// Returns the fee rule to be used by the transaction builder.
|
||||
pub fn fee_rule(&self) -> &FeeRuleT {
|
||||
&self.fee_rule
|
||||
}
|
||||
/// Returns the target height for which the proposal was prepared.
|
||||
///
|
||||
/// The chain must contain at least this many blocks in order for the proposal to
|
||||
/// be executed.
|
||||
pub fn min_target_height(&self) -> BlockHeight {
|
||||
self.min_target_height
|
||||
}
|
||||
/// Returns a flag indicating whether or not the proposed transaction
|
||||
/// is exclusively wallet-internal (if it does not involve any external
|
||||
/// recipients).
|
||||
pub fn is_shielding(&self) -> bool {
|
||||
self.is_shielding
|
||||
}
|
||||
}
|
||||
|
||||
impl<FeeRuleT, NoteRef> Debug for Proposal<FeeRuleT, NoteRef> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Proposal")
|
||||
.field("transaction_request", &self.transaction_request)
|
||||
.field("transparent_inputs", &self.transparent_inputs)
|
||||
.field(
|
||||
"shielded_inputs",
|
||||
&self.shielded_inputs().map(|i| i.notes.len()),
|
||||
)
|
||||
.field(
|
||||
"anchor_height",
|
||||
&self.shielded_inputs().map(|i| i.anchor_height),
|
||||
)
|
||||
.field("balance", &self.balance)
|
||||
//.field("fee_rule", &self.fee_rule)
|
||||
.field("min_target_height", &self.min_target_height)
|
||||
.field("is_shielding", &self.is_shielding)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
|
@ -22,11 +22,9 @@ use zcash_primitives::{
|
|||
use zcash_note_encryption::{EphemeralKeyBytes, COMPACT_NOTE_SIZE};
|
||||
|
||||
use crate::{
|
||||
data_api::{
|
||||
wallet::input_selection::{Proposal, ProposalError, ShieldedInputs},
|
||||
InputSource,
|
||||
},
|
||||
data_api::InputSource,
|
||||
fees::{ChangeValue, TransactionBalance},
|
||||
proposal::{Proposal, ProposalError, ShieldedInputs},
|
||||
zip321::{TransactionRequest, Zip321Error},
|
||||
PoolType, ShieldedProtocol,
|
||||
};
|
||||
|
|
|
@ -30,14 +30,13 @@ use zcash_client_backend::{
|
|||
chain::{scan_cached_blocks, BlockSource, ScanSummary},
|
||||
wallet::{
|
||||
create_proposed_transaction, create_spend_to_address,
|
||||
input_selection::{
|
||||
GreedyInputSelector, GreedyInputSelectorError, InputSelector, Proposal,
|
||||
},
|
||||
input_selection::{GreedyInputSelector, GreedyInputSelectorError, InputSelector},
|
||||
propose_standard_transfer_to_address, propose_transfer, spend,
|
||||
},
|
||||
AccountBalance, AccountBirthday, WalletRead, WalletSummary, WalletWrite,
|
||||
},
|
||||
keys::UnifiedSpendingKey,
|
||||
proposal::Proposal,
|
||||
proto::compact_formats::{
|
||||
self as compact, CompactBlock, CompactSaplingOutput, CompactSaplingSpend, CompactTx,
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue