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