Skip to main content

zip321/
lib.rs

1//! Reference implementation of the ZIP-321 standard for payment requests.
2//!
3//! This crate provides data structures, parsing, and rendering functions
4//! for interpreting and producing valid ZIP 321 URIs.
5//!
6//! The specification for ZIP 321 URIs may be found at <https://zips.z.cash/zip-0321>
7use core::fmt::Debug;
8use std::{
9    collections::BTreeMap,
10    fmt::{self, Display},
11};
12
13use base64::{Engine, prelude::BASE64_URL_SAFE_NO_PAD};
14use nom::{
15    character::complete::char, combinator::all_consuming, multi::separated_list0,
16    sequence::preceded,
17};
18
19use zcash_address::{ConversionError, ZcashAddress};
20use zcash_protocol::{
21    memo::{self, MemoBytes},
22    value::BalanceError,
23    value::Zatoshis,
24};
25
26/// Errors that may be produced in constructing a [`Payment`].
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum PaymentError {
29    /// A memo was provided for a recipient address that cannot receive a memo.
30    TransparentMemo,
31    /// A zero-valued output was requested for a transparent recipient address, which is
32    /// disallowed by the Zcash consensus rules.
33    ZeroValuedTransparentOutput,
34    /// The recipient is a Sprout address, which is not supported in payment requests per
35    /// ZIP 321.
36    SproutAddress,
37}
38
39impl Display for PaymentError {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            PaymentError::TransparentMemo => {
43                write!(f, "Cannot send a memo to a transparent recipient address")
44            }
45            PaymentError::ZeroValuedTransparentOutput => write!(
46                f,
47                "Zero-valued transparent outputs are disallowed by consensus"
48            ),
49            PaymentError::SproutAddress => write!(
50                f,
51                "Sprout recipients are not supported in payment requests (ZIP 321)"
52            ),
53        }
54    }
55}
56
57impl std::error::Error for PaymentError {}
58
59impl PaymentError {
60    /// Adds a payment index to this error to produce a [`Zip321Error`].
61    pub fn with_index(self, index: usize) -> Zip321Error {
62        match self {
63            PaymentError::TransparentMemo => Zip321Error::TransparentMemo(index),
64            PaymentError::ZeroValuedTransparentOutput => {
65                Zip321Error::ZeroValuedTransparentOutput(index)
66            }
67            PaymentError::SproutAddress => Zip321Error::SproutRecipient(index),
68        }
69    }
70}
71
72/// Errors that may be produced in decoding of payment requests.
73#[derive(Debug, Clone, PartialEq, Eq)]
74#[non_exhaustive]
75pub enum Zip321Error {
76    /// A memo field in the ZIP 321 URI was not properly base-64 encoded
77    InvalidBase64(base64::DecodeError),
78    /// A memo value exceeded 512 bytes in length or could not be interpreted as a UTF-8 string
79    /// when using a valid UTF-8 lead byte.
80    MemoBytesError(memo::Error),
81    /// The ZIP 321 request included more payments than can be created within a single Zcash
82    /// transaction. The wrapped value is the number of payments in the request.
83    TooManyPayments(usize),
84    /// Parsing encountered a duplicate ZIP 321 URI parameter for the returned payment index.
85    DuplicateParameter(parse::Param, usize),
86    /// The payment at the wrapped index attempted to include a memo when sending to a
87    /// transparent recipient address, which is not supported by the protocol.
88    TransparentMemo(usize),
89    /// The payment at the wrapped index requested a zero-valued output to a transparent recipient
90    /// address, which is disallowed by the Zcash consensus rules.
91    ZeroValuedTransparentOutput(usize),
92    /// The payment at the wrapped index requests payment to a Sprout address, which is not
93    /// supported in payment requests per ZIP 321.
94    SproutRecipient(usize),
95    /// The payment at the wrapped index did not include a recipient address.
96    RecipientMissing(usize),
97    /// The ZIP 321 URI was malformed and failed to parse.
98    ParseError(String),
99}
100
101impl<E: Display> From<ConversionError<E>> for Zip321Error {
102    fn from(value: ConversionError<E>) -> Self {
103        Zip321Error::ParseError(format!("Address parsing failed: {value}"))
104    }
105}
106
107impl Display for Zip321Error {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        match self {
110            Zip321Error::InvalidBase64(err) => {
111                write!(f, "Memo value was not correctly base64-encoded: {err:?}")
112            }
113            Zip321Error::MemoBytesError(err) => write!(
114                f,
115                "Memo exceeded maximum length or violated UTF-8 encoding restrictions: {err:?}"
116            ),
117            Zip321Error::TooManyPayments(n) => write!(
118                f,
119                "Cannot create a Zcash transaction containing {n} payments"
120            ),
121            Zip321Error::DuplicateParameter(param, idx) => write!(
122                f,
123                "There is a duplicate {} parameter at index {}",
124                param.name(),
125                idx
126            ),
127            Zip321Error::TransparentMemo(idx) => write!(
128                f,
129                "Payment {idx} is invalid: cannot send a memo to a transparent recipient address"
130            ),
131            Zip321Error::ZeroValuedTransparentOutput(idx) => write!(
132                f,
133                "Payment {idx} is invalid: zero-valued transparent outputs are disallowed by consensus"
134            ),
135            Zip321Error::SproutRecipient(idx) => write!(
136                f,
137                "Payment {idx} is invalid: Sprout recipients are not supported in payment requests (ZIP 321)"
138            ),
139            Zip321Error::RecipientMissing(idx) => {
140                write!(f, "Payment {idx} is missing its recipient address")
141            }
142            Zip321Error::ParseError(s) => write!(f, "Parse failure: {s}"),
143        }
144    }
145}
146
147impl std::error::Error for Zip321Error {
148    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
149        match self {
150            Zip321Error::InvalidBase64(err) => Some(err),
151            Zip321Error::MemoBytesError(err) => Some(err),
152            _ => None,
153        }
154    }
155}
156
157/// Converts a [`MemoBytes`] value to a ZIP 321 compatible base64-encoded string.
158///
159/// [`MemoBytes`]: zcash_protocol::memo::MemoBytes
160pub fn memo_to_base64(memo: &MemoBytes) -> String {
161    BASE64_URL_SAFE_NO_PAD.encode(memo.as_slice())
162}
163
164/// Parse a [`MemoBytes`] value from a ZIP 321 compatible base64-encoded string.
165///
166/// [`MemoBytes`]: zcash_protocol::memo::MemoBytes
167pub fn memo_from_base64(s: &str) -> Result<MemoBytes, Zip321Error> {
168    BASE64_URL_SAFE_NO_PAD
169        .decode(s)
170        .map_err(Zip321Error::InvalidBase64)
171        .and_then(|b| MemoBytes::from_bytes(&b).map_err(Zip321Error::MemoBytesError))
172}
173
174/// A single payment being requested.
175#[derive(Debug, Clone, PartialEq, Eq)]
176pub struct Payment {
177    /// The address to which the payment should be sent.
178    recipient_address: ZcashAddress,
179    /// The amount of the payment that is being requested, if any.
180    amount: Option<Zatoshis>,
181    /// A memo that, if included, must be provided with the payment.
182    /// If a memo is present and [`recipient_address`] is not a shielded
183    /// address, the wallet should report an error.
184    ///
185    /// [`recipient_address`]: #structfield.recipient_address
186    memo: Option<MemoBytes>,
187    /// A human-readable label for this payment within the larger structure
188    /// of the transaction request.
189    label: Option<String>,
190    /// A human-readable message to be displayed to the user describing the
191    /// purpose of this payment.
192    message: Option<String>,
193    /// A list of other arbitrary key/value pairs associated with this payment.
194    other_params: Vec<(String, String)>,
195}
196
197impl Payment {
198    /// Constructs a new [`Payment`] from its constituent parts.
199    ///
200    /// # Parameters
201    /// - `recipient_address`: The address to which the payment should be sent.
202    /// - `amount`: The amount of the payment that is being requested, if any. If no amount is
203    ///   provided, this indicates that the sender of should specify the amount.
204    /// - `memo`: A memo that, if included, must be provided with the payment. If a memo is present
205    ///   and `recipient_address` is not a shielded address, the wallet should report an error.
206    /// - `label` A human-readable label for this payment (usually identifying information
207    ///   associated with the recipient address) within the larger structure of the transaction
208    ///   request.
209    /// - `message`: A human-readable message to be displayed to the user describing the
210    ///   purpose of this payment within the larger structure of the transaction request.
211    /// - `other_params`: A list of other arbitrary key/value pairs associated with this payment.
212    ///
213    /// Returns an error if the recipient is a Sprout address, if the payment requests that a
214    /// memo be sent to a recipient that cannot receive a memo, or if a zero-valued output would
215    /// be sent to a transparent address.
216    pub fn new(
217        recipient_address: ZcashAddress,
218        amount: Option<Zatoshis>,
219        memo: Option<MemoBytes>,
220        label: Option<String>,
221        message: Option<String>,
222        other_params: Vec<(String, String)>,
223    ) -> Result<Self, PaymentError> {
224        if recipient_address.is_sprout() {
225            Err(PaymentError::SproutAddress)
226        } else if memo.is_some() && !recipient_address.can_receive_memo() {
227            Err(PaymentError::TransparentMemo)
228        } else if recipient_address.is_transparent_only() && amount == Some(Zatoshis::ZERO) {
229            Err(PaymentError::ZeroValuedTransparentOutput)
230        } else {
231            Ok(Self {
232                recipient_address,
233                amount,
234                memo,
235                label,
236                message,
237                other_params,
238            })
239        }
240    }
241
242    /// Constructs a new [`Payment`] paying the given address the specified amount.
243    ///
244    /// # Parameters
245    /// - `recipient_address`: The address to which the payment should be sent.
246    /// - `amount`: The amount of the payment that is being requested.
247    pub fn without_memo(recipient_address: ZcashAddress, amount: Zatoshis) -> Self {
248        Self {
249            recipient_address,
250            amount: Some(amount),
251            memo: None,
252            label: None,
253            message: None,
254            other_params: vec![],
255        }
256    }
257
258    /// Returns the payment address to which the payment should be sent.
259    pub fn recipient_address(&self) -> &ZcashAddress {
260        &self.recipient_address
261    }
262
263    /// Returns the value of the payment that is being requested, in zatoshis.
264    pub fn amount(&self) -> Option<Zatoshis> {
265        self.amount
266    }
267
268    /// Returns the memo that, if included, must be provided with the payment.
269    pub fn memo(&self) -> Option<&MemoBytes> {
270        self.memo.as_ref()
271    }
272
273    /// A human-readable label for this payment within the larger structure
274    /// of the transaction request.
275    pub fn label(&self) -> Option<&String> {
276        self.label.as_ref()
277    }
278
279    /// A human-readable message to be displayed to the user describing the
280    /// purpose of this payment.
281    pub fn message(&self) -> Option<&String> {
282        self.message.as_ref()
283    }
284
285    /// A list of other arbitrary key/value pairs associated with this payment.
286    pub fn other_params(&self) -> &[(String, String)] {
287        self.other_params.as_ref()
288    }
289
290    /// A utility for use in tests to help check round-trip serialization properties.
291    #[cfg(any(test, feature = "test-dependencies"))]
292    pub(crate) fn normalize(&mut self) {
293        self.other_params.sort();
294    }
295}
296
297/// A ZIP321 transaction request.
298///
299/// A ZIP 321 request may include one or more such requests for payment.
300/// When constructing a transaction in response to such a request,
301/// a separate output should be added to the transaction for each
302/// payment value in the request.
303#[derive(Debug, Clone, PartialEq, Eq)]
304pub struct TransactionRequest {
305    payments: BTreeMap<usize, Payment>,
306}
307
308impl TransactionRequest {
309    /// Constructs a new empty transaction request.
310    pub fn empty() -> Self {
311        Self {
312            payments: BTreeMap::new(),
313        }
314    }
315
316    /// Constructs a new transaction request that obeys the ZIP-321 invariants.
317    pub fn new(payments: Vec<Payment>) -> Result<TransactionRequest, Zip321Error> {
318        // Payment indices are limited to 4 digits
319        if payments.len() > 9999 {
320            return Err(Zip321Error::TooManyPayments(payments.len()));
321        }
322
323        let request = TransactionRequest {
324            payments: payments.into_iter().enumerate().collect(),
325        };
326
327        // Enforce validity requirements.
328        if !request.payments.is_empty() {
329            TransactionRequest::from_uri(&request.to_uri())?;
330        }
331
332        Ok(request)
333    }
334
335    /// Constructs a new transaction request from the provided map from payment
336    /// index to payment.
337    ///
338    /// Payment index 0 will be mapped to the empty payment index.
339    pub fn from_indexed(
340        payments: BTreeMap<usize, Payment>,
341    ) -> Result<TransactionRequest, Zip321Error> {
342        if let Some(k) = payments.keys().find(|k| **k > 9999) {
343            // This is not quite the correct error, but close enough.
344            return Err(Zip321Error::TooManyPayments(*k));
345        }
346
347        Ok(TransactionRequest { payments })
348    }
349
350    /// Returns the map of payments that make up this request.
351    ///
352    /// This is a map from payment index to payment. Payment index `0` is used to denote
353    /// the empty payment index in the returned values.
354    pub fn payments(&self) -> &BTreeMap<usize, Payment> {
355        &self.payments
356    }
357
358    /// Returns the total value of payments to be made.
359    ///
360    /// Returns Ok(None) if any payment does not specify an amount, as the total of the request is
361    /// not well-defined in this circumstance.
362    ///
363    /// Returns `Err` if any summation step results in a value outside the range `0..=MAX_MONEY`
364    /// zatoshis.
365    pub fn total(&self) -> Result<Option<Zatoshis>, BalanceError> {
366        self.payments
367            .values()
368            .try_fold(Some(Zatoshis::ZERO), |total_opt, payment| {
369                if let (Some(total), Some(value)) = (total_opt, payment.amount) {
370                    (total + value).ok_or(BalanceError::Overflow).map(Some)
371                } else {
372                    Ok(None)
373                }
374            })
375    }
376
377    /// A utility for use in tests to help check round-trip serialization properties.
378    #[cfg(any(test, feature = "test-dependencies"))]
379    pub(crate) fn normalize(&mut self) {
380        for p in &mut self.payments.values_mut() {
381            p.normalize();
382        }
383    }
384
385    /// A utility for use in tests to help check round-trip serialization properties.
386    /// by comparing a two transaction requests for equality after normalization.
387    #[cfg(test)]
388    pub(crate) fn normalize_and_eq(a: &mut TransactionRequest, b: &mut TransactionRequest) -> bool {
389        a.normalize();
390        b.normalize();
391
392        a == b
393    }
394
395    /// Convert this request to a URI string.
396    ///
397    /// Returns None if the payment request is empty.
398    pub fn to_uri(&self) -> String {
399        fn payment_params(
400            payment: &Payment,
401            payment_index: Option<usize>,
402        ) -> impl IntoIterator<Item = String> + '_ {
403            std::iter::empty()
404                .chain(
405                    payment
406                        .amount
407                        .map(|a| render::amount_param(a, payment_index)),
408                )
409                .chain(
410                    payment
411                        .memo
412                        .as_ref()
413                        .map(|m| render::memo_param(m, payment_index)),
414                )
415                .chain(
416                    payment
417                        .label
418                        .as_ref()
419                        .map(|m| render::str_param("label", m, payment_index)),
420                )
421                .chain(
422                    payment
423                        .message
424                        .as_ref()
425                        .map(|m| render::str_param("message", m, payment_index)),
426                )
427                .chain(
428                    payment
429                        .other_params
430                        .iter()
431                        .map(move |(name, value)| render::str_param(name, value, payment_index)),
432                )
433        }
434
435        match self.payments.len() {
436            0 => "zcash:".to_string(),
437            1 if *self.payments.iter().next().unwrap().0 == 0 => {
438                let (_, payment) = self.payments.iter().next().unwrap();
439                let query_params = payment_params(payment, None)
440                    .into_iter()
441                    .collect::<Vec<String>>();
442
443                format!(
444                    "zcash:{}{}{}",
445                    payment.recipient_address.encode(),
446                    if query_params.is_empty() { "" } else { "?" },
447                    query_params.join("&")
448                )
449            }
450            _ => {
451                let query_params = self
452                    .payments
453                    .iter()
454                    .flat_map(|(i, payment)| {
455                        let idx = if *i == 0 { None } else { Some(*i) };
456                        let primary_address = payment.recipient_address.clone();
457                        std::iter::empty()
458                            .chain(Some(render::addr_param(&primary_address, idx)))
459                            .chain(payment_params(payment, idx))
460                    })
461                    .collect::<Vec<String>>();
462
463                format!("zcash:?{}", query_params.join("&"))
464            }
465        }
466    }
467
468    /// Parse the provided URI to a payment request value.
469    pub fn from_uri(uri: &str) -> Result<Self, Zip321Error> {
470        // Parse the leading zcash:<address>
471        let (rest, primary_addr_param) = parse::lead_addr(uri)
472            .map_err(|e| Zip321Error::ParseError(format!("Error parsing lead address: {e}")))?;
473
474        // Parse the remaining parameters as an undifferentiated list
475        let (_, xs) = if rest.is_empty() {
476            ("", vec![])
477        } else {
478            all_consuming(preceded(
479                char('?'),
480                separated_list0(char('&'), parse::zcashparam),
481            ))(rest)
482            .map_err(|e| Zip321Error::ParseError(format!("Error parsing query parameters: {e}")))?
483        };
484
485        // Construct sets of payment parameters, keyed by the payment index.
486        let mut params_by_index: BTreeMap<usize, Vec<parse::Param>> = BTreeMap::new();
487
488        // Add the primary address, if any, to the index.
489        if let Some(p) = primary_addr_param {
490            params_by_index.insert(p.payment_index, vec![p.param]);
491        }
492
493        // Group the remaining parameters by payment index
494        for p in xs {
495            match params_by_index.get_mut(&p.payment_index) {
496                None => {
497                    params_by_index.insert(p.payment_index, vec![p.param]);
498                }
499
500                Some(current) => {
501                    if parse::has_duplicate_param(current, &p.param) {
502                        return Err(Zip321Error::DuplicateParameter(p.param, p.payment_index));
503                    } else {
504                        current.push(p.param);
505                    }
506                }
507            }
508        }
509
510        // Build the actual payment values from the index.
511        params_by_index
512            .into_iter()
513            .map(|(i, params)| parse::to_payment(params, i).map(|payment| (i, payment)))
514            .collect::<Result<BTreeMap<usize, Payment>, _>>()
515            .map(|payments| TransactionRequest { payments })
516    }
517}
518
519mod render {
520    use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};
521    use zcash_address::ZcashAddress;
522    use zcash_protocol::{
523        memo::MemoBytes,
524        value::{COIN, Zatoshis},
525    };
526
527    use super::memo_to_base64;
528
529    /// The set of ASCII characters that must be percent-encoded according
530    /// to the definition of ZIP 321. This is the complement of the subset of
531    /// ASCII characters defined by `qchar`
532    ///
533    //      unreserved      = ALPHA / DIGIT / "-" / "." / "_" / "~"
534    //      allowed-delims  = "!" / "$" / "'" / "(" / ")" / "*" / "+" / "," / ";"
535    //      qchar           = unreserved / pct-encoded / allowed-delims / ":" / "@"
536    pub const QCHAR_ENCODE: &AsciiSet = &CONTROLS
537        .add(b' ')
538        .add(b'"')
539        .add(b'#')
540        .add(b'%')
541        .add(b'&')
542        .add(b'/')
543        .add(b'<')
544        .add(b'=')
545        .add(b'>')
546        .add(b'?')
547        .add(b'[')
548        .add(b'\\')
549        .add(b']')
550        .add(b'^')
551        .add(b'`')
552        .add(b'{')
553        .add(b'|')
554        .add(b'}');
555
556    /// Converts a parameter index value to the `String` representation
557    /// that must be appended to a parameter name when constructing a ZIP 321 URI.
558    pub fn param_index(idx: Option<usize>) -> String {
559        match idx {
560            Some(i) if i > 0 => format!(".{i}"),
561            _otherwise => "".to_string(),
562        }
563    }
564
565    /// Constructs an "address" key/value pair containing the encoded recipient address
566    /// at the specified parameter index.
567    pub fn addr_param(addr: &ZcashAddress, idx: Option<usize>) -> String {
568        format!("address{}={}", param_index(idx), addr.encode())
569    }
570
571    /// Converts a [`Zatoshis`] value to a correctly formatted decimal ZEC
572    /// value for inclusion in a ZIP 321 URI.
573    pub fn amount_str(amount: Zatoshis) -> String {
574        let coins = u64::from(amount) / COIN;
575        let zats = u64::from(amount) % COIN;
576        if zats == 0 {
577            format!("{coins}")
578        } else {
579            format!("{coins}.{zats:0>8}")
580                .trim_end_matches('0')
581                .to_string()
582        }
583    }
584
585    /// Constructs an "amount" key/value pair containing the encoded ZEC amount
586    /// at the specified parameter index.
587    pub fn amount_param(amount: Zatoshis, idx: Option<usize>) -> String {
588        format!("amount{}={}", param_index(idx), amount_str(amount))
589    }
590
591    /// Constructs a "memo" key/value pair containing the base64URI-encoded memo
592    /// at the specified parameter index.
593    pub fn memo_param(value: &MemoBytes, idx: Option<usize>) -> String {
594        format!("{}{}={}", "memo", param_index(idx), memo_to_base64(value))
595    }
596
597    /// Utility function for an arbitrary string key/value pair for inclusion in
598    /// a ZIP 321 URI at the specified parameter index.
599    pub fn str_param(label: &str, value: &str, idx: Option<usize>) -> String {
600        format!(
601            "{}{}={}",
602            label,
603            param_index(idx),
604            utf8_percent_encode(value, QCHAR_ENCODE)
605        )
606    }
607}
608
609mod parse {
610    use core::fmt::Debug;
611
612    use nom::{
613        AsChar, IResult, InputTakeAtPosition,
614        bytes::complete::{tag, take_till},
615        character::complete::{alpha1, char, digit0, digit1, one_of},
616        combinator::{all_consuming, map_opt, map_res, opt, recognize},
617        sequence::{preceded, separated_pair, tuple},
618    };
619    use percent_encoding::percent_decode;
620    use zcash_address::ZcashAddress;
621    use zcash_protocol::value::BalanceError;
622    use zcash_protocol::{
623        memo::MemoBytes,
624        value::{COIN, Zatoshis},
625    };
626
627    use super::{Payment, Zip321Error, memo_from_base64};
628
629    /// A data type that defines the possible parameter types which may occur within a
630    /// ZIP 321 URI.
631    #[derive(Debug, Clone, PartialEq, Eq)]
632    pub enum Param {
633        Addr(Box<ZcashAddress>),
634        Amount(Zatoshis),
635        Memo(Box<MemoBytes>),
636        Label(String),
637        Message(String),
638        Other(String, String),
639    }
640
641    impl Param {
642        /// Returns the name of the parameter from which this value was parsed.
643        pub fn name(&self) -> String {
644            match self {
645                Param::Addr(_) => "address".to_owned(),
646                Param::Amount(_) => "amount".to_owned(),
647                Param::Memo(_) => "memo".to_owned(),
648                Param::Label(_) => "label".to_owned(),
649                Param::Message(_) => "message".to_owned(),
650                Param::Other(name, _) => name.clone(),
651            }
652        }
653    }
654
655    /// A [`Param`] value with its associated index.
656    #[derive(Debug, Clone, PartialEq, Eq)]
657    pub struct IndexedParam {
658        pub param: Param,
659        pub payment_index: usize,
660    }
661
662    /// Utility function for determining parameter uniqueness.
663    ///
664    /// Utility function for determining whether a newly parsed param is a duplicate
665    /// of a previous parameter.
666    pub fn has_duplicate_param(v: &[Param], p: &Param) -> bool {
667        for p0 in v {
668            match (p0, p) {
669                (Param::Addr(_), Param::Addr(_)) => return true,
670                (Param::Amount(_), Param::Amount(_)) => return true,
671                (Param::Memo(_), Param::Memo(_)) => return true,
672                (Param::Label(_), Param::Label(_)) => return true,
673                (Param::Message(_), Param::Message(_)) => return true,
674                (Param::Other(n, _), Param::Other(n0, _)) if (n == n0) => return true,
675                _otherwise => continue,
676            }
677        }
678
679        false
680    }
681
682    /// Converts an vector of [`Param`] values to a [`Payment`].
683    ///
684    /// This function performs checks to ensure that the resulting [`Payment`] is structurally
685    /// valid; for example, a request for memo contents may not be associated with a
686    /// transparent payment address.
687    pub fn to_payment(vs: Vec<Param>, i: usize) -> Result<Payment, Zip321Error> {
688        let addr = vs.iter().find_map(|v| match v {
689            Param::Addr(a) => Some(a.clone()),
690            _otherwise => None,
691        });
692
693        let recipient_address = *addr.ok_or(Zip321Error::RecipientMissing(i))?;
694        if recipient_address.is_sprout() {
695            return Err(Zip321Error::SproutRecipient(i));
696        }
697
698        let mut payment = Payment {
699            recipient_address,
700            amount: None,
701            memo: None,
702            label: None,
703            message: None,
704            other_params: vec![],
705        };
706
707        for v in vs {
708            match v {
709                Param::Amount(a) => {
710                    if payment.recipient_address().is_transparent_only() && a == Zatoshis::ZERO {
711                        return Err(Zip321Error::ZeroValuedTransparentOutput(i));
712                    } else {
713                        payment.amount = Some(a)
714                    }
715                }
716                Param::Memo(m) => {
717                    if payment.recipient_address.can_receive_memo() {
718                        payment.memo = Some(*m);
719                    } else {
720                        return Err(Zip321Error::TransparentMemo(i));
721                    }
722                }
723                Param::Label(m) => payment.label = Some(m),
724                Param::Message(m) => payment.message = Some(m),
725                Param::Other(n, m) => payment.other_params.push((n, m)),
726                _otherwise => {}
727            }
728        }
729
730        Ok(payment)
731    }
732
733    /// Parses and consumes the leading "zcash:\[address\]" from a ZIP 321 URI.
734    pub fn lead_addr(input: &str) -> IResult<&str, Option<IndexedParam>> {
735        map_opt(
736            preceded(tag("zcash:"), take_till(|c| c == '?')),
737            |addr_str: &str| {
738                if addr_str.is_empty() {
739                    Some(None) // no address is ok, so wrap in `Some`
740                } else {
741                    // `try_from_encoded(..).ok()` returns `None` on error, which we want to then
742                    // cause `map_opt` to fail.
743                    ZcashAddress::try_from_encoded(addr_str)
744                        .map(|a| {
745                            Some(IndexedParam {
746                                param: Param::Addr(Box::new(a)),
747                                payment_index: 0,
748                            })
749                        })
750                        .ok()
751                }
752            },
753        )(input)
754    }
755
756    /// The primary parser for `name=value` query-string parameter pairs.
757    pub fn zcashparam(input: &str) -> IResult<&str, IndexedParam> {
758        map_res(
759            separated_pair(indexed_name, char('='), recognize(qchars)),
760            to_indexed_param,
761        )(input)
762    }
763
764    /// Extension for the `alphanumeric0` parser which extends that parser
765    /// by also permitting the characters that are members of the `allowed`
766    /// string.
767    fn alphanum_or(allowed: &str) -> impl (Fn(&str) -> IResult<&str, &str>) + '_ {
768        move |input| {
769            input.split_at_position_complete(|item| {
770                let c = item.as_char();
771                !(c.is_alphanum() || allowed.contains(c))
772            })
773        }
774    }
775
776    /// Parses valid characters which may appear in parameter values.
777    pub fn qchars(input: &str) -> IResult<&str, &str> {
778        alphanum_or("-._~!$'()*+,;:@%")(input)
779    }
780
781    /// Parses valid characters that may appear in parameter names.
782    pub fn namechars(input: &str) -> IResult<&str, &str> {
783        alphanum_or("+-")(input)
784    }
785
786    /// Parses a parameter name and its associated index.
787    pub fn indexed_name(input: &str) -> IResult<&str, (&str, Option<&str>)> {
788        let paramname = recognize(tuple((alpha1, namechars)));
789
790        tuple((
791            paramname,
792            opt(preceded(
793                char('.'),
794                recognize(tuple((
795                    one_of("123456789"),
796                    map_opt(digit0, |s: &str| if s.len() > 3 { None } else { Some(s) }),
797                ))),
798            )),
799        ))(input)
800    }
801
802    /// Parses a value in decimal ZEC.
803    pub fn parse_amount(input: &str) -> IResult<&str, Zatoshis> {
804        map_res(
805            all_consuming(tuple((
806                digit1,
807                opt(preceded(
808                    char('.'),
809                    map_opt(digit1, |s: &str| if s.len() > 8 { None } else { Some(s) }),
810                )),
811            ))),
812            |(whole_s, decimal_s): (&str, Option<&str>)| {
813                let coins: u64 = whole_s
814                    .to_string()
815                    .parse::<u64>()
816                    .map_err(|e| e.to_string())?;
817
818                let zats: u64 = match decimal_s {
819                    Some(d) => format!("{d:0<8}")
820                        .parse::<u64>()
821                        .map_err(|e| e.to_string())?,
822                    None => 0,
823                };
824
825                coins
826                    .checked_mul(COIN)
827                    .and_then(|coin_zats| coin_zats.checked_add(zats))
828                    .ok_or(BalanceError::Overflow)
829                    .and_then(Zatoshis::from_u64)
830                    .map_err(|_| format!("Not a valid zat amount: {coins}.{zats}"))
831            },
832        )(input)
833    }
834
835    fn to_indexed_param(
836        ((name, iopt), value): ((&str, Option<&str>), &str),
837    ) -> Result<IndexedParam, String> {
838        let param = match name {
839            "address" => ZcashAddress::try_from_encoded(value)
840                .map(Box::new)
841                .map(Param::Addr)
842                .map_err(|err| {
843                    format!("Could not interpret {value} as a valid Zcash address: {err}")
844                }),
845
846            "amount" => parse_amount(value)
847                .map_err(|e| e.to_string())
848                .map(|(_, a)| Param::Amount(a)),
849
850            "label" => percent_decode(value.as_bytes())
851                .decode_utf8()
852                .map(|s| Param::Label(s.into_owned()))
853                .map_err(|e| e.to_string()),
854
855            "message" => percent_decode(value.as_bytes())
856                .decode_utf8()
857                .map(|s| Param::Message(s.into_owned()))
858                .map_err(|e| e.to_string()),
859
860            "memo" => memo_from_base64(value)
861                .map(Box::new)
862                .map(Param::Memo)
863                .map_err(|e| format!("Decoded memo was invalid: {e:?}")),
864
865            other if other.starts_with("req-") => {
866                Err(format!("Required parameter {other} not recognized"))
867            }
868
869            other => percent_decode(value.as_bytes())
870                .decode_utf8()
871                .map(|s| Param::Other(other.to_string(), s.into_owned()))
872                .map_err(|e| e.to_string()),
873        }?;
874
875        let payment_index = match iopt {
876            Some(istr) => istr.parse::<usize>().map(Some).map_err(|e| e.to_string()),
877            None => Ok(None),
878        }?;
879
880        Ok(IndexedParam {
881            param,
882            payment_index: payment_index.unwrap_or(0),
883        })
884    }
885}
886
887#[cfg(any(test, feature = "test-dependencies"))]
888pub mod testing {
889    use proptest::collection::{btree_map, vec};
890    use proptest::option;
891    use proptest::prelude::{any, prop_compose};
892    use proptest::strategy::Strategy as _;
893
894    use zcash_address::testing::arb_address;
895    use zcash_protocol::value::Zatoshis;
896    use zcash_protocol::{consensus::NetworkType, value::testing::arb_zatoshis};
897
898    use super::{MemoBytes, Payment, TransactionRequest};
899    pub const VALID_PARAMNAME: &str = "[a-zA-Z][a-zA-Z0-9+-]*";
900
901    prop_compose! {
902        pub fn arb_valid_memo()(bytes in vec(any::<u8>(), 0..512)) -> MemoBytes {
903            MemoBytes::from_bytes(&bytes).unwrap()
904        }
905    }
906
907    /// Parameter names that are reserved by ZIP 321 and must not appear in `other_params`.
908    const RESERVED_PARAM_NAMES: &[&str] = &["address", "amount", "memo", "label", "message"];
909
910    prop_compose! {
911        pub fn arb_zip321_payment(network: NetworkType)(
912            (recipient_address, amount) in arb_address(network)
913                .prop_filter("Sprout recipients are not supported in payment requests", |addr| {
914                    !addr.is_sprout()
915                })
916                .prop_flat_map(|addr| {
917                arb_zatoshis().prop_filter_map(
918                    "zero-valued outputs to transparent addresses are disallowed by consensus",
919                    move |zat| (!addr.is_transparent_only() || zat > Zatoshis::ZERO).then_some(
920                        (addr.clone(), zat)
921                    )
922                )
923            }),
924            memo in option::of(arb_valid_memo()),
925            message in option::of(any::<String>()),
926            label in option::of(any::<String>()),
927            // prevent duplicates by generating a set rather than a vec
928            other_params in btree_map(VALID_PARAMNAME, any::<String>(), 0..3),
929        ) -> Payment {
930            let memo = memo.filter(|_| recipient_address.can_receive_memo());
931            let other_params: Vec<(String, String)> = other_params
932                .into_iter()
933                .filter(|(name, _)| {
934                    !name.starts_with("req-")
935                        && !RESERVED_PARAM_NAMES.contains(&name.as_str())
936                })
937                .collect();
938            Payment {
939                recipient_address,
940                amount: Some(amount),
941                memo,
942                label,
943                message,
944                other_params,
945            }
946        }
947    }
948
949    prop_compose! {
950        pub fn arb_zip321_request(network: NetworkType)(
951            payments in btree_map(0usize..10000, arb_zip321_payment(network), 1..10)
952        ) -> TransactionRequest {
953            let mut req = TransactionRequest::from_indexed(payments).unwrap();
954            req.normalize(); // just to make test comparisons easier
955            req
956        }
957    }
958
959    prop_compose! {
960        pub fn arb_zip321_request_sequential(network: NetworkType)(
961            payments in vec(arb_zip321_payment(network), 1..10)
962        ) -> TransactionRequest {
963            let mut req = TransactionRequest::new(payments).unwrap();
964            req.normalize(); // just to make test comparisons easier
965            req
966        }
967    }
968
969    prop_compose! {
970        pub fn arb_zip321_uri(network: NetworkType)(req in arb_zip321_request(network)) -> String {
971            req.to_uri()
972        }
973    }
974
975    prop_compose! {
976        pub fn arb_addr_str(network: NetworkType)(
977            recipient_address in arb_address(network)
978        ) -> String {
979            recipient_address.encode()
980        }
981    }
982}
983
984#[cfg(test)]
985mod tests {
986    use proptest::prelude::{any, proptest};
987    use std::str::FromStr;
988
989    use zcash_address::{ZcashAddress, testing::arb_address};
990    use zcash_protocol::{
991        consensus::NetworkType,
992        memo::{Memo, MemoBytes},
993        value::{Zatoshis, testing::arb_zatoshis},
994    };
995
996    use crate::{PaymentError, Zip321Error};
997
998    use super::{
999        Payment, TransactionRequest, memo_from_base64, memo_to_base64,
1000        parse::{Param, parse_amount, zcashparam},
1001        render::{amount_str, memo_param, str_param},
1002        testing::{arb_addr_str, arb_valid_memo, arb_zip321_request, arb_zip321_uri},
1003    };
1004
1005    fn check_roundtrip(req: TransactionRequest) {
1006        let req_uri = req.to_uri();
1007        let parsed = TransactionRequest::from_uri(&req_uri).unwrap();
1008        assert_eq!(parsed, req);
1009    }
1010
1011    #[test]
1012    fn test_zip321_roundtrip_simple_amounts() {
1013        let amounts = vec![1u64, 1000u64, 100000u64, 100000000u64, 100000000000u64];
1014
1015        for amt_u64 in amounts {
1016            let amt = Zatoshis::const_from_u64(amt_u64);
1017            let amt_str = amount_str(amt);
1018            assert_eq!(amt, parse_amount(&amt_str).unwrap().1);
1019        }
1020    }
1021
1022    #[test]
1023    fn test_zip321_parse_empty_message() {
1024        let fragment = "message=";
1025
1026        let result = zcashparam(fragment).unwrap().1.param;
1027        assert_eq!(result, Param::Message("".to_string()));
1028    }
1029
1030    #[test]
1031    fn test_zip321_parse_simple() {
1032        let uri = "zcash:ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k?amount=3768769.02796286&message=";
1033        let parse_result = TransactionRequest::from_uri(uri).unwrap();
1034
1035        let expected = TransactionRequest::new(
1036            vec![
1037                Payment {
1038                    recipient_address: ZcashAddress::try_from_encoded("ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap(),
1039                    amount: Some(Zatoshis::const_from_u64(376876902796286)),
1040                    memo: None,
1041                    label: None,
1042                    message: Some("".to_string()),
1043                    other_params: vec![],
1044                }
1045            ]
1046        ).unwrap();
1047
1048        assert_eq!(parse_result, expected);
1049    }
1050
1051    #[test]
1052    fn test_zip321_parse_no_query_params() {
1053        let uri = "zcash:ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k";
1054        let parse_result = TransactionRequest::from_uri(uri).unwrap();
1055
1056        let expected = TransactionRequest::new(
1057            vec![
1058                Payment {
1059                    recipient_address: ZcashAddress::try_from_encoded("ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap(),
1060                    amount: None,
1061                    memo: None,
1062                    label: None,
1063                    message: None,
1064                    other_params: vec![],
1065                }
1066            ]
1067        ).unwrap();
1068
1069        assert_eq!(parse_result, expected);
1070    }
1071
1072    #[test]
1073    fn test_zip321_roundtrip_empty_message() {
1074        let req = TransactionRequest::new(
1075            vec![
1076                Payment {
1077                    recipient_address: ZcashAddress::try_from_encoded("ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap(),
1078                    amount: Some(Zatoshis::ZERO),
1079                    memo: None,
1080                    label: None,
1081                    message: Some("".to_string()),
1082                    other_params: vec![]
1083                }
1084            ]
1085        ).unwrap();
1086
1087        check_roundtrip(req);
1088    }
1089
1090    #[test]
1091    fn test_zip321_memos() {
1092        let m_simple: MemoBytes = Memo::from_str("This is a simple memo.").unwrap().into();
1093        let m_simple_64 = memo_to_base64(&m_simple);
1094        assert_eq!(memo_from_base64(&m_simple_64).unwrap(), m_simple);
1095
1096        let m_json: MemoBytes = Memo::from_str("{ \"key\": \"This is a JSON-structured memo.\" }")
1097            .unwrap()
1098            .into();
1099        let m_json_64 = memo_to_base64(&m_json);
1100        assert_eq!(memo_from_base64(&m_json_64).unwrap(), m_json);
1101
1102        let m_unicode: MemoBytes = Memo::from_str("This is a unicode memo ✨🦄🏆🎉")
1103            .unwrap()
1104            .into();
1105        let m_unicode_64 = memo_to_base64(&m_unicode);
1106        assert_eq!(memo_from_base64(&m_unicode_64).unwrap(), m_unicode);
1107    }
1108
1109    #[test]
1110    fn test_zip321_spec_valid_examples() {
1111        let valid_0 = "zcash:";
1112        let v0r = TransactionRequest::from_uri(valid_0).unwrap();
1113        assert!(v0r.payments.is_empty());
1114
1115        let valid_0 = "zcash:?";
1116        let v0r = TransactionRequest::from_uri(valid_0).unwrap();
1117        assert!(v0r.payments.is_empty());
1118
1119        let valid_1 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=1&memo=VGhpcyBpcyBhIHNpbXBsZSBtZW1vLg&message=Thank%20you%20for%20your%20purchase";
1120        let v1r = TransactionRequest::from_uri(valid_1).unwrap();
1121        assert_eq!(
1122            v1r.payments.get(&0).and_then(|p| p.amount),
1123            Some(Zatoshis::const_from_u64(100000000))
1124        );
1125
1126        let valid_2 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=123.456&address.1=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.1=0.789&memo.1=VGhpcyBpcyBhIHVuaWNvZGUgbWVtbyDinKjwn6aE8J-PhvCfjok";
1127        let mut v2r = TransactionRequest::from_uri(valid_2).unwrap();
1128        v2r.normalize();
1129        assert_eq!(
1130            v2r.payments.get(&0).and_then(|p| p.amount),
1131            Some(Zatoshis::const_from_u64(12345600000))
1132        );
1133        assert_eq!(
1134            v2r.payments.get(&1).and_then(|p| p.amount),
1135            Some(Zatoshis::const_from_u64(78900000))
1136        );
1137
1138        // valid; amount just less than MAX_MONEY
1139        // 20999999.99999999
1140        let valid_3 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=20999999.99999999";
1141        let v3r = TransactionRequest::from_uri(valid_3).unwrap();
1142        assert_eq!(
1143            v3r.payments.get(&0).and_then(|p| p.amount),
1144            Some(Zatoshis::const_from_u64(2099999999999999))
1145        );
1146
1147        // valid; MAX_MONEY
1148        // 21000000
1149        let valid_4 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=21000000";
1150        let v4r = TransactionRequest::from_uri(valid_4).unwrap();
1151        assert_eq!(
1152            v4r.payments.get(&0).and_then(|p| p.amount),
1153            Some(Zatoshis::const_from_u64(2100000000000000))
1154        );
1155    }
1156
1157    #[test]
1158    fn test_zip321_spec_regtest_valid_examples() {
1159        let valid_1 = "zcash:zregtestsapling1qqqqqqqqqqqqqqqqqqcguyvaw2vjk4sdyeg0lc970u659lvhqq7t0np6hlup5lusxle7505hlz3?amount=1&memo=VGhpcyBpcyBhIHNpbXBsZSBtZW1vLg&message=Thank%20you%20for%20your%20purchase";
1160        let v1r = TransactionRequest::from_uri(valid_1).unwrap();
1161        assert_eq!(
1162            v1r.payments.get(&0).and_then(|p| p.amount),
1163            Some(Zatoshis::const_from_u64(100000000))
1164        );
1165    }
1166
1167    #[test]
1168    fn test_zip321_spec_invalid_examples() {
1169        // invalid; empty string
1170        let invalid_0 = "";
1171        let i0r = TransactionRequest::from_uri(invalid_0);
1172        assert!(i0r.is_err());
1173
1174        // invalid; missing `address=`
1175        let invalid_1 = "zcash:?amount=3491405.05201255&address.1=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.1=5740296.87793245";
1176        let i1r = TransactionRequest::from_uri(invalid_1);
1177        assert!(i1r.is_err());
1178
1179        // invalid; missing `address.1=`
1180        let invalid_2 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=1&amount.1=2&address.2=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez";
1181        let i2r = TransactionRequest::from_uri(invalid_2);
1182        assert!(i2r.is_err());
1183
1184        // invalid; `address.0=` and `amount.0=` are not permitted (leading 0s).
1185        let invalid_3 = "zcash:?address.0=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.0=2";
1186        let i3r = TransactionRequest::from_uri(invalid_3);
1187        assert!(i3r.is_err());
1188
1189        // invalid; duplicate `amount=` field
1190        let invalid_4 =
1191            "zcash:?amount=1.234&amount=2.345&address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU";
1192        let i4r = TransactionRequest::from_uri(invalid_4);
1193        assert!(i4r.is_err());
1194
1195        // invalid; duplicate `amount.1=` field
1196        let invalid_5 =
1197            "zcash:?amount.1=1.234&amount.1=2.345&address.1=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU";
1198        let i5r = TransactionRequest::from_uri(invalid_5);
1199        assert!(i5r.is_err());
1200
1201        //invalid; memo associated with t-addr
1202        let invalid_6 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=123.456&memo=eyAia2V5IjogIlRoaXMgaXMgYSBKU09OLXN0cnVjdHVyZWQgbWVtby4iIH0&address.1=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.1=0.789&memo.1=VGhpcyBpcyBhIHVuaWNvZGUgbWVtbyDinKjwn6aE8J-PhvCfjok";
1203        let i6r = TransactionRequest::from_uri(invalid_6);
1204        assert!(i6r.is_err());
1205
1206        // invalid; amount component exceeds an i64
1207        // 9223372036854775808 = i64::MAX + 1
1208        let invalid_7 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=9223372036854775808";
1209        let i7r = TransactionRequest::from_uri(invalid_7);
1210        assert!(i7r.is_err());
1211
1212        // invalid; amount component wraps into a valid small positive i64
1213        // 18446744073709551624
1214        let invalid_7a = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=18446744073709551624";
1215        let i7ar = TransactionRequest::from_uri(invalid_7a);
1216        assert!(i7ar.is_err());
1217
1218        // invalid; amount component is MAX_MONEY
1219        // 21000000.00000001
1220        let invalid_8 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=21000000.00000001";
1221        let i8r = TransactionRequest::from_uri(invalid_8);
1222        assert!(i8r.is_err());
1223
1224        // invalid; negative amount
1225        let invalid_9 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=-1";
1226        let i9r = TransactionRequest::from_uri(invalid_9);
1227        assert!(i9r.is_err());
1228
1229        // invalid; parameter index too large
1230        let invalid_10 =
1231            "zcash:?amount.10000=1.23&address.10000=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU";
1232        let i10r = TransactionRequest::from_uri(invalid_10);
1233        assert!(i10r.is_err());
1234
1235        // invalid: bad amount format
1236        let invalid_11 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=123.";
1237        let i11r = TransactionRequest::from_uri(invalid_11);
1238        assert!(i11r.is_err());
1239
1240        // invalid: unrecognized required parameters are not allowed
1241        let invalid_12 =
1242            "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=123.45&req-unknown=x";
1243        let i12r = TransactionRequest::from_uri(invalid_12);
1244        assert!(i12r.is_err());
1245    }
1246
1247    #[test]
1248    fn sprout_recipients_are_rejected() {
1249        // The Sprout address test vector from the `zcash_address::encoding` tests.
1250        const SPROUT: &str = "zc8E5gYid86n4bo2Usdq1cpr7PpfoJGzttwBHEEgGhGkLUg7SPPVFNB2AkRFXZ7usfphup5426dt1buMmY3fkYeRrQGLa8y";
1251
1252        // Lead address, reproducing the issue.
1253        assert!(matches!(
1254            TransactionRequest::from_uri(&format!("zcash:{SPROUT}?amount=1&memo=aGk")),
1255            Err(Zip321Error::SproutRecipient(0))
1256        ));
1257
1258        // `address` query parameter.
1259        assert!(matches!(
1260            TransactionRequest::from_uri(&format!("zcash:?address={SPROUT}&amount=1")),
1261            Err(Zip321Error::SproutRecipient(0))
1262        ));
1263
1264        // Indexed `address` query parameter.
1265        assert!(matches!(
1266            TransactionRequest::from_uri(&format!("zcash:?address.3={SPROUT}&amount.3=1")),
1267            Err(Zip321Error::SproutRecipient(3))
1268        ));
1269
1270        // Direct construction via `Payment::new`.
1271        let addr = ZcashAddress::try_from_encoded(SPROUT).unwrap();
1272        assert!(matches!(
1273            Payment::new(addr, None, None, None, None, vec![]),
1274            Err(PaymentError::SproutAddress)
1275        ));
1276    }
1277
1278    proptest! {
1279        #[test]
1280        fn prop_zip321_roundtrip_address(addr in arb_address(NetworkType::Test)) {
1281            let a = addr.encode();
1282            assert_eq!(ZcashAddress::try_from_encoded(&a), Ok(addr));
1283        }
1284
1285        #[test]
1286        fn prop_zip321_roundtrip_address_str(a in arb_addr_str(NetworkType::Test)) {
1287            let addr = ZcashAddress::try_from_encoded(&a).unwrap();
1288            assert_eq!(addr.encode(), a);
1289        }
1290
1291        #[test]
1292        fn prop_zip321_roundtrip_amount(amt in arb_zatoshis()) {
1293            let amt_str = amount_str(amt);
1294            assert_eq!(amt, parse_amount(&amt_str).unwrap().1);
1295        }
1296
1297        #[test]
1298        fn prop_zip321_roundtrip_str_param(
1299            message in any::<String>(), i in proptest::option::of(0usize..2000)) {
1300            let fragment = str_param("message", &message, i);
1301            let (rest, iparam) = zcashparam(&fragment).unwrap();
1302            assert_eq!(rest, "");
1303            assert_eq!(iparam.param, Param::Message(message));
1304            assert_eq!(iparam.payment_index, i.unwrap_or(0));
1305        }
1306
1307        #[test]
1308        fn prop_zip321_roundtrip_memo_param(
1309            memo in arb_valid_memo(), i in proptest::option::of(0usize..2000)) {
1310            let fragment = memo_param(&memo, i);
1311            let (rest, iparam) = zcashparam(&fragment).unwrap();
1312            assert_eq!(rest, "");
1313            assert_eq!(iparam.param, Param::Memo(Box::new(memo)));
1314            assert_eq!(iparam.payment_index, i.unwrap_or(0));
1315        }
1316
1317        #[test]
1318        fn prop_zip321_roundtrip_request(mut req in arb_zip321_request(NetworkType::Test)) {
1319            let req_uri = req.to_uri();
1320            let mut parsed = TransactionRequest::from_uri(&req_uri).unwrap();
1321            assert!(TransactionRequest::normalize_and_eq(&mut parsed, &mut req));
1322        }
1323
1324        #[test]
1325        fn prop_zip321_roundtrip_uri(uri in arb_zip321_uri(NetworkType::Test)) {
1326            let mut parsed = TransactionRequest::from_uri(&uri).unwrap();
1327            parsed.normalize();
1328            let serialized = parsed.to_uri();
1329            assert_eq!(serialized, uri)
1330        }
1331    }
1332}