1use 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#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum PaymentError {
29 TransparentMemo,
31 ZeroValuedTransparentOutput,
34 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 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#[derive(Debug, Clone, PartialEq, Eq)]
74#[non_exhaustive]
75pub enum Zip321Error {
76 InvalidBase64(base64::DecodeError),
78 MemoBytesError(memo::Error),
81 TooManyPayments(usize),
84 DuplicateParameter(parse::Param, usize),
86 TransparentMemo(usize),
89 ZeroValuedTransparentOutput(usize),
92 SproutRecipient(usize),
95 RecipientMissing(usize),
97 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
157pub fn memo_to_base64(memo: &MemoBytes) -> String {
161 BASE64_URL_SAFE_NO_PAD.encode(memo.as_slice())
162}
163
164pub 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#[derive(Debug, Clone, PartialEq, Eq)]
176pub struct Payment {
177 recipient_address: ZcashAddress,
179 amount: Option<Zatoshis>,
181 memo: Option<MemoBytes>,
187 label: Option<String>,
190 message: Option<String>,
193 other_params: Vec<(String, String)>,
195}
196
197impl Payment {
198 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 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 pub fn recipient_address(&self) -> &ZcashAddress {
260 &self.recipient_address
261 }
262
263 pub fn amount(&self) -> Option<Zatoshis> {
265 self.amount
266 }
267
268 pub fn memo(&self) -> Option<&MemoBytes> {
270 self.memo.as_ref()
271 }
272
273 pub fn label(&self) -> Option<&String> {
276 self.label.as_ref()
277 }
278
279 pub fn message(&self) -> Option<&String> {
282 self.message.as_ref()
283 }
284
285 pub fn other_params(&self) -> &[(String, String)] {
287 self.other_params.as_ref()
288 }
289
290 #[cfg(any(test, feature = "test-dependencies"))]
292 pub(crate) fn normalize(&mut self) {
293 self.other_params.sort();
294 }
295}
296
297#[derive(Debug, Clone, PartialEq, Eq)]
304pub struct TransactionRequest {
305 payments: BTreeMap<usize, Payment>,
306}
307
308impl TransactionRequest {
309 pub fn empty() -> Self {
311 Self {
312 payments: BTreeMap::new(),
313 }
314 }
315
316 pub fn new(payments: Vec<Payment>) -> Result<TransactionRequest, Zip321Error> {
318 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 if !request.payments.is_empty() {
329 TransactionRequest::from_uri(&request.to_uri())?;
330 }
331
332 Ok(request)
333 }
334
335 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 return Err(Zip321Error::TooManyPayments(*k));
345 }
346
347 Ok(TransactionRequest { payments })
348 }
349
350 pub fn payments(&self) -> &BTreeMap<usize, Payment> {
355 &self.payments
356 }
357
358 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 #[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 #[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 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 pub fn from_uri(uri: &str) -> Result<Self, Zip321Error> {
470 let (rest, primary_addr_param) = parse::lead_addr(uri)
472 .map_err(|e| Zip321Error::ParseError(format!("Error parsing lead address: {e}")))?;
473
474 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 let mut params_by_index: BTreeMap<usize, Vec<parse::Param>> = BTreeMap::new();
487
488 if let Some(p) = primary_addr_param {
490 params_by_index.insert(p.payment_index, vec![p.param]);
491 }
492
493 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 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 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 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 pub fn addr_param(addr: &ZcashAddress, idx: Option<usize>) -> String {
568 format!("address{}={}", param_index(idx), addr.encode())
569 }
570
571 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 pub fn amount_param(amount: Zatoshis, idx: Option<usize>) -> String {
588 format!("amount{}={}", param_index(idx), amount_str(amount))
589 }
590
591 pub fn memo_param(value: &MemoBytes, idx: Option<usize>) -> String {
594 format!("{}{}={}", "memo", param_index(idx), memo_to_base64(value))
595 }
596
597 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 #[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 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 #[derive(Debug, Clone, PartialEq, Eq)]
657 pub struct IndexedParam {
658 pub param: Param,
659 pub payment_index: usize,
660 }
661
662 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 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 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) } else {
741 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 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 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 pub fn qchars(input: &str) -> IResult<&str, &str> {
778 alphanum_or("-._~!$'()*+,;:@%")(input)
779 }
780
781 pub fn namechars(input: &str) -> IResult<&str, &str> {
783 alphanum_or("+-")(input)
784 }
785
786 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 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 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 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(); 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(); 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 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 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 let invalid_0 = "";
1171 let i0r = TransactionRequest::from_uri(invalid_0);
1172 assert!(i0r.is_err());
1173
1174 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 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 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 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 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 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 let invalid_7 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=9223372036854775808";
1209 let i7r = TransactionRequest::from_uri(invalid_7);
1210 assert!(i7r.is_err());
1211
1212 let invalid_7a = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=18446744073709551624";
1215 let i7ar = TransactionRequest::from_uri(invalid_7a);
1216 assert!(i7ar.is_err());
1217
1218 let invalid_8 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=21000000.00000001";
1221 let i8r = TransactionRequest::from_uri(invalid_8);
1222 assert!(i8r.is_err());
1223
1224 let invalid_9 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=-1";
1226 let i9r = TransactionRequest::from_uri(invalid_9);
1227 assert!(i9r.is_err());
1228
1229 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 let invalid_11 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=123.";
1237 let i11r = TransactionRequest::from_uri(invalid_11);
1238 assert!(i11r.is_err());
1239
1240 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 const SPROUT: &str = "zc8E5gYid86n4bo2Usdq1cpr7PpfoJGzttwBHEEgGhGkLUg7SPPVFNB2AkRFXZ7usfphup5426dt1buMmY3fkYeRrQGLa8y";
1251
1252 assert!(matches!(
1254 TransactionRequest::from_uri(&format!("zcash:{SPROUT}?amount=1&memo=aGk")),
1255 Err(Zip321Error::SproutRecipient(0))
1256 ));
1257
1258 assert!(matches!(
1260 TransactionRequest::from_uri(&format!("zcash:?address={SPROUT}&amount=1")),
1261 Err(Zip321Error::SproutRecipient(0))
1262 ));
1263
1264 assert!(matches!(
1266 TransactionRequest::from_uri(&format!("zcash:?address.3={SPROUT}&amount.3=1")),
1267 Err(Zip321Error::SproutRecipient(3))
1268 ));
1269
1270 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}