zcash_client_backend: Add selected output pools to transaction proposals.
Fixes #1174
This commit is contained in:
parent
0ef5cad2ff
commit
6e0d9a9420
|
@ -47,7 +47,7 @@ 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}`
|
||||
- `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,8 +59,6 @@ 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::proto::`
|
||||
- `PROPOSAL_SER_V1`
|
||||
- `ProposalDecodingError`
|
||||
|
@ -75,8 +73,9 @@ and this library adheres to Rust's notion of
|
|||
wallet::{ReceivedSaplingNote, WalletTransparentOutput},
|
||||
wallet::input_selection::{Proposal, SaplingInputs},
|
||||
}`
|
||||
- `zcash_client_backend::zip321::to_uri` now returns a `String` rather than an
|
||||
`Option<String>` and provides canonical serialization for the empty proposal.
|
||||
- `zcash_client_backend::zip321
|
||||
` `TransactionRequest::{total, from_indexed}`
|
||||
- `parse::Param::name`
|
||||
|
||||
### Moved
|
||||
- `zcash_client_backend::data_api::{PoolType, ShieldedProtocol}` have
|
||||
|
@ -196,6 +195,11 @@ and this library adheres to Rust's notion of
|
|||
`ReceivedSaplingNote::from_parts` for construction instead. Accessor methods
|
||||
are provided for each previously public field.
|
||||
- `zcash_client_backend::scanning::ScanError` has a new variant, `TreeSizeInvalid`.
|
||||
- `zcash_client_backend::zip321::TransactionRequest::payments` now returns a
|
||||
`BTreeMap<usize, Payment>` instead of `&[Payment]` so that parameter
|
||||
indices may be preserved.
|
||||
- `zcash_client_backend::zip321::to_uri` now returns a `String` rather than an
|
||||
`Option<String>` and provides canonical serialization for the empty proposal.
|
||||
- The following fields now have type `NonNegativeAmount` instead of `Amount`:
|
||||
- `zcash_client_backend::data_api`:
|
||||
- `error::Error::InsufficientFunds.{available, required}`
|
||||
|
|
|
@ -11,25 +11,28 @@ message Proposal {
|
|||
uint32 protoVersion = 1;
|
||||
// ZIP 321 serialized transaction request
|
||||
string transactionRequest = 2;
|
||||
// The vector of selected payment index / output pool mappings. Payment index
|
||||
// 0 corresponds to the payment with no explicit index.
|
||||
repeated PaymentOutputPool paymentOutputPools = 3;
|
||||
// The anchor height to be used in creating the transaction, if any.
|
||||
// Setting the anchor height to zero will disallow the use of any shielded
|
||||
// inputs.
|
||||
uint32 anchorHeight = 3;
|
||||
uint32 anchorHeight = 4;
|
||||
// The inputs to be used in creating the transaction.
|
||||
repeated ProposedInput inputs = 4;
|
||||
repeated ProposedInput inputs = 5;
|
||||
// The total value, fee value, and change outputs of the proposed
|
||||
// transaction
|
||||
TransactionBalance balance = 5;
|
||||
TransactionBalance balance = 6;
|
||||
// The fee rule used in constructing this proposal
|
||||
FeeRule feeRule = 6;
|
||||
FeeRule feeRule = 7;
|
||||
// The target height for which the proposal was constructed
|
||||
//
|
||||
// The chain must contain at least this many blocks in order for the proposal to
|
||||
// be executed.
|
||||
uint32 minTargetHeight = 7;
|
||||
uint32 minTargetHeight = 8;
|
||||
// A flag indicating whether the proposal is for a shielding transaction,
|
||||
// used for determining which OVK to select for wallet-internal outputs.
|
||||
bool isShielding = 8;
|
||||
bool isShielding = 9;
|
||||
}
|
||||
|
||||
enum ValuePool {
|
||||
|
@ -46,6 +49,14 @@ enum ValuePool {
|
|||
Orchard = 3;
|
||||
}
|
||||
|
||||
// A mapping from ZIP 321 payment index to the output pool that has been chosen
|
||||
// for that payment, based upon the payment address and the selected inputs to
|
||||
// the transaction.
|
||||
message PaymentOutputPool {
|
||||
uint32 paymentIndex = 1;
|
||||
ValuePool valuePool = 2;
|
||||
}
|
||||
|
||||
// The unique identifier and value for each proposed input.
|
||||
message ProposedInput {
|
||||
bytes txid = 1;
|
||||
|
|
|
@ -661,7 +661,7 @@ where
|
|||
|
||||
let mut sapling_output_meta = vec![];
|
||||
let mut transparent_output_meta = vec![];
|
||||
for payment in proposal.transaction_request().payments() {
|
||||
for payment in proposal.transaction_request().payments().values() {
|
||||
match &payment.recipient_address {
|
||||
Address::Unified(ua) => {
|
||||
let memo = payment
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
//! Types related to the process of selecting inputs to be spent given a transaction request.
|
||||
|
||||
use core::marker::PhantomData;
|
||||
use std::fmt::{self, Debug, Display};
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
fmt::{self, Debug, Display},
|
||||
};
|
||||
|
||||
use nonempty::NonEmpty;
|
||||
use zcash_primitives::{
|
||||
|
@ -23,7 +26,7 @@ use crate::{
|
|||
fees::{sapling, ChangeError, ChangeStrategy, DustOutputPolicy, TransactionBalance},
|
||||
wallet::{Note, ReceivedNote, WalletTransparentOutput},
|
||||
zip321::TransactionRequest,
|
||||
ShieldedProtocol,
|
||||
PoolType, ShieldedProtocol,
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "transparent-inputs"))]
|
||||
|
@ -85,6 +88,7 @@ impl<DE: fmt::Display, SE: fmt::Display> fmt::Display for InputSelectorError<DE,
|
|||
#[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,
|
||||
|
@ -150,9 +154,22 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
|
|||
///
|
||||
/// 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,
|
||||
|
@ -191,6 +208,7 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
|
|||
if input_total == output_total {
|
||||
Ok(Self {
|
||||
transaction_request,
|
||||
payment_pools,
|
||||
transparent_inputs,
|
||||
shielded_inputs,
|
||||
balance,
|
||||
|
@ -210,6 +228,11 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
|
|||
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
|
||||
|
@ -525,46 +548,46 @@ where
|
|||
let mut sapling_outputs = vec![];
|
||||
#[cfg(feature = "orchard")]
|
||||
let mut orchard_outputs = vec![];
|
||||
for payment in transaction_request.payments() {
|
||||
let mut push_transparent = |taddr: TransparentAddress| {
|
||||
transparent_outputs.push(TxOut {
|
||||
value: payment.amount,
|
||||
script_pubkey: taddr.script(),
|
||||
});
|
||||
};
|
||||
let mut push_sapling = || {
|
||||
sapling_outputs.push(SaplingPayment(payment.amount));
|
||||
};
|
||||
#[cfg(feature = "orchard")]
|
||||
let mut push_orchard = || {
|
||||
orchard_outputs.push(OrchardPayment(payment.amount));
|
||||
};
|
||||
|
||||
let mut payment_pools = BTreeMap::new();
|
||||
for (idx, payment) in transaction_request.payments() {
|
||||
match &payment.recipient_address {
|
||||
Address::Transparent(addr) => {
|
||||
push_transparent(*addr);
|
||||
payment_pools.insert(*idx, PoolType::Transparent);
|
||||
transparent_outputs.push(TxOut {
|
||||
value: payment.amount,
|
||||
script_pubkey: addr.script(),
|
||||
});
|
||||
}
|
||||
Address::Sapling(_) => {
|
||||
push_sapling();
|
||||
payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Sapling));
|
||||
sapling_outputs.push(SaplingPayment(payment.amount));
|
||||
}
|
||||
Address::Unified(addr) => {
|
||||
#[cfg(feature = "orchard")]
|
||||
let has_orchard = addr.orchard().is_some();
|
||||
#[cfg(not(feature = "orchard"))]
|
||||
let has_orchard = false;
|
||||
|
||||
if has_orchard {
|
||||
#[cfg(feature = "orchard")]
|
||||
push_orchard();
|
||||
} else if addr.sapling().is_some() {
|
||||
push_sapling();
|
||||
} else if let Some(addr) = addr.transparent() {
|
||||
push_transparent(*addr);
|
||||
} else {
|
||||
return Err(InputSelectorError::Selection(
|
||||
GreedyInputSelectorError::UnsupportedAddress(Box::new(addr.clone())),
|
||||
));
|
||||
if addr.orchard().is_some() {
|
||||
payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Orchard));
|
||||
orchard_outputs.push(OrchardPayment(payment.amount));
|
||||
continue;
|
||||
}
|
||||
|
||||
if addr.sapling().is_some() {
|
||||
payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Sapling));
|
||||
sapling_outputs.push(SaplingPayment(payment.amount));
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(addr) = addr.transparent() {
|
||||
payment_pools.insert(*idx, PoolType::Transparent);
|
||||
transparent_outputs.push(TxOut {
|
||||
value: payment.amount,
|
||||
script_pubkey: addr.script(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
return Err(InputSelectorError::Selection(
|
||||
GreedyInputSelectorError::UnsupportedAddress(Box::new(addr.clone())),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -617,6 +640,7 @@ where
|
|||
Ok(balance) => {
|
||||
return Ok(Proposal {
|
||||
transaction_request,
|
||||
payment_pools,
|
||||
transparent_inputs: vec![],
|
||||
shielded_inputs: NonEmpty::from_vec(shielded_inputs).map(|notes| {
|
||||
ShieldedInputs {
|
||||
|
@ -768,6 +792,7 @@ where
|
|||
if balance.total() >= shielding_threshold {
|
||||
Ok(Proposal {
|
||||
transaction_request: TransactionRequest::empty(),
|
||||
payment_pools: BTreeMap::new(),
|
||||
transparent_inputs,
|
||||
shielded_inputs: None,
|
||||
balance,
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use std::{
|
||||
array::TryFromSliceError,
|
||||
collections::BTreeMap,
|
||||
fmt::{self, Display},
|
||||
io,
|
||||
};
|
||||
|
@ -334,6 +335,15 @@ impl proposal::ChangeValue {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<PoolType> for proposal::ValuePool {
|
||||
fn from(value: PoolType) -> Self {
|
||||
match value {
|
||||
PoolType::Transparent => proposal::ValuePool::Transparent,
|
||||
PoolType::Shielded(p) => p.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ShieldedProtocol> for proposal::ValuePool {
|
||||
fn from(value: ShieldedProtocol) -> Self {
|
||||
match value {
|
||||
|
@ -376,6 +386,15 @@ impl proposal::Proposal {
|
|||
}))
|
||||
.collect();
|
||||
|
||||
let payment_output_pools = value
|
||||
.payment_pools()
|
||||
.iter()
|
||||
.map(|(idx, pool_type)| proposal::PaymentOutputPool {
|
||||
payment_index: u32::try_from(*idx).expect("Payment index fits into a u32"),
|
||||
value_pool: proposal::ValuePool::from(*pool_type).into(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let balance = Some(proposal::TransactionBalance {
|
||||
proposed_change: value
|
||||
.balance()
|
||||
|
@ -396,6 +415,7 @@ impl proposal::Proposal {
|
|||
proposal::Proposal {
|
||||
proto_version: PROPOSAL_SER_V1,
|
||||
transaction_request,
|
||||
payment_output_pools,
|
||||
anchor_height,
|
||||
inputs,
|
||||
balance,
|
||||
|
@ -435,6 +455,19 @@ impl proposal::Proposal {
|
|||
let transaction_request =
|
||||
TransactionRequest::from_uri(params, &self.transaction_request)?;
|
||||
|
||||
let payment_pools = self
|
||||
.payment_output_pools
|
||||
.iter()
|
||||
.map(|pop| {
|
||||
Ok((
|
||||
usize::try_from(pop.payment_index)
|
||||
.expect("Payment index fits into a usize"),
|
||||
pool_type(pop.value_pool)?,
|
||||
))
|
||||
})
|
||||
.collect::<Result<BTreeMap<usize, PoolType>, ProposalDecodingError<DbError>>>(
|
||||
)?;
|
||||
|
||||
#[cfg(not(feature = "transparent-inputs"))]
|
||||
let transparent_inputs = vec![];
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
|
@ -522,6 +555,7 @@ impl proposal::Proposal {
|
|||
|
||||
Proposal::from_parts(
|
||||
transaction_request,
|
||||
payment_pools,
|
||||
transparent_inputs,
|
||||
shielded_inputs,
|
||||
balance,
|
||||
|
|
|
@ -8,32 +8,47 @@ pub struct Proposal {
|
|||
/// ZIP 321 serialized transaction request
|
||||
#[prost(string, tag = "2")]
|
||||
pub transaction_request: ::prost::alloc::string::String,
|
||||
/// The vector of selected payment index / output pool mappings. Payment index
|
||||
/// 0 corresponds to the payment with no explicit index.
|
||||
#[prost(message, repeated, tag = "3")]
|
||||
pub payment_output_pools: ::prost::alloc::vec::Vec<PaymentOutputPool>,
|
||||
/// The anchor height to be used in creating the transaction, if any.
|
||||
/// Setting the anchor height to zero will disallow the use of any shielded
|
||||
/// inputs.
|
||||
#[prost(uint32, tag = "3")]
|
||||
#[prost(uint32, tag = "4")]
|
||||
pub anchor_height: u32,
|
||||
/// The inputs to be used in creating the transaction.
|
||||
#[prost(message, repeated, tag = "4")]
|
||||
#[prost(message, repeated, tag = "5")]
|
||||
pub inputs: ::prost::alloc::vec::Vec<ProposedInput>,
|
||||
/// The total value, fee value, and change outputs of the proposed
|
||||
/// transaction
|
||||
#[prost(message, optional, tag = "5")]
|
||||
#[prost(message, optional, tag = "6")]
|
||||
pub balance: ::core::option::Option<TransactionBalance>,
|
||||
/// The fee rule used in constructing this proposal
|
||||
#[prost(enumeration = "FeeRule", tag = "6")]
|
||||
#[prost(enumeration = "FeeRule", tag = "7")]
|
||||
pub fee_rule: i32,
|
||||
/// The target height for which the proposal was constructed
|
||||
///
|
||||
/// The chain must contain at least this many blocks in order for the proposal to
|
||||
/// be executed.
|
||||
#[prost(uint32, tag = "7")]
|
||||
#[prost(uint32, tag = "8")]
|
||||
pub min_target_height: u32,
|
||||
/// A flag indicating whether the proposal is for a shielding transaction,
|
||||
/// used for determining which OVK to select for wallet-internal outputs.
|
||||
#[prost(bool, tag = "8")]
|
||||
#[prost(bool, tag = "9")]
|
||||
pub is_shielding: bool,
|
||||
}
|
||||
/// A mapping from ZIP 321 payment index to the output pool that has been chosen
|
||||
/// for that payment, based upon the payment address and the selected inputs to
|
||||
/// the transaction.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct PaymentOutputPool {
|
||||
#[prost(uint32, tag = "1")]
|
||||
pub payment_index: u32,
|
||||
#[prost(enumeration = "ValuePool", tag = "2")]
|
||||
pub value_pool: i32,
|
||||
}
|
||||
/// The unique identifier and value for each proposed input.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
//! The specification for ZIP 321 URIs may be found at <https://zips.z.cash/zip-0321>
|
||||
use core::fmt::Debug;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
collections::BTreeMap,
|
||||
fmt::{self, Display},
|
||||
};
|
||||
|
||||
|
@ -21,9 +21,6 @@ use zcash_primitives::{
|
|||
transaction::components::amount::NonNegativeAmount,
|
||||
};
|
||||
|
||||
#[cfg(any(test, feature = "test-dependencies"))]
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use crate::address::Address;
|
||||
|
||||
/// Errors that may be produced in decoding of payment requests.
|
||||
|
@ -139,28 +136,6 @@ impl Payment {
|
|||
pub(in crate::zip321) fn normalize(&mut self) {
|
||||
self.other_params.sort();
|
||||
}
|
||||
|
||||
/// Returns a function which compares two normalized payments, with addresses sorted by their
|
||||
/// string representation given the specified network. This does not perform normalization
|
||||
/// internally, so payments must be normalized prior to being passed to the comparison function
|
||||
/// returned from this method.
|
||||
#[cfg(any(test, feature = "test-dependencies"))]
|
||||
pub(in crate::zip321) fn compare_normalized<P: consensus::Parameters>(
|
||||
params: &P,
|
||||
) -> impl Fn(&Payment, &Payment) -> Ordering + '_ {
|
||||
move |a: &Payment, b: &Payment| {
|
||||
let a_addr = a.recipient_address.encode(params);
|
||||
let b_addr = b.recipient_address.encode(params);
|
||||
|
||||
a_addr
|
||||
.cmp(&b_addr)
|
||||
.then(a.amount.cmp(&b.amount))
|
||||
.then(a.memo.cmp(&b.memo))
|
||||
.then(a.label.cmp(&b.label))
|
||||
.then(a.message.cmp(&b.message))
|
||||
.then(a.other_params.cmp(&b.other_params))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A ZIP321 transaction request.
|
||||
|
@ -171,18 +146,27 @@ impl Payment {
|
|||
/// payment value in the request.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TransactionRequest {
|
||||
payments: Vec<Payment>,
|
||||
payments: BTreeMap<usize, Payment>,
|
||||
}
|
||||
|
||||
impl TransactionRequest {
|
||||
/// Constructs a new empty transaction request.
|
||||
pub fn empty() -> Self {
|
||||
Self { payments: vec![] }
|
||||
Self {
|
||||
payments: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a new transaction request that obeys the ZIP-321 invariants
|
||||
/// Constructs a new transaction request that obeys the ZIP-321 invariants.
|
||||
pub fn new(payments: Vec<Payment>) -> Result<TransactionRequest, Zip321Error> {
|
||||
let request = TransactionRequest { payments };
|
||||
// Payment indices are limited to 4 digits
|
||||
if payments.len() > 9999 {
|
||||
return Err(Zip321Error::TooManyPayments(payments.len()));
|
||||
}
|
||||
|
||||
let request = TransactionRequest {
|
||||
payments: payments.into_iter().enumerate().collect(),
|
||||
};
|
||||
|
||||
// Enforce validity requirements.
|
||||
if !request.payments.is_empty() {
|
||||
|
@ -195,9 +179,27 @@ impl TransactionRequest {
|
|||
Ok(request)
|
||||
}
|
||||
|
||||
/// Returns the slice of payments that make up this request.
|
||||
pub fn payments(&self) -> &[Payment] {
|
||||
&self.payments[..]
|
||||
/// Constructs a new transaction request from the provided map from payment
|
||||
/// index to payment.
|
||||
///
|
||||
/// Payment index 0 will be mapped to the empty payment index.
|
||||
pub fn from_indexed(
|
||||
payments: BTreeMap<usize, Payment>,
|
||||
) -> Result<TransactionRequest, Zip321Error> {
|
||||
if let Some(k) = payments.keys().find(|k| **k > 9999) {
|
||||
// This is not quite the correct error, but close enough.
|
||||
return Err(Zip321Error::TooManyPayments(*k));
|
||||
}
|
||||
|
||||
Ok(TransactionRequest { payments })
|
||||
}
|
||||
|
||||
/// Returns the map of payments that make up this request.
|
||||
///
|
||||
/// This is a map from payment index to payment. Payment index `0` is used to denote
|
||||
/// the empty payment index in the returned values.
|
||||
pub fn payments(&self) -> &BTreeMap<usize, Payment> {
|
||||
&self.payments
|
||||
}
|
||||
|
||||
/// Returns the total value of payments to be made.
|
||||
|
@ -205,36 +207,29 @@ impl TransactionRequest {
|
|||
/// Returns `Err` in the case of overflow, or if the value is
|
||||
/// outside the range `0..=MAX_MONEY` zatoshis.
|
||||
pub fn total(&self) -> Result<NonNegativeAmount, ()> {
|
||||
if self.payments.is_empty() {
|
||||
Ok(NonNegativeAmount::ZERO)
|
||||
} else {
|
||||
self.payments
|
||||
.iter()
|
||||
.map(|p| p.amount)
|
||||
.fold(Ok(NonNegativeAmount::ZERO), |acc, a| (acc? + a).ok_or(()))
|
||||
}
|
||||
self.payments
|
||||
.values()
|
||||
.map(|p| p.amount)
|
||||
.fold(Ok(NonNegativeAmount::ZERO), |acc, a| (acc? + a).ok_or(()))
|
||||
}
|
||||
|
||||
/// A utility for use in tests to help check round-trip serialization properties.
|
||||
#[cfg(any(test, feature = "test-dependencies"))]
|
||||
pub(in crate::zip321) fn normalize<P: consensus::Parameters>(&mut self, params: &P) {
|
||||
for p in &mut self.payments {
|
||||
pub(in crate::zip321) fn normalize(&mut self) {
|
||||
for p in self.payments.values_mut() {
|
||||
p.normalize();
|
||||
}
|
||||
|
||||
self.payments.sort_by(Payment::compare_normalized(params));
|
||||
}
|
||||
|
||||
/// A utility for use in tests to help check round-trip serialization properties.
|
||||
/// by comparing a two transaction requests for equality after normalization.
|
||||
#[cfg(all(test, feature = "test-dependencies"))]
|
||||
pub(in crate::zip321) fn normalize_and_eq<P: consensus::Parameters>(
|
||||
params: &P,
|
||||
pub(in crate::zip321) fn normalize_and_eq(
|
||||
a: &mut TransactionRequest,
|
||||
b: &mut TransactionRequest,
|
||||
) -> bool {
|
||||
a.normalize(params);
|
||||
b.normalize(params);
|
||||
a.normalize();
|
||||
b.normalize();
|
||||
|
||||
a == b
|
||||
}
|
||||
|
@ -275,9 +270,10 @@ impl TransactionRequest {
|
|||
)
|
||||
}
|
||||
|
||||
match &self.payments[..] {
|
||||
[] => "zcash:".to_string(),
|
||||
[payment] => {
|
||||
match self.payments.len() {
|
||||
0 => "zcash:".to_string(),
|
||||
1 if *self.payments.iter().next().unwrap().0 == 0 => {
|
||||
let (_, payment) = self.payments.iter().next().unwrap();
|
||||
let query_params = payment_params(payment, None)
|
||||
.into_iter()
|
||||
.collect::<Vec<String>>();
|
||||
|
@ -293,12 +289,12 @@ impl TransactionRequest {
|
|||
let query_params = self
|
||||
.payments
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(i, payment)| {
|
||||
let idx = if *i == 0 { None } else { Some(*i) };
|
||||
let primary_address = payment.recipient_address.clone();
|
||||
std::iter::empty()
|
||||
.chain(Some(render::addr_param(params, &primary_address, Some(i))))
|
||||
.chain(payment_params(payment, Some(i)))
|
||||
.chain(Some(render::addr_param(params, &primary_address, idx)))
|
||||
.chain(payment_params(payment, idx))
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
|
@ -325,7 +321,7 @@ impl TransactionRequest {
|
|||
};
|
||||
|
||||
// Construct sets of payment parameters, keyed by the payment index.
|
||||
let mut params_by_index: HashMap<usize, Vec<parse::Param>> = HashMap::new();
|
||||
let mut params_by_index: BTreeMap<usize, Vec<parse::Param>> = BTreeMap::new();
|
||||
|
||||
// Add the primary address, if any, to the index.
|
||||
if let Some(p) = primary_addr_param {
|
||||
|
@ -352,8 +348,8 @@ impl TransactionRequest {
|
|||
// Build the actual payment values from the index.
|
||||
params_by_index
|
||||
.into_iter()
|
||||
.map(|(i, params)| parse::to_payment(params, i))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map(|(i, params)| parse::to_payment(params, i).map(|payment| (i, payment)))
|
||||
.collect::<Result<BTreeMap<usize, Payment>, _>>()
|
||||
.map(|payments| TransactionRequest { payments })
|
||||
}
|
||||
}
|
||||
|
@ -797,9 +793,17 @@ pub mod testing {
|
|||
}
|
||||
|
||||
prop_compose! {
|
||||
pub fn arb_zip321_request()(payments in vec(arb_zip321_payment(), 1..10)) -> TransactionRequest {
|
||||
let mut req = TransactionRequest { payments };
|
||||
req.normalize(&TEST_NETWORK); // just to make test comparisons easier
|
||||
pub fn arb_zip321_request()(payments in btree_map(0usize..10000, arb_zip321_payment(), 1..10)) -> TransactionRequest {
|
||||
let mut req = TransactionRequest::from_indexed(payments).unwrap();
|
||||
req.normalize(); // just to make test comparisons easier
|
||||
req
|
||||
}
|
||||
}
|
||||
|
||||
prop_compose! {
|
||||
pub fn arb_zip321_request_sequential()(payments in vec(arb_zip321_payment(), 1..10)) -> TransactionRequest {
|
||||
let mut req = TransactionRequest::new(payments).unwrap();
|
||||
req.normalize(); // just to make test comparisons easier
|
||||
req
|
||||
}
|
||||
}
|
||||
|
@ -883,8 +887,8 @@ mod tests {
|
|||
let uri = "zcash:ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k?amount=3768769.02796286&message=";
|
||||
let parse_result = TransactionRequest::from_uri(&TEST_NETWORK, uri).unwrap();
|
||||
|
||||
let expected = TransactionRequest {
|
||||
payments: vec![
|
||||
let expected = TransactionRequest::new(
|
||||
vec![
|
||||
Payment {
|
||||
recipient_address: Address::Sapling(decode_payment_address(TEST_NETWORK.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()),
|
||||
amount: NonNegativeAmount::const_from_u64(376876902796286),
|
||||
|
@ -894,7 +898,7 @@ mod tests {
|
|||
other_params: vec![],
|
||||
}
|
||||
]
|
||||
};
|
||||
).unwrap();
|
||||
|
||||
assert_eq!(parse_result, expected);
|
||||
}
|
||||
|
@ -904,8 +908,8 @@ mod tests {
|
|||
let uri = "zcash:ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k";
|
||||
let parse_result = TransactionRequest::from_uri(&TEST_NETWORK, uri).unwrap();
|
||||
|
||||
let expected = TransactionRequest {
|
||||
payments: vec![
|
||||
let expected = TransactionRequest::new(
|
||||
vec![
|
||||
Payment {
|
||||
recipient_address: Address::Sapling(decode_payment_address(TEST_NETWORK.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()),
|
||||
amount: NonNegativeAmount::ZERO,
|
||||
|
@ -915,15 +919,15 @@ mod tests {
|
|||
other_params: vec![],
|
||||
}
|
||||
]
|
||||
};
|
||||
).unwrap();
|
||||
|
||||
assert_eq!(parse_result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zip321_roundtrip_empty_message() {
|
||||
let req = TransactionRequest {
|
||||
payments: vec![
|
||||
let req = TransactionRequest::new(
|
||||
vec![
|
||||
Payment {
|
||||
recipient_address: Address::Sapling(decode_payment_address(TEST_NETWORK.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()),
|
||||
amount: NonNegativeAmount::ZERO,
|
||||
|
@ -933,7 +937,7 @@ mod tests {
|
|||
other_params: vec![]
|
||||
}
|
||||
]
|
||||
};
|
||||
).unwrap();
|
||||
|
||||
check_roundtrip(req);
|
||||
}
|
||||
|
@ -970,19 +974,19 @@ mod tests {
|
|||
let valid_1 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=1&memo=VGhpcyBpcyBhIHNpbXBsZSBtZW1vLg&message=Thank%20you%20for%20your%20purchase";
|
||||
let v1r = TransactionRequest::from_uri(&TEST_NETWORK, valid_1).unwrap();
|
||||
assert_eq!(
|
||||
v1r.payments.get(0).map(|p| p.amount),
|
||||
v1r.payments.get(&0).map(|p| p.amount),
|
||||
Some(NonNegativeAmount::const_from_u64(100000000))
|
||||
);
|
||||
|
||||
let valid_2 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=123.456&address.1=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.1=0.789&memo.1=VGhpcyBpcyBhIHVuaWNvZGUgbWVtbyDinKjwn6aE8J-PhvCfjok";
|
||||
let mut v2r = TransactionRequest::from_uri(&TEST_NETWORK, valid_2).unwrap();
|
||||
v2r.normalize(&TEST_NETWORK);
|
||||
v2r.normalize();
|
||||
assert_eq!(
|
||||
v2r.payments.get(0).map(|p| p.amount),
|
||||
v2r.payments.get(&0).map(|p| p.amount),
|
||||
Some(NonNegativeAmount::const_from_u64(12345600000))
|
||||
);
|
||||
assert_eq!(
|
||||
v2r.payments.get(1).map(|p| p.amount),
|
||||
v2r.payments.get(&1).map(|p| p.amount),
|
||||
Some(NonNegativeAmount::const_from_u64(78900000))
|
||||
);
|
||||
|
||||
|
@ -991,7 +995,7 @@ mod tests {
|
|||
let valid_3 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=20999999.99999999";
|
||||
let v3r = TransactionRequest::from_uri(&TEST_NETWORK, valid_3).unwrap();
|
||||
assert_eq!(
|
||||
v3r.payments.get(0).map(|p| p.amount),
|
||||
v3r.payments.get(&0).map(|p| p.amount),
|
||||
Some(NonNegativeAmount::const_from_u64(2099999999999999u64))
|
||||
);
|
||||
|
||||
|
@ -1000,7 +1004,7 @@ mod tests {
|
|||
let valid_4 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=21000000";
|
||||
let v4r = TransactionRequest::from_uri(&TEST_NETWORK, valid_4).unwrap();
|
||||
assert_eq!(
|
||||
v4r.payments.get(0).map(|p| p.amount),
|
||||
v4r.payments.get(&0).map(|p| p.amount),
|
||||
Some(NonNegativeAmount::const_from_u64(2100000000000000u64))
|
||||
);
|
||||
}
|
||||
|
@ -1021,7 +1025,7 @@ mod tests {
|
|||
let valid_1 = "zcash:zregtestsapling1qqqqqqqqqqqqqqqqqqcguyvaw2vjk4sdyeg0lc970u659lvhqq7t0np6hlup5lusxle7505hlz3?amount=1&memo=VGhpcyBpcyBhIHNpbXBsZSBtZW1vLg&message=Thank%20you%20for%20your%20purchase";
|
||||
let v1r = TransactionRequest::from_uri(¶ms, valid_1).unwrap();
|
||||
assert_eq!(
|
||||
v1r.payments.get(0).map(|p| p.amount),
|
||||
v1r.payments.get(&0).map(|p| p.amount),
|
||||
Some(NonNegativeAmount::const_from_u64(100000000))
|
||||
);
|
||||
}
|
||||
|
@ -1140,13 +1144,13 @@ mod tests {
|
|||
fn prop_zip321_roundtrip_request(mut req in arb_zip321_request()) {
|
||||
let req_uri = req.to_uri(&TEST_NETWORK);
|
||||
let mut parsed = TransactionRequest::from_uri(&TEST_NETWORK, &req_uri).unwrap();
|
||||
assert!(TransactionRequest::normalize_and_eq(&TEST_NETWORK, &mut parsed, &mut req));
|
||||
assert!(TransactionRequest::normalize_and_eq(&mut parsed, &mut req));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prop_zip321_roundtrip_uri(uri in arb_zip321_uri()) {
|
||||
let mut parsed = TransactionRequest::from_uri(&TEST_NETWORK, &uri).unwrap();
|
||||
parsed.normalize(&TEST_NETWORK);
|
||||
parsed.normalize();
|
||||
let serialized = parsed.to_uri(&TEST_NETWORK);
|
||||
assert_eq!(serialized, uri)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue