From 1db3109cb46f8f2c96e0bef4d9a764239c2b7da7 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Mon, 12 Feb 2024 12:46:34 -0700 Subject: [PATCH] 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. --- zcash_client_backend/CHANGELOG.md | 25 +- zcash_client_backend/src/data_api/error.rs | 9 + zcash_client_backend/src/data_api/wallet.rs | 9 +- .../src/data_api/wallet/input_selection.rs | 291 +++--------------- zcash_client_backend/src/lib.rs | 1 + zcash_client_backend/src/proposal.rs | 248 +++++++++++++++ zcash_client_backend/src/proto.rs | 6 +- zcash_client_sqlite/src/testing.rs | 5 +- 8 files changed, 326 insertions(+), 268 deletions(-) create mode 100644 zcash_client_backend/src/proposal.rs diff --git a/zcash_client_backend/CHANGELOG.md b/zcash_client_backend/CHANGELOG.md index ab6b5658c..44e28b652 100644 --- a/zcash_client_backend/CHANGELOG.md +++ b/zcash_client_backend/CHANGELOG.md @@ -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 diff --git a/zcash_client_backend/src/data_api/error.rs b/zcash_client_backend/src/data_api/error.rs index a482bd2ef..e708afee2 100644 --- a/zcash_client_backend/src/data_api/error.rs +++ b/zcash_client_backend/src/data_api/error.rs @@ -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 { /// 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 From> for Error match e { InputSelectorError::DataSource(e) => Error::DataSource(e), InputSelectorError::Selection(e) => Error::NoteSelection(e), + InputSelectorError::Proposal(e) => Error::Proposal(e), InputSelectorError::InsufficientFunds { available, required, diff --git a/zcash_client_backend/src/data_api/wallet.rs b/zcash_client_backend/src/data_api/wallet.rs index b6a1807b3..5a97fa79c 100644 --- a/zcash_client_backend/src/data_api/wallet.rs +++ b/zcash_client_backend/src/data_api/wallet.rs @@ -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, diff --git a/zcash_client_backend/src/data_api/wallet/input_selection.rs b/zcash_client_backend/src/data_api/wallet/input_selection.rs index 193a580e4..689ddc66b 100644 --- a/zcash_client_backend/src/data_api/wallet/input_selection.rs +++ b/zcash_client_backend/src/data_api/wallet/input_selection.rs @@ -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 { /// 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 fmt::Display for InputSelectorError { 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 fmt::Display for InputSelectorError { - transaction_request: TransactionRequest, - payment_pools: BTreeMap, - transparent_inputs: Vec, - shielded_inputs: Option>, - 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 error::Error for InputSelectorError +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 Proposal { - /// 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, - transparent_inputs: Vec, - shielded_inputs: Option>, - balance: TransactionBalance, - fee_rule: FeeRuleT, - min_target_height: BlockHeight, - is_shielding: bool, - ) -> Result { - 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 { - &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> { - 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 Debug for Proposal { - 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 { - anchor_height: BlockHeight, - notes: NonEmpty>, -} - -impl ShieldedInputs { - /// Constructs a [`ShieldedInputs`] from its constituent parts. - pub fn from_parts( - anchor_height: BlockHeight, - notes: NonEmpty>, - ) -> 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> { - &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(), diff --git a/zcash_client_backend/src/lib.rs b/zcash_client_backend/src/lib.rs index 94f2c4eae..0c8238a7e 100644 --- a/zcash_client_backend/src/lib.rs +++ b/zcash_client_backend/src/lib.rs @@ -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; diff --git a/zcash_client_backend/src/proposal.rs b/zcash_client_backend/src/proposal.rs new file mode 100644 index 000000000..500c866ed --- /dev/null +++ b/zcash_client_backend/src/proposal.rs @@ -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 { + anchor_height: BlockHeight, + notes: NonEmpty>, +} + +impl ShieldedInputs { + /// Constructs a [`ShieldedInputs`] from its constituent parts. + pub fn from_parts( + anchor_height: BlockHeight, + notes: NonEmpty>, + ) -> 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> { + &self.notes + } +} + +/// The inputs to be consumed and outputs to be produced in a proposed transaction. +#[derive(Clone, PartialEq, Eq)] +pub struct Proposal { + transaction_request: TransactionRequest, + payment_pools: BTreeMap, + transparent_inputs: Vec, + shielded_inputs: Option>, + balance: TransactionBalance, + fee_rule: FeeRuleT, + min_target_height: BlockHeight, + is_shielding: bool, +} + +impl Proposal { + /// 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, + transparent_inputs: Vec, + shielded_inputs: Option>, + balance: TransactionBalance, + fee_rule: FeeRuleT, + min_target_height: BlockHeight, + is_shielding: bool, + ) -> Result { + 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 { + &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> { + 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 Debug for Proposal { + 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() + } +} diff --git a/zcash_client_backend/src/proto.rs b/zcash_client_backend/src/proto.rs index 9c23e7588..4ea33c1f2 100644 --- a/zcash_client_backend/src/proto.rs +++ b/zcash_client_backend/src/proto.rs @@ -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, }; diff --git a/zcash_client_sqlite/src/testing.rs b/zcash_client_sqlite/src/testing.rs index 31ca460d3..eec846dc8 100644 --- a/zcash_client_sqlite/src/testing.rs +++ b/zcash_client_sqlite/src/testing.rs @@ -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, },