zcash_client_backend: Add selected output pools to transaction proposals.

Fixes #1174
This commit is contained in:
Kris Nuttycombe 2024-02-12 12:13:17 -07:00
parent 0ef5cad2ff
commit 6e0d9a9420
7 changed files with 224 additions and 131 deletions

View File

@ -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}`

View File

@ -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;

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -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)]

View File

@ -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(&params, 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)
}