//! Types related to the construction and evaluation of transaction proposals. 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 [`Step`]. #[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, /// A reference to the output of a prior step is invalid. ReferenceError(StepOutput), } 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." ), ProposalError::ReferenceError(r) => write!(f, "No prior step output found for {:?}", r), } } } 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 } } /// A proposal for a series of transactions to be created. /// /// Each step of the proposal represents a separate transaction to be created. At present, only /// transparent outputs of earlier steps may be spent in later steps; the ability to chain shielded /// transaction steps may be added in a future update. #[derive(Clone, PartialEq, Eq)] pub struct Proposal { fee_rule: FeeRuleT, min_target_height: BlockHeight, steps: NonEmpty>, } impl Proposal { /// Constructs a validated multi-step [`Proposal`]. /// /// This operation validates the proposal for agreement between outputs and inputs /// in the case of multi-step proposals, and ensures that no double-spends are being /// proposed. /// /// Parameters: /// * `fee_rule`: The fee rule observed by the proposed transaction. /// * `min_target_height`: The minimum block height at which the transaction may be created. /// * `steps`: A vector of steps that make up the proposal. pub fn multi_step( fee_rule: FeeRuleT, min_target_height: BlockHeight, steps: NonEmpty>, ) -> Result { // TODO: actually perform the validation described in the documentation. Ok(Self { fee_rule, min_target_height, steps, }) } /// Constructs a validated [`Proposal`] having only a single step 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 single_step( 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 { Ok(Self { fee_rule, min_target_height, steps: NonEmpty::singleton(Step::from_parts( &[], transaction_request, payment_pools, transparent_inputs, shielded_inputs, vec![], balance, is_shielding, )?), }) } /// 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 the steps of the proposal. Each step corresponds to an independent transaction to /// be generated as a result of this proposal. pub fn steps(&self) -> &NonEmpty> { &self.steps } } impl Debug for Proposal { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Proposal") .field("fee_rule", &self.fee_rule) .field("min_target_height", &self.min_target_height) .field("steps", &self.steps) .finish() } } /// A reference to either a payment or change output within a step. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum StepOutputIndex { Payment(usize), Change(usize), } /// A reference to the output of a step in a proposal. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct StepOutput { step_index: usize, output_index: StepOutputIndex, } impl StepOutput { /// Constructs a new [`StepOutput`] from its constituent parts. pub fn new(step_index: usize, output_index: StepOutputIndex) -> Self { Self { step_index, output_index, } } /// Returns the step index to which this reference refers. pub fn step_index(&self) -> usize { self.step_index } /// Returns the identifier for the payment or change output within /// the referenced step. pub fn output_index(&self) -> StepOutputIndex { self.output_index } } /// The inputs to be consumed and outputs to be produced in a proposed transaction. #[derive(Clone, PartialEq, Eq)] pub struct Step { transaction_request: TransactionRequest, payment_pools: BTreeMap, transparent_inputs: Vec, shielded_inputs: Option>, prior_step_inputs: Vec, balance: TransactionBalance, is_shielding: bool, } impl Step { /// Constructs a validated [`Step`] 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. /// * `is_shielding`: A flag that identifies whether this is a wallet-internal shielding /// transaction. #[allow(clippy::too_many_arguments)] pub fn from_parts( prior_steps: &[Step], transaction_request: TransactionRequest, payment_pools: BTreeMap, transparent_inputs: Vec, shielded_inputs: Option>, prior_step_inputs: Vec, balance: TransactionBalance, 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 prior_step_input_total = prior_step_inputs .iter() .map(|s_ref| { let step = prior_steps .get(s_ref.step_index) .ok_or(ProposalError::ReferenceError(*s_ref))?; Ok(match s_ref.output_index { StepOutputIndex::Payment(i) => { step.transaction_request .payments() .get(&i) .ok_or(ProposalError::ReferenceError(*s_ref))? .amount } StepOutputIndex::Change(i) => step .balance .proposed_change() .get(i) .ok_or(ProposalError::ReferenceError(*s_ref))? .value(), }) }) .collect::, _>>()? .into_iter() .fold(Some(NonNegativeAmount::ZERO), |acc, a| (acc? + a)) .ok_or(ProposalError::Overflow)?; let input_total = (transparent_input_total + shielded_input_total + prior_step_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, prior_step_inputs, balance, 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 shielded inputs that have been selected to fund the transaction. pub fn shielded_inputs(&self) -> Option<&ShieldedInputs> { self.shielded_inputs.as_ref() } /// Returns the inputs that should be obtained from the outputs of the transaction /// created to satisfy a previous step of the proposal. pub fn prior_step_inputs(&self) -> &[StepOutput] { self.prior_step_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 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 Step { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Step") .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("is_shielding", &self.is_shielding) .finish_non_exhaustive() } }