1use 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#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum Zip321Error {
29 InvalidBase64(base64::DecodeError),
31 MemoBytesError(memo::Error),
34 TooManyPayments(usize),
37 DuplicateParameter(parse::Param, usize),
39 TransparentMemo(usize),
42 RecipientMissing(usize),
44 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
99pub fn memo_to_base64(memo: &MemoBytes) -> String {
103 BASE64_URL_SAFE_NO_PAD.encode(memo.as_slice())
104}
105
106pub 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#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct Payment {
119 recipient_address: ZcashAddress,
121 amount: Zatoshis,
123 memo: Option<MemoBytes>,
129 label: Option<String>,
132 message: Option<String>,
135 other_params: Vec<(String, String)>,
137}
138
139impl Payment {
140 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 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 pub fn recipient_address(&self) -> &ZcashAddress {
180 &self.recipient_address
181 }
182
183 pub fn amount(&self) -> Zatoshis {
185 self.amount
186 }
187
188 pub fn memo(&self) -> Option<&MemoBytes> {
190 self.memo.as_ref()
191 }
192
193 pub fn label(&self) -> Option<&String> {
196 self.label.as_ref()
197 }
198
199 pub fn message(&self) -> Option<&String> {
202 self.message.as_ref()
203 }
204
205 pub fn other_params(&self) -> &[(String, String)] {
207 self.other_params.as_ref()
208 }
209
210 #[cfg(any(test, feature = "test-dependencies"))]
212 pub(crate) fn normalize(&mut self) {
213 self.other_params.sort();
214 }
215}
216
217#[derive(Debug, Clone, PartialEq, Eq)]
224pub struct TransactionRequest {
225 payments: BTreeMap<usize, Payment>,
226}
227
228impl TransactionRequest {
229 pub fn empty() -> Self {
231 Self {
232 payments: BTreeMap::new(),
233 }
234 }
235
236 pub fn new(payments: Vec<Payment>) -> Result<TransactionRequest, Zip321Error> {
238 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 if !request.payments.is_empty() {
249 TransactionRequest::from_uri(&request.to_uri())?;
250 }
251
252 Ok(request)
253 }
254
255 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 return Err(Zip321Error::TooManyPayments(*k));
265 }
266
267 Ok(TransactionRequest { payments })
268 }
269
270 pub fn payments(&self) -> &BTreeMap<usize, Payment> {
275 &self.payments
276 }
277
278 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 #[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 #[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 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 pub fn from_uri(uri: &str) -> Result<Self, Zip321Error> {
380 let (rest, primary_addr_param) = parse::lead_addr(uri)
382 .map_err(|e| Zip321Error::ParseError(format!("Error parsing lead address: {}", e)))?;
383
384 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 let mut params_by_index: BTreeMap<usize, Vec<parse::Param>> = BTreeMap::new();
399
400 if let Some(p) = primary_addr_param {
402 params_by_index.insert(p.payment_index, vec![p.param]);
403 }
404
405 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 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 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 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 pub fn addr_param(addr: &ZcashAddress, idx: Option<usize>) -> String {
480 format!("address{}={}", param_index(idx), addr.encode())
481 }
482
483 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 pub fn amount_param(amount: Zatoshis, idx: Option<usize>) -> String {
500 format!("amount{}={}", param_index(idx), amount_str(amount))
501 }
502
503 pub fn memo_param(value: &MemoBytes, idx: Option<usize>) -> String {
506 format!("{}{}={}", "memo", param_index(idx), memo_to_base64(value))
507 }
508
509 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 #[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 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 #[derive(Debug, Clone, PartialEq, Eq)]
569 pub struct IndexedParam {
570 pub param: Param,
571 pub payment_index: usize,
572 }
573
574 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 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 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) } else {
642 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 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 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 pub fn qchars(input: &str) -> IResult<&str, &str> {
679 alphanum_or("-._~!$'()*+,;:@%")(input)
680 }
681
682 pub fn namechars(input: &str) -> IResult<&str, &str> {
684 alphanum_or("+-")(input)
685 }
686
687 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 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 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(); 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(); 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 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 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 let invalid_0 = "";
1053 let i0r = TransactionRequest::from_uri(invalid_0);
1054 assert!(i0r.is_err());
1055
1056 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 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 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 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 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 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 let invalid_7 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=9223372036854775808";
1091 let i7r = TransactionRequest::from_uri(invalid_7);
1092 assert!(i7r.is_err());
1093
1094 let invalid_7a = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=18446744073709551624";
1097 let i7ar = TransactionRequest::from_uri(invalid_7a);
1098 assert!(i7ar.is_err());
1099
1100 let invalid_8 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=21000000.00000001";
1103 let i8r = TransactionRequest::from_uri(invalid_8);
1104 assert!(i8r.is_err());
1105
1106 let invalid_9 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=-1";
1108 let i9r = TransactionRequest::from_uri(invalid_9);
1109 assert!(i9r.is_err());
1110
1111 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 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}