zip321: Replace dependencies on `zcash_keys` types with `zcash_address`

This commit is contained in:
Kris Nuttycombe 2024-01-16 08:51:53 -07:00
parent fdf86ad740
commit d982d7826a
11 changed files with 364 additions and 214 deletions

9
Cargo.lock generated
View File

@ -3068,6 +3068,7 @@ dependencies = [
"zcash_proofs",
"zcash_protocol",
"zip32",
"zip321",
]
[[package]]
@ -3331,3 +3332,11 @@ dependencies = [
[[package]]
name = "zip321"
version = "0.0.0"
dependencies = [
"base64",
"nom",
"percent-encoding",
"proptest",
"zcash_address",
"zcash_protocol",
]

View File

@ -7,6 +7,13 @@ and this library adheres to Rust's notion of
## [Unreleased]
### Added
- `zcash_address::ZcashAddress::can_receive_memo`
- `zcash_address::unified::Address::can_receive_memo`
- Module `zcash_address::testing` under the `test-dependencies` feature.
- Module `zcash_address::unified::address::testing` under the
`test-dependencies` feature.
## [0.3.2] - 2024-03-06
### Added
- `zcash_address::convert`:

View File

@ -19,18 +19,18 @@ all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[dependencies]
bech32 = "0.9"
bs58 = { version = "0.5", features = ["check"] }
bech32.workspace = true
bs58.workspace = true
f4jumble = { version = "0.1", path = "../f4jumble" }
zcash_protocol.workspace = true
zcash_encoding.workspace = true
proptest = { workspace = true, optional = true }
[dev-dependencies]
assert_matches = "1.3.0"
proptest = "1"
assert_matches.workspace = true
[features]
test-dependencies = []
test-dependencies = ["dep:proptest"]
[lib]
bench = false

View File

@ -101,6 +101,15 @@ impl SealedItem for Receiver {
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Address(pub(crate) Vec<Receiver>);
impl Address {
/// Returns whether this address can receive a memo.
pub fn can_receive_memo(&self) -> bool {
self.0
.iter()
.any(|r| matches!(r, Receiver::Sapling(_) | Receiver::Orchard(_)))
}
}
impl super::private::SealedContainer for Address {
/// The HRP for a Bech32m-encoded mainnet Unified Address.
///
@ -133,27 +142,19 @@ impl super::Container for Address {
}
}
#[cfg(any(test, feature = "test-dependencies"))]
pub mod test_vectors;
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use zcash_encoding::MAX_COMPACT_SIZE;
use crate::{
kind::unified::{private::SealedContainer, Container, Encoding},
Network,
};
#[cfg(feature = "test-dependencies")]
pub mod testing {
use proptest::{
array::{uniform11, uniform20, uniform32},
collection::vec,
prelude::*,
sample::select,
strategy::Strategy,
};
use zcash_encoding::MAX_COMPACT_SIZE;
use super::{Address, ParseError, Receiver, Typecode};
use super::{Address, Receiver};
use crate::unified::Typecode;
prop_compose! {
fn uniform43()(a in uniform11(0u8..), b in uniform32(0u8..)) -> [u8; 43] {
@ -164,11 +165,13 @@ mod tests {
}
}
fn arb_transparent_typecode() -> impl Strategy<Value = Typecode> {
/// A strategy to generate an arbitrary transparent typecode.
pub fn arb_transparent_typecode() -> impl Strategy<Value = Typecode> {
select(vec![Typecode::P2pkh, Typecode::P2sh])
}
fn arb_shielded_typecode() -> impl Strategy<Value = Typecode> {
/// A strategy to generate an arbitrary shielded (Sapling, Orchard, or unknown) typecode.
pub fn arb_shielded_typecode() -> impl Strategy<Value = Typecode> {
prop_oneof![
Just(Typecode::Sapling),
Just(Typecode::Orchard),
@ -179,7 +182,7 @@ mod tests {
/// A strategy to generate an arbitrary valid set of typecodes without
/// duplication and containing only one of P2sh and P2pkh transparent
/// typecodes. The resulting vector will be sorted in encoding order.
fn arb_typecodes() -> impl Strategy<Value = Vec<Typecode>> {
pub fn arb_typecodes() -> impl Strategy<Value = Vec<Typecode>> {
prop::option::of(arb_transparent_typecode()).prop_flat_map(|transparent| {
prop::collection::hash_set(arb_shielded_typecode(), 1..4).prop_map(move |xs| {
let mut typecodes: Vec<_> = xs.into_iter().chain(transparent).collect();
@ -189,7 +192,11 @@ mod tests {
})
}
fn arb_unified_address_for_typecodes(
/// Generates an arbitrary Unified address containing receivers corresponding to the provided
/// set of typecodes.. The receivers of this address are ikely to not represent valid protocol
/// receivers, and should only be used for testing parsing and/or encoding functions that do
/// not concern themselves with the validity of the underlying receivers.
pub fn arb_unified_address_for_typecodes(
typecodes: Vec<Typecode>,
) -> impl Strategy<Value = Vec<Receiver>> {
typecodes
@ -206,11 +213,33 @@ mod tests {
.collect::<Vec<_>>()
}
fn arb_unified_address() -> impl Strategy<Value = Address> {
/// Generates an arbitrary Unified address. The receivers of this address are ikely to not
/// represent valid protocol receivers, and should only be used for testing parsing and/or
/// encoding functions that do not concern themselves with the validity of the underlying
/// receivers.
pub fn arb_unified_address() -> impl Strategy<Value = Address> {
arb_typecodes()
.prop_flat_map(arb_unified_address_for_typecodes)
.prop_map(Address)
}
}
#[cfg(any(test, feature = "test-dependencies"))]
pub mod test_vectors;
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use crate::{
kind::unified::{private::SealedContainer, Container, Encoding},
unified::address::testing::arb_unified_address,
Network,
};
use proptest::{prelude::*, sample::select};
use super::{Address, ParseError, Receiver, Typecode};
proptest! {
#[test]

View File

@ -266,4 +266,89 @@ impl ZcashAddress {
}),
}
}
/// Returns whether this address can receive a memo.
pub fn can_receive_memo(&self) -> bool {
match &self.kind {
AddressKind::Sprout(_) => true,
AddressKind::Sapling(_) => true,
AddressKind::Unified(addr) => addr.can_receive_memo(),
AddressKind::P2pkh(_) => false,
AddressKind::P2sh(_) => false,
AddressKind::Tex(_) => false,
}
}
}
#[cfg(feature = "test-dependencies")]
pub mod testing {
use std::convert::TryInto;
use proptest::{array::uniform20, collection::vec, prelude::any, prop_compose, prop_oneof};
use crate::{unified::address::testing::arb_unified_address, AddressKind, ZcashAddress};
use zcash_protocol::consensus::NetworkType;
prop_compose! {
fn arb_sprout_addr_kind()(
r_bytes in vec(any::<u8>(), 64)
) -> AddressKind {
AddressKind::Sprout(r_bytes.try_into().unwrap())
}
}
prop_compose! {
fn arb_sapling_addr_kind()(
r_bytes in vec(any::<u8>(), 43)
) -> AddressKind {
AddressKind::Sapling(r_bytes.try_into().unwrap())
}
}
prop_compose! {
fn arb_p2pkh_addr_kind()(
r_bytes in uniform20(any::<u8>())
) -> AddressKind {
AddressKind::P2pkh(r_bytes)
}
}
prop_compose! {
fn arb_p2sh_addr_kind()(
r_bytes in uniform20(any::<u8>())
) -> AddressKind {
AddressKind::P2sh(r_bytes)
}
}
prop_compose! {
fn arb_unified_addr_kind()(
uaddr in arb_unified_address()
) -> AddressKind {
AddressKind::Unified(uaddr)
}
}
prop_compose! {
fn arb_tex_addr_kind()(
r_bytes in uniform20(any::<u8>())
) -> AddressKind {
AddressKind::Tex(r_bytes)
}
}
prop_compose! {
pub fn arb_address(net: NetworkType)(
kind in prop_oneof!(
arb_sprout_addr_kind(),
arb_sapling_addr_kind(),
arb_p2pkh_addr_kind(),
arb_p2sh_addr_kind(),
arb_unified_addr_kind(),
arb_tex_addr_kind()
)
) -> ZcashAddress {
ZcashAddress { net, kind }
}
}
}

View File

@ -6,4 +6,25 @@ and this library adheres to Rust's notion of
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
The entries below are relative to the `zcash_client_backend` crate as of
`zcash_client_backend-0.10.0`.
### Added
- `impl From<zcash_address:ConversionError<E>> for Zip321Error`
### Changed
- `zip321::Payment::recipient_address` has type `zcash_address::ZcashAddress`
- `zip321::Payment::without_memo` now takes a `zcash_address::ZcashAddress` for
its `recipient_address` argument.
- Uses of `zcash_primitives::transaction::components::amount::NonNegartiveAmount`
have been replace with `zcash_protocol::value::Zatoshis`. Also, some incorrect
uses of the signed `zcash_primitibves::transaction::components::amount Amount`
type have been corrected via replacement with the `Zatoshis` type.
- The following methods that previously required a
`zcash_primitives::consensus::Parameters` argument to facilitate address
parsing no longer take such an argument.
- `zip321::TransactionRequest::{to_uri, from_uri}`
- `zip321::render::addr_param`
- `zip321::parse::{lead_addr, zcashparam}`
- `zip321::Param::Memo` now boxes its argument.
- `zip321::Param::Addr` now wraps a `zcash_address::ZcashAddress`

View File

@ -14,3 +14,15 @@ rust-version.workspace = true
categories.workspace = true
[dependencies]
zcash_address.workspace = true
zcash_protocol.workspace = true
# - Parsing and Encoding
nom = "7"
base64.workspace = true
percent-encoding.workspace = true
[dev-dependencies]
zcash_address = { workspace = true, features = ["test-dependencies"] }
zcash_protocol = { workspace = true, features = ["test-dependencies"] }
proptest.workspace = true

View File

@ -1,6 +1,6 @@
//! Reference implementation of the ZIP-321 standard for payment requests.
//!
//! This module provides data structures, parsing, and rendering functions
//! This crate provides data structures, parsing, and rendering functions
//! for interpreting and producing valid ZIP 321 URIs.
//!
//! The specification for ZIP 321 URIs may be found at <https://zips.z.cash/zip-0321>
@ -15,13 +15,13 @@ use nom::{
character::complete::char, combinator::all_consuming, multi::separated_list0,
sequence::preceded,
};
use zcash_primitives::{
memo::{self, MemoBytes},
transaction::components::amount::NonNegativeAmount,
};
use zcash_protocol::{consensus, value::BalanceError};
use crate::address::Address;
use zcash_address::{ConversionError, ZcashAddress};
use zcash_protocol::{
memo::{self, MemoBytes},
value::BalanceError,
value::Zatoshis,
};
/// Errors that may be produced in decoding of payment requests.
#[derive(Debug, Clone, PartialEq, Eq)]
@ -45,6 +45,12 @@ pub enum Zip321Error {
ParseError(String),
}
impl<E: Display> From<ConversionError<E>> for Zip321Error {
fn from(value: ConversionError<E>) -> Self {
Zip321Error::ParseError(format!("Address parsing failed: {}", value))
}
}
impl Display for Zip321Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
@ -111,9 +117,9 @@ pub fn memo_from_base64(s: &str) -> Result<MemoBytes, Zip321Error> {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Payment {
/// The payment address to which the payment should be sent.
pub recipient_address: Address,
/// The amount of the payment that is being requested.
pub amount: NonNegativeAmount,
pub recipient_address: ZcashAddress,
/// The value of the payment that is being requested, in zatoshis.
pub amount: Zatoshis,
/// A memo that, if included, must be provided with the payment.
/// If a memo is present and [`recipient_address`] is not a shielded
/// address, the wallet should report an error.
@ -132,7 +138,7 @@ pub struct Payment {
impl Payment {
/// Constructs a new [`Payment`] paying the given address the specified amount.
pub fn without_memo(recipient_address: Address, amount: NonNegativeAmount) -> Self {
pub fn without_memo(recipient_address: ZcashAddress, amount: Zatoshis) -> Self {
Self {
recipient_address,
amount,
@ -145,7 +151,7 @@ impl Payment {
/// 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(&mut self) {
pub(crate) fn normalize(&mut self) {
self.other_params.sort();
}
}
@ -182,10 +188,7 @@ impl TransactionRequest {
// Enforce validity requirements.
if !request.payments.is_empty() {
// It doesn't matter what params we use here, as none of the validity
// requirements depend on them.
let params = consensus::MAIN_NETWORK;
TransactionRequest::from_uri(&params, &request.to_uri(&params))?;
TransactionRequest::from_uri(&request.to_uri())?;
}
Ok(request)
@ -218,19 +221,19 @@ 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, BalanceError> {
pub fn total(&self) -> Result<Zatoshis, BalanceError> {
self.payments
.values()
.map(|p| p.amount)
.fold(Ok(NonNegativeAmount::ZERO), |acc, a| {
.fold(Ok(Zatoshis::ZERO), |acc, a| {
(acc? + a).ok_or(BalanceError::Overflow)
})
}
/// 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(&mut self) {
for p in self.payments.values_mut() {
pub(crate) fn normalize(&mut self) {
for p in &mut self.payments.values_mut() {
p.normalize();
}
}
@ -238,10 +241,7 @@ impl TransactionRequest {
/// A utility for use in tests to help check round-trip serialization properties.
/// by comparing a two transaction requests for equality after normalization.
#[cfg(test)]
pub(in crate::zip321) fn normalize_and_eq(
a: &mut TransactionRequest,
b: &mut TransactionRequest,
) -> bool {
pub(crate) fn normalize_and_eq(a: &mut TransactionRequest, b: &mut TransactionRequest) -> bool {
a.normalize();
b.normalize();
@ -251,7 +251,7 @@ impl TransactionRequest {
/// Convert this request to a URI string.
///
/// Returns None if the payment request is empty.
pub fn to_uri<P: consensus::Parameters>(&self, params: &P) -> String {
pub fn to_uri(&self) -> String {
fn payment_params(
payment: &Payment,
payment_index: Option<usize>,
@ -294,7 +294,7 @@ impl TransactionRequest {
format!(
"zcash:{}{}{}",
payment.recipient_address.encode(params),
payment.recipient_address.encode(),
if query_params.is_empty() { "" } else { "?" },
query_params.join("&")
)
@ -307,7 +307,7 @@ impl TransactionRequest {
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, idx)))
.chain(Some(render::addr_param(&primary_address, idx)))
.chain(payment_params(payment, idx))
})
.collect::<Vec<String>>();
@ -318,9 +318,9 @@ impl TransactionRequest {
}
/// Parse the provided URI to a payment request value.
pub fn from_uri<P: consensus::Parameters>(params: &P, uri: &str) -> Result<Self, Zip321Error> {
pub fn from_uri(uri: &str) -> Result<Self, Zip321Error> {
// Parse the leading zcash:<address>
let (rest, primary_addr_param) = parse::lead_addr(params)(uri)
let (rest, primary_addr_param) = parse::lead_addr(uri)
.map_err(|e| Zip321Error::ParseError(format!("Error parsing lead address: {}", e)))?;
// Parse the remaining parameters as an undifferentiated list
@ -329,7 +329,7 @@ impl TransactionRequest {
} else {
all_consuming(preceded(
char('?'),
separated_list0(char('&'), parse::zcashparam(params)),
separated_list0(char('&'), parse::zcashparam),
))(rest)
.map_err(|e| {
Zip321Error::ParseError(format!("Error parsing query parameters: {}", e))
@ -372,13 +372,13 @@ impl TransactionRequest {
mod render {
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
use zcash_primitives::{
consensus, transaction::components::amount::NonNegativeAmount,
transaction::components::amount::COIN,
use zcash_address::ZcashAddress;
use zcash_protocol::{
memo::MemoBytes,
value::{Zatoshis, COIN},
};
use super::{memo_to_base64, Address, MemoBytes};
use super::memo_to_base64;
/// The set of ASCII characters that must be percent-encoded according
/// to the definition of ZIP 321. This is the complement of the subset of
@ -418,17 +418,13 @@ mod render {
/// Constructs an "address" key/value pair containing the encoded recipient address
/// at the specified parameter index.
pub fn addr_param<P: consensus::Parameters>(
params: &P,
addr: &Address,
idx: Option<usize>,
) -> String {
format!("address{}={}", param_index(idx), addr.encode(params))
pub fn addr_param(addr: &ZcashAddress, idx: Option<usize>) -> String {
format!("address{}={}", param_index(idx), addr.encode())
}
/// Converts a [`NonNegativeAmount`] value to a correctly formatted decimal ZEC
/// string for inclusion in a ZIP 321 URI.
pub fn amount_str(amount: NonNegativeAmount) -> String {
/// Converts an [`Zatoshis`] value to a correctly formatted decimal ZEC
/// value for inclusion in a ZIP 321 URI.
pub fn amount_str(amount: Zatoshis) -> String {
let coins = u64::from(amount) / COIN;
let zats = u64::from(amount) % COIN;
if zats == 0 {
@ -442,7 +438,7 @@ mod render {
/// Constructs an "amount" key/value pair containing the encoded ZEC amount
/// at the specified parameter index.
pub fn amount_param(amount: NonNegativeAmount, idx: Option<usize>) -> String {
pub fn amount_param(amount: Zatoshis, idx: Option<usize>) -> String {
format!("amount{}={}", param_index(idx), amount_str(amount))
}
@ -475,23 +471,22 @@ mod parse {
AsChar, IResult, InputTakeAtPosition,
};
use percent_encoding::percent_decode;
use zcash_primitives::{
consensus, transaction::components::amount::NonNegativeAmount,
transaction::components::amount::COIN,
};
use zcash_address::ZcashAddress;
use zcash_protocol::value::BalanceError;
use zcash_protocol::{
memo::MemoBytes,
value::{Zatoshis, COIN},
};
use crate::address::Address;
use super::{memo_from_base64, MemoBytes, Payment, Zip321Error};
use super::{memo_from_base64, Payment, Zip321Error};
/// A data type that defines the possible parameter types which may occur within a
/// ZIP 321 URI.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Param {
Addr(Box<Address>),
Amount(NonNegativeAmount),
Memo(MemoBytes),
Addr(Box<ZcashAddress>),
Amount(Zatoshis),
Memo(Box<MemoBytes>),
Label(String),
Message(String),
Other(String, String),
@ -551,7 +546,7 @@ mod parse {
let mut payment = Payment {
recipient_address: *addr.ok_or(Zip321Error::RecipientMissing(i))?,
amount: NonNegativeAmount::ZERO,
amount: Zatoshis::ZERO,
memo: None,
label: None,
message: None,
@ -561,11 +556,13 @@ mod parse {
for v in vs {
match v {
Param::Amount(a) => payment.amount = a,
Param::Memo(m) => match payment.recipient_address {
Address::Sapling(_) | Address::Unified(_) => payment.memo = Some(m),
Address::Transparent(_) => return Err(Zip321Error::TransparentMemo(i)),
},
Param::Memo(m) => {
if payment.recipient_address.can_receive_memo() {
payment.memo = Some(*m);
} else {
return Err(Zip321Error::TransparentMemo(i));
}
}
Param::Label(m) => payment.label = Some(m),
Param::Message(m) => payment.message = Some(m),
Param::Other(n, m) => payment.other_params.push((n, m)),
@ -577,40 +574,34 @@ mod parse {
}
/// Parses and consumes the leading "zcash:\[address\]" from a ZIP 321 URI.
pub fn lead_addr<P: consensus::Parameters>(
params: &P,
) -> impl Fn(&str) -> IResult<&str, Option<IndexedParam>> + '_ {
move |input: &str| {
map_opt(
preceded(tag("zcash:"), take_till(|c| c == '?')),
|addr_str: &str| {
if addr_str.is_empty() {
Some(None) // no address is ok, so wrap in `Some`
} else {
// `decode` returns `None` on error, which we want to
// then cause `map_opt` to fail.
Address::decode(params, addr_str).map(|a| {
pub fn lead_addr(input: &str) -> IResult<&str, Option<IndexedParam>> {
map_opt(
preceded(tag("zcash:"), take_till(|c| c == '?')),
|addr_str: &str| {
if addr_str.is_empty() {
Some(None) // no address is ok, so wrap in `Some`
} else {
// `try_from_encoded(..).ok()` returns `None` on error, which we want to then
// cause `map_opt` to fail.
dbg!(ZcashAddress::try_from_encoded(addr_str)
.map(|a| {
Some(IndexedParam {
param: Param::Addr(Box::new(a)),
payment_index: 0,
})
})
}
},
)(input)
}
.ok())
}
},
)(input)
}
/// The primary parser for <name>=<value> query-string parameter pair.
pub fn zcashparam<P: consensus::Parameters>(
params: &P,
) -> impl Fn(&str) -> IResult<&str, IndexedParam> + '_ {
move |input| {
map_res(
separated_pair(indexed_name, char('='), recognize(qchars)),
move |r| to_indexed_param(params, r),
)(input)
}
pub fn zcashparam(input: &str) -> IResult<&str, IndexedParam> {
map_res(
separated_pair(indexed_name, char('='), recognize(qchars)),
to_indexed_param,
)(input)
}
/// Extension for the `alphanumeric0` parser which extends that parser
@ -652,7 +643,7 @@ mod parse {
}
/// Parses a value in decimal ZEC.
pub fn parse_amount(input: &str) -> IResult<&str, NonNegativeAmount> {
pub fn parse_amount(input: &str) -> IResult<&str, Zatoshis> {
map_res(
all_consuming(tuple((
digit1,
@ -678,28 +669,29 @@ mod parse {
.checked_mul(COIN)
.and_then(|coin_zats| coin_zats.checked_add(zats))
.ok_or(BalanceError::Overflow)
.and_then(NonNegativeAmount::from_u64)
.map_err(|_| format!("Not a valid amount: {} ZEC", input))
.and_then(Zatoshis::from_u64)
.map_err(|_| format!("Not a valid zat amount: {}.{}", coins, zats))
},
)(input)
}
fn to_indexed_param<'a, P: consensus::Parameters>(
params: &'a P,
fn to_indexed_param(
((name, iopt), value): ((&str, Option<&str>), &str),
) -> Result<IndexedParam, String> {
let param = match name {
"address" => Address::decode(params, value)
"address" => ZcashAddress::try_from_encoded(value)
.map(Box::new)
.map(Param::Addr)
.ok_or(format!(
"Could not interpret {} as a valid Zcash address.",
value
)),
.map_err(|err| {
format!(
"Could not interpret {} as a valid Zcash address: {}",
value, err
)
}),
"amount" => parse_amount(value)
.map_err(|e| e.to_string())
.map(|(_, amt)| Param::Amount(amt)),
.map(|(_, a)| Param::Amount(a)),
"label" => percent_decode(value.as_bytes())
.decode_utf8()
@ -712,6 +704,7 @@ mod parse {
.map_err(|e| e.to_string()),
"memo" => memo_from_base64(value)
.map(Box::new)
.map(Param::Memo)
.map_err(|e| format!("Decoded memo was invalid: {:?}", e)),
@ -743,25 +736,13 @@ pub mod testing {
use proptest::collection::vec;
use proptest::option;
use proptest::prelude::{any, prop_compose};
use zcash_keys::address::testing::arb_addr;
use zcash_keys::keys::UnifiedAddressRequest;
use zcash_primitives::{
consensus::TEST_NETWORK, transaction::components::amount::testing::arb_nonnegative_amount,
};
use crate::address::Address;
use zcash_address::testing::arb_address;
use zcash_protocol::{consensus::NetworkType, value::testing::arb_zatoshis};
use super::{MemoBytes, Payment, TransactionRequest};
pub const VALID_PARAMNAME: &str = "[a-zA-Z][a-zA-Z0-9+-]*";
#[cfg(feature = "transparent-inputs")]
const TRANSPARENT_INPUTS_ENABLED: bool = true;
#[cfg(not(feature = "transparent-inputs"))]
const TRANSPARENT_INPUTS_ENABLED: bool = false;
pub(crate) const UA_REQUEST: UnifiedAddressRequest =
UnifiedAddressRequest::unsafe_new(false, true, TRANSPARENT_INPUTS_ENABLED);
prop_compose! {
pub fn arb_valid_memo()(bytes in vec(any::<u8>(), 0..512)) -> MemoBytes {
MemoBytes::from_bytes(&bytes).unwrap()
@ -769,24 +750,20 @@ pub mod testing {
}
prop_compose! {
pub fn arb_zip321_payment()(
recipient_address in arb_addr(UA_REQUEST),
amount in arb_nonnegative_amount(),
pub fn arb_zip321_payment(network: NetworkType)(
recipient_address in arb_address(network),
amount in arb_zatoshis(),
memo in option::of(arb_valid_memo()),
message in option::of(any::<String>()),
label in option::of(any::<String>()),
// prevent duplicates by generating a set rather than a vec
other_params in btree_map(VALID_PARAMNAME, any::<String>(), 0..3),
) -> Payment {
let is_shielded = match recipient_address {
Address::Transparent(_) => false,
Address::Sapling(_) | Address::Unified(_) => true,
};
let memo = memo.filter(|_| recipient_address.can_receive_memo());
Payment {
recipient_address,
amount,
memo: memo.filter(|_| is_shielded),
memo,
label,
message,
other_params: other_params.into_iter().collect(),
@ -795,7 +772,9 @@ pub mod testing {
}
prop_compose! {
pub fn arb_zip321_request()(payments in btree_map(0usize..10000, arb_zip321_payment(), 1..10)) -> TransactionRequest {
pub fn arb_zip321_request(network: NetworkType)(
payments in btree_map(0usize..10000, arb_zip321_payment(network), 1..10)
) -> TransactionRequest {
let mut req = TransactionRequest::from_indexed(payments).unwrap();
req.normalize(); // just to make test comparisons easier
req
@ -803,7 +782,9 @@ pub mod testing {
}
prop_compose! {
pub fn arb_zip321_request_sequential()(payments in vec(arb_zip321_payment(), 1..10)) -> TransactionRequest {
pub fn arb_zip321_request_sequential(network: NetworkType)(
payments in vec(arb_zip321_payment(network), 1..10)
) -> TransactionRequest {
let mut req = TransactionRequest::new(payments).unwrap();
req.normalize(); // just to make test comparisons easier
req
@ -811,16 +792,16 @@ pub mod testing {
}
prop_compose! {
pub fn arb_zip321_uri()(req in arb_zip321_request()) -> String {
req.to_uri(&TEST_NETWORK)
pub fn arb_zip321_uri(network: NetworkType)(req in arb_zip321_request(network)) -> String {
req.to_uri()
}
}
prop_compose! {
pub fn arb_addr_str()(
recipient_address in arb_addr(UA_REQUEST)
pub fn arb_addr_str(network: NetworkType)(
recipient_address in arb_address(network)
) -> String {
recipient_address.encode(&TEST_NETWORK)
recipient_address.encode()
}
}
}
@ -830,29 +811,27 @@ mod tests {
use proptest::prelude::{any, proptest};
use std::str::FromStr;
use zcash_keys::address::testing::arb_addr;
use zcash_primitives::{
memo::Memo,
transaction::components::amount::{testing::arb_nonnegative_amount, NonNegativeAmount},
use zcash_address::{testing::arb_address, ZcashAddress};
use zcash_protocol::{
consensus::NetworkType,
memo::{Memo, MemoBytes},
value::{testing::arb_zatoshis, Zatoshis},
};
use zcash_protocol::consensus::{NetworkConstants, NetworkType, TEST_NETWORK};
#[cfg(feature = "local-consensus")]
use zcash_primitives::{local_consensus::LocalNetwork, BlockHeight};
use crate::{address::Address, encoding::decode_payment_address, zip321::testing::UA_REQUEST};
use zcash_protocol::{local_consensus::LocalNetwork, BlockHeight};
use super::{
memo_from_base64, memo_to_base64,
parse::{parse_amount, zcashparam, Param},
render::{amount_str, memo_param, str_param},
testing::{arb_addr_str, arb_valid_memo, arb_zip321_request, arb_zip321_uri},
MemoBytes, Payment, TransactionRequest,
Payment, TransactionRequest,
};
fn check_roundtrip(req: TransactionRequest) {
let req_uri = req.to_uri(&TEST_NETWORK);
let parsed = TransactionRequest::from_uri(&TEST_NETWORK, &req_uri).unwrap();
let req_uri = req.to_uri();
let parsed = TransactionRequest::from_uri(&req_uri).unwrap();
assert_eq!(parsed, req);
}
@ -861,7 +840,7 @@ mod tests {
let amounts = vec![1u64, 1000u64, 100000u64, 100000000u64, 100000000000u64];
for amt_u64 in amounts {
let amt = NonNegativeAmount::from_u64(amt_u64).unwrap();
let amt = Zatoshis::const_from_u64(amt_u64);
let amt_str = amount_str(amt);
assert_eq!(amt, parse_amount(&amt_str).unwrap().1);
}
@ -871,20 +850,20 @@ mod tests {
fn test_zip321_parse_empty_message() {
let fragment = "message=";
let result = zcashparam(&TEST_NETWORK)(fragment).unwrap().1.param;
let result = zcashparam(fragment).unwrap().1.param;
assert_eq!(result, Param::Message("".to_string()));
}
#[test]
fn test_zip321_parse_simple() {
let uri = "zcash:ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k?amount=3768769.02796286&message=";
let parse_result = TransactionRequest::from_uri(&TEST_NETWORK, uri).unwrap();
let parse_result = TransactionRequest::from_uri(uri).unwrap();
let expected = TransactionRequest::new(
vec![
Payment {
recipient_address: Address::Sapling(decode_payment_address(NetworkType::Test.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()),
amount: NonNegativeAmount::const_from_u64(376876902796286),
recipient_address: ZcashAddress::try_from_encoded("ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap(),
amount: Zatoshis::const_from_u64(376876902796286),
memo: None,
label: None,
message: Some("".to_string()),
@ -899,13 +878,13 @@ mod tests {
#[test]
fn test_zip321_parse_no_query_params() {
let uri = "zcash:ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k";
let parse_result = TransactionRequest::from_uri(&TEST_NETWORK, uri).unwrap();
let parse_result = TransactionRequest::from_uri(uri).unwrap();
let expected = TransactionRequest::new(
vec![
Payment {
recipient_address: Address::Sapling(decode_payment_address(NetworkType::Test.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()),
amount: NonNegativeAmount::ZERO,
recipient_address: ZcashAddress::try_from_encoded("ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap(),
amount: Zatoshis::ZERO,
memo: None,
label: None,
message: None,
@ -922,8 +901,8 @@ mod tests {
let req = TransactionRequest::new(
vec![
Payment {
recipient_address: Address::Sapling(decode_payment_address(NetworkType::Test.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()),
amount: NonNegativeAmount::ZERO,
recipient_address: ZcashAddress::try_from_encoded("ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap(),
amount: Zatoshis::ZERO,
memo: None,
label: None,
message: Some("".to_string()),
@ -957,48 +936,48 @@ mod tests {
#[test]
fn test_zip321_spec_valid_examples() {
let valid_0 = "zcash:";
let v0r = TransactionRequest::from_uri(&TEST_NETWORK, valid_0).unwrap();
let v0r = TransactionRequest::from_uri(valid_0).unwrap();
assert!(v0r.payments.is_empty());
let valid_0 = "zcash:?";
let v0r = TransactionRequest::from_uri(&TEST_NETWORK, valid_0).unwrap();
let v0r = TransactionRequest::from_uri(valid_0).unwrap();
assert!(v0r.payments.is_empty());
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();
let v1r = TransactionRequest::from_uri(valid_1).unwrap();
assert_eq!(
v1r.payments.get(&0).map(|p| p.amount),
Some(NonNegativeAmount::const_from_u64(100000000))
Some(Zatoshis::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();
let mut v2r = TransactionRequest::from_uri(valid_2).unwrap();
v2r.normalize();
assert_eq!(
v2r.payments.get(&0).map(|p| p.amount),
Some(NonNegativeAmount::const_from_u64(12345600000))
Some(Zatoshis::const_from_u64(12345600000))
);
assert_eq!(
v2r.payments.get(&1).map(|p| p.amount),
Some(NonNegativeAmount::const_from_u64(78900000))
Some(Zatoshis::const_from_u64(78900000))
);
// valid; amount just less than MAX_MONEY
// 20999999.99999999
let valid_3 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=20999999.99999999";
let v3r = TransactionRequest::from_uri(&TEST_NETWORK, valid_3).unwrap();
let v3r = TransactionRequest::from_uri(valid_3).unwrap();
assert_eq!(
v3r.payments.get(&0).map(|p| p.amount),
Some(NonNegativeAmount::const_from_u64(2099999999999999u64))
Some(Zatoshis::const_from_u64(2099999999999999))
);
// valid; MAX_MONEY
// 21000000
let valid_4 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=21000000";
let v4r = TransactionRequest::from_uri(&TEST_NETWORK, valid_4).unwrap();
let v4r = TransactionRequest::from_uri(valid_4).unwrap();
assert_eq!(
v4r.payments.get(&0).map(|p| p.amount),
Some(NonNegativeAmount::const_from_u64(2100000000000000u64))
Some(Zatoshis::const_from_u64(2100000000000000))
);
}
@ -1019,7 +998,7 @@ mod tests {
let v1r = TransactionRequest::from_uri(&params, valid_1).unwrap();
assert_eq!(
v1r.payments.get(&0).map(|p| p.amount),
Some(NonNegativeAmount::const_from_u64(100000000))
Some(Zatoshis::const_from_u64(100000000))
);
}
@ -1027,91 +1006,91 @@ mod tests {
fn test_zip321_spec_invalid_examples() {
// invalid; empty string
let invalid_0 = "";
let i0r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_0);
let i0r = TransactionRequest::from_uri(invalid_0);
assert!(i0r.is_err());
// invalid; missing `address=`
let invalid_1 = "zcash:?amount=3491405.05201255&address.1=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.1=5740296.87793245";
let i1r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_1);
let i1r = TransactionRequest::from_uri(invalid_1);
assert!(i1r.is_err());
// invalid; missing `address.1=`
let invalid_2 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=1&amount.1=2&address.2=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez";
let i2r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_2);
let i2r = TransactionRequest::from_uri(invalid_2);
assert!(i2r.is_err());
// invalid; `address.0=` and `amount.0=` are not permitted (leading 0s).
let invalid_3 = "zcash:?address.0=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.0=2";
let i3r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_3);
let i3r = TransactionRequest::from_uri(invalid_3);
assert!(i3r.is_err());
// invalid; duplicate `amount=` field
let invalid_4 =
"zcash:?amount=1.234&amount=2.345&address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU";
let i4r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_4);
let i4r = TransactionRequest::from_uri(invalid_4);
assert!(i4r.is_err());
// invalid; duplicate `amount.1=` field
let invalid_5 =
"zcash:?amount.1=1.234&amount.1=2.345&address.1=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU";
let i5r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_5);
let i5r = TransactionRequest::from_uri(invalid_5);
assert!(i5r.is_err());
//invalid; memo associated with t-addr
let invalid_6 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=123.456&memo=eyAia2V5IjogIlRoaXMgaXMgYSBKU09OLXN0cnVjdHVyZWQgbWVtby4iIH0&address.1=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.1=0.789&memo.1=VGhpcyBpcyBhIHVuaWNvZGUgbWVtbyDinKjwn6aE8J-PhvCfjok";
let i6r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_6);
let i6r = TransactionRequest::from_uri(invalid_6);
assert!(i6r.is_err());
// invalid; amount component exceeds an i64
// 9223372036854775808 = i64::MAX + 1
let invalid_7 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=9223372036854775808";
let i7r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_7);
let i7r = TransactionRequest::from_uri(invalid_7);
assert!(i7r.is_err());
// invalid; amount component wraps into a valid small positive i64
// 18446744073709551624
let invalid_7a = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=18446744073709551624";
let i7ar = TransactionRequest::from_uri(&TEST_NETWORK, invalid_7a);
let i7ar = TransactionRequest::from_uri(invalid_7a);
assert!(i7ar.is_err());
// invalid; amount component is MAX_MONEY
// 21000000.00000001
let invalid_8 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=21000000.00000001";
let i8r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_8);
let i8r = TransactionRequest::from_uri(invalid_8);
assert!(i8r.is_err());
// invalid; negative amount
let invalid_9 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=-1";
let i9r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_9);
let i9r = TransactionRequest::from_uri(invalid_9);
assert!(i9r.is_err());
// invalid; parameter index too large
let invalid_10 =
"zcash:?amount.10000=1.23&address.10000=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU";
let i10r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_10);
let i10r = TransactionRequest::from_uri(invalid_10);
assert!(i10r.is_err());
// invalid: bad amount format
let invalid_11 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=123.";
let i11r = TransactionRequest::from_uri(&TEST_NETWORK, invalid_11);
let i11r = TransactionRequest::from_uri(invalid_11);
assert!(i11r.is_err());
}
proptest! {
#[test]
fn prop_zip321_roundtrip_address(addr in arb_addr(UA_REQUEST)) {
let a = addr.encode(&TEST_NETWORK);
assert_eq!(Address::decode(&TEST_NETWORK, &a), Some(addr));
fn prop_zip321_roundtrip_address(addr in arb_address(NetworkType::Test)) {
let a = addr.encode();
assert_eq!(ZcashAddress::try_from_encoded(&a), Ok(addr));
}
#[test]
fn prop_zip321_roundtrip_address_str(a in arb_addr_str()) {
let addr = Address::decode(&TEST_NETWORK, &a).unwrap();
assert_eq!(addr.encode(&TEST_NETWORK), a);
fn prop_zip321_roundtrip_address_str(a in arb_addr_str(NetworkType::Test)) {
let addr = ZcashAddress::try_from_encoded(&a).unwrap();
assert_eq!(addr.encode(), a);
}
#[test]
fn prop_zip321_roundtrip_amount(amt in arb_nonnegative_amount()) {
fn prop_zip321_roundtrip_amount(amt in arb_zatoshis()) {
let amt_str = amount_str(amt);
assert_eq!(amt, parse_amount(&amt_str).unwrap().1);
}
@ -1120,7 +1099,7 @@ mod tests {
fn prop_zip321_roundtrip_str_param(
message in any::<String>(), i in proptest::option::of(0usize..2000)) {
let fragment = str_param("message", &message, i);
let (rest, iparam) = zcashparam(&TEST_NETWORK)(&fragment).unwrap();
let (rest, iparam) = zcashparam(&fragment).unwrap();
assert_eq!(rest, "");
assert_eq!(iparam.param, Param::Message(message));
assert_eq!(iparam.payment_index, i.unwrap_or(0));
@ -1130,24 +1109,24 @@ mod tests {
fn prop_zip321_roundtrip_memo_param(
memo in arb_valid_memo(), i in proptest::option::of(0usize..2000)) {
let fragment = memo_param(&memo, i);
let (rest, iparam) = zcashparam(&TEST_NETWORK)(&fragment).unwrap();
let (rest, iparam) = zcashparam(&fragment).unwrap();
assert_eq!(rest, "");
assert_eq!(iparam.param, Param::Memo(memo));
assert_eq!(iparam.param, Param::Memo(Box::new(memo)));
assert_eq!(iparam.payment_index, i.unwrap_or(0));
}
#[test]
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();
fn prop_zip321_roundtrip_request(mut req in arb_zip321_request(NetworkType::Test)) {
let req_uri = req.to_uri();
let mut parsed = TransactionRequest::from_uri(&req_uri).unwrap();
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();
fn prop_zip321_roundtrip_uri(uri in arb_zip321_uri(NetworkType::Test)) {
let mut parsed = TransactionRequest::from_uri(&uri).unwrap();
parsed.normalize();
let serialized = parsed.to_uri(&TEST_NETWORK);
let serialized = parsed.to_uri();
assert_eq!(serialized, uri)
}
}

View File

@ -14,6 +14,12 @@ and this library adheres to Rust's notion of
- `testing` module
- `zcash_client_backend::sync` module, behind the `sync` feature flag.
### Changed
- `zcash_client_backend::zip321` has been extracted to, and is now a reexport
of the root module of the `zip321` crate. Several of the APIs of this module
have changed as a consequence of this extraction; please see the `zip321`
CHANGELOG for details.
## [0.12.1] - 2024-03-27
### Fixed

View File

@ -41,6 +41,7 @@ zcash_note_encryption.workspace = true
zcash_primitives.workspace = true
zcash_protocol.workspace = true
zip32.workspace = true
zip321.workspace = true
# Dependencies exposed in a public API:
# (Breaking upgrades to these require a breaking upgrade to this crate.)

View File

@ -72,6 +72,7 @@ pub mod proto;
pub mod scan;
pub mod scanning;
pub mod wallet;
pub use zip321;
#[cfg(feature = "sync")]
pub mod sync;