payto.rs (18996B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2024, 2025, 2026 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Affero General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 12 13 You should have received a copy of the GNU Affero General Public License along with 14 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 use std::{ 18 fmt::{Debug, Display}, 19 ops::{Deref, DerefMut}, 20 str::FromStr, 21 }; 22 23 use compact_str::CompactString; 24 use serde::{Deserialize, Serialize, de::DeserializeOwned}; 25 use serde_with::{DeserializeFromStr, SerializeDisplay}; 26 use url::Url; 27 28 use super::{ 29 amount::Amount, 30 iban::{BIC, IBAN}, 31 }; 32 33 /// Parse a payto URI, panic if malformed 34 pub fn payto(url: impl AsRef<str>) -> PaytoURI { 35 url.as_ref().parse().expect("invalid payto") 36 } 37 38 pub trait PaytoImpl: Sized { 39 fn full(self, name: &str) -> FullPayto<Self> { 40 FullPayto::new(self, name) 41 } 42 43 fn transfer( 44 self, 45 name: &str, 46 amount: Option<Amount>, 47 subject: Option<&str>, 48 ) -> TransferPayto<Self> { 49 TransferPayto::new(self, name, amount, subject) 50 } 51 52 fn as_uri(&self) -> PaytoURI; 53 fn as_full_uri(&self, name: &str) -> PaytoURI { 54 self.as_uri().as_full_payto(name) 55 } 56 fn as_transfer_uri( 57 &self, 58 name: &str, 59 amount: Option<&Amount>, 60 subject: Option<&str>, 61 ) -> PaytoURI { 62 self.as_uri().as_transfer_payto(name, amount, subject) 63 } 64 fn parse(uri: &PaytoURI) -> Result<Self, PaytoErr>; 65 } 66 67 /// A generic RFC 8905 payto URI 68 #[derive( 69 Debug, Clone, PartialEq, Eq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay, 70 )] 71 pub struct PaytoURI(Url); 72 73 impl PaytoURI { 74 pub fn raw(&self) -> &str { 75 self.0.as_str() 76 } 77 78 pub fn from_parts(domain: &str, path: impl Display) -> Self { 79 payto(format!("payto://{domain}{path}")) 80 } 81 82 pub fn as_full_payto(self, name: &str) -> PaytoURI { 83 self.with_query([("receiver-name", name)]) 84 } 85 86 pub fn as_transfer_payto( 87 self, 88 name: &str, 89 amount: Option<&Amount>, 90 subject: Option<&str>, 91 ) -> PaytoURI { 92 self.as_full_payto(name) 93 .with_query([("amount", amount)]) 94 .with_query([("message", subject)]) 95 } 96 97 pub fn query<Q: DeserializeOwned>(&self) -> Result<Q, PaytoErr> { 98 let query = self.0.query().unwrap_or_default().as_bytes(); 99 let de = serde_urlencoded::Deserializer::new(url::form_urlencoded::parse(query)); 100 serde_path_to_error::deserialize(de).map_err(PaytoErr::Query) 101 } 102 103 fn with_query(mut self, query: impl Serialize) -> Self { 104 let mut urlencoder = self.0.query_pairs_mut(); 105 query 106 .serialize(serde_urlencoded::Serializer::new(&mut urlencoder)) 107 .unwrap(); 108 let _ = urlencoder.finish(); 109 drop(urlencoder); 110 self 111 } 112 } 113 114 impl AsRef<Url> for PaytoURI { 115 fn as_ref(&self) -> &Url { 116 &self.0 117 } 118 } 119 120 impl std::fmt::Display for PaytoURI { 121 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 122 std::fmt::Display::fmt(self.raw(), f) 123 } 124 } 125 126 #[derive(Debug, thiserror::Error)] 127 pub enum PaytoErr { 128 #[error("invalid payto URI: {0}")] 129 Url(#[from] url::ParseError), 130 #[error("malformed payto URI query: {0}")] 131 Query(#[from] serde_path_to_error::Error<serde_urlencoded::de::Error>), 132 #[error("expected a payto URI got {0}")] 133 NotPayto(CompactString), 134 #[error("unsupported payto kind, expected {0} got {1}")] 135 UnsupportedKind(&'static str, CompactString), 136 #[error("to much path segment for a {0} payto uri")] 137 TooLong(&'static str), 138 #[error("missing segment {0} in path")] 139 MissingSegment(&'static str), 140 #[error("malformed segment {0}: {1}")] 141 MalformedSegment( 142 &'static str, 143 Box<dyn std::error::Error + Sync + Send + 'static>, 144 ), 145 } 146 147 impl PaytoErr { 148 pub fn malformed_segment<E: std::error::Error + Sync + Send + 'static>( 149 segment: &'static str, 150 e: E, 151 ) -> Self { 152 Self::MalformedSegment(segment, Box::new(e)) 153 } 154 } 155 156 impl FromStr for PaytoURI { 157 type Err = PaytoErr; 158 159 fn from_str(s: &str) -> Result<Self, Self::Err> { 160 // Parse url 161 let url: Url = s.parse()?; 162 // Check scheme 163 if url.scheme() != "payto" { 164 return Err(PaytoErr::NotPayto(url.scheme().into())); 165 } 166 Ok(Self(url)) 167 } 168 } 169 170 pub type IbanPayto = Payto<BankID>; 171 pub type FullIbanPayto = FullPayto<BankID>; 172 pub type TransferIbanPayto = TransferPayto<BankID>; 173 174 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 175 pub struct BankID { 176 pub iban: IBAN, 177 pub bic: Option<BIC>, 178 } 179 180 const IBAN: &str = "iban"; 181 182 impl PaytoImpl for BankID { 183 fn as_uri(&self) -> PaytoURI { 184 PaytoURI::from_parts( 185 IBAN, 186 format_args!( 187 "/{}", 188 std::fmt::from_fn(|f| { 189 if let Some(bic) = &self.bic { 190 write!(f, "{bic}/")?; 191 } 192 write!(f, "{}", self.iban) 193 }) 194 ), 195 ) 196 } 197 198 fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> { 199 let url = raw.as_ref(); 200 if url.domain() != Some(IBAN) { 201 return Err(PaytoErr::UnsupportedKind( 202 IBAN, 203 url.domain().unwrap_or_default().into(), 204 )); 205 } 206 let Some(mut segments) = url.path_segments() else { 207 return Err(PaytoErr::MissingSegment("iban")); 208 }; 209 let Some(first) = segments.next() else { 210 return Err(PaytoErr::MissingSegment("iban")); 211 }; 212 let (iban, bic) = match segments.next() { 213 Some(second) => (second, Some(first)), 214 None => (first, None), 215 }; 216 217 Ok(Self { 218 iban: iban 219 .parse() 220 .map_err(|e| PaytoErr::malformed_segment("iban", e))?, 221 bic: bic 222 .map(|bic| { 223 bic.parse() 224 .map_err(|e| PaytoErr::malformed_segment("bic", e)) 225 }) 226 .transpose()?, 227 }) 228 } 229 } 230 231 impl PaytoImpl for IBAN { 232 fn as_uri(&self) -> PaytoURI { 233 PaytoURI::from_parts("iban", format_args!("/{self}")) 234 } 235 236 fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> { 237 raw.as_ref().path_segments().unwrap_or("".split('/')); 238 let payto = BankID::parse(raw)?; 239 Ok(payto.iban) 240 } 241 } 242 243 /// Full payto query 244 #[derive(Debug, Clone, Deserialize)] 245 pub struct FullQuery { 246 #[serde(rename = "receiver-name")] 247 receiver_name: CompactString, 248 } 249 250 /// Transfer payto query 251 #[derive(Debug, Clone, Deserialize)] 252 pub struct TransferQuery { 253 #[serde(rename = "receiver-name")] 254 receiver_name: CompactString, 255 amount: Option<Amount>, 256 message: Option<CompactString>, 257 } 258 259 /// Parsed payto query 260 #[derive(Debug, Clone, Deserialize)] 261 pub struct ParsedQuery { 262 #[serde(rename = "receiver-name")] 263 receiver_name: Option<CompactString>, 264 amount: Option<Amount>, 265 message: Option<CompactString>, 266 #[serde(rename = "ch-qrr")] 267 ch_qrr: Option<CompactString>, 268 } 269 270 #[derive(Debug, Clone, Copy, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] 271 pub struct Payto<P> { 272 inner: P, 273 } 274 275 impl<P> Payto<P> { 276 pub fn convert<T: From<P>>(self) -> Payto<T> { 277 Payto { 278 inner: self.inner.into(), 279 } 280 } 281 } 282 283 impl<P: PaytoImpl> Payto<P> { 284 pub fn new(inner: P) -> Self { 285 Self { inner } 286 } 287 288 pub fn as_uri(&self) -> PaytoURI { 289 self.inner.as_uri() 290 } 291 292 pub fn into_inner(self) -> P { 293 self.inner 294 } 295 } 296 297 impl<P: PaytoImpl> TryFrom<&PaytoURI> for Payto<P> { 298 type Error = PaytoErr; 299 300 fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> { 301 Ok(Self::new(P::parse(value)?)) 302 } 303 } 304 305 impl<P: PaytoImpl> From<FullPayto<P>> for Payto<P> { 306 fn from(value: FullPayto<P>) -> Payto<P> { 307 Self::new(value.inner) 308 } 309 } 310 311 impl<P: PaytoImpl> From<TransferPayto<P>> for Payto<P> { 312 fn from(value: TransferPayto<P>) -> Payto<P> { 313 Self::new(value.inner) 314 } 315 } 316 317 impl<P: PaytoImpl> std::fmt::Display for Payto<P> { 318 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 319 std::fmt::Display::fmt(&self.as_uri(), f) 320 } 321 } 322 323 impl<P: PaytoImpl> FromStr for Payto<P> { 324 type Err = PaytoErr; 325 326 fn from_str(s: &str) -> Result<Self, Self::Err> { 327 let payto: PaytoURI = s.parse()?; 328 Self::try_from(&payto) 329 } 330 } 331 332 impl<P: PaytoImpl> Deref for Payto<P> { 333 type Target = P; 334 335 fn deref(&self) -> &Self::Target { 336 &self.inner 337 } 338 } 339 340 impl<P: PaytoImpl> DerefMut for Payto<P> { 341 fn deref_mut(&mut self) -> &mut Self::Target { 342 &mut self.inner 343 } 344 } 345 346 #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] 347 pub struct FullPayto<P> { 348 inner: P, 349 pub name: CompactString, 350 } 351 352 impl<P: PaytoImpl> FullPayto<P> { 353 pub fn new(inner: P, name: &str) -> Self { 354 Self { 355 inner, 356 name: CompactString::new(name), 357 } 358 } 359 360 pub fn as_uri(&self) -> PaytoURI { 361 self.inner.as_full_uri(&self.name) 362 } 363 364 pub fn into_inner(self) -> P { 365 self.inner 366 } 367 } 368 369 impl<P> FullPayto<P> { 370 pub fn convert<T: From<P>>(self) -> FullPayto<T> { 371 FullPayto { 372 inner: self.inner.into(), 373 name: self.name, 374 } 375 } 376 } 377 378 impl<P: PaytoImpl> TryFrom<&PaytoURI> for FullPayto<P> { 379 type Error = PaytoErr; 380 381 fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> { 382 let payto = P::parse(value)?; 383 let query: FullQuery = value.query()?; 384 Ok(Self { 385 inner: payto, 386 name: query.receiver_name, 387 }) 388 } 389 } 390 391 impl<P: PaytoImpl> From<TransferPayto<P>> for FullPayto<P> { 392 fn from(value: TransferPayto<P>) -> FullPayto<P> { 393 FullPayto { 394 inner: value.inner, 395 name: value.name, 396 } 397 } 398 } 399 400 impl<P: PaytoImpl> std::fmt::Display for FullPayto<P> { 401 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 402 std::fmt::Display::fmt(&self.as_uri(), f) 403 } 404 } 405 406 impl<P: PaytoImpl> FromStr for FullPayto<P> { 407 type Err = PaytoErr; 408 409 fn from_str(s: &str) -> Result<Self, Self::Err> { 410 let raw: PaytoURI = s.parse()?; 411 Self::try_from(&raw) 412 } 413 } 414 415 impl<P: PaytoImpl> Deref for FullPayto<P> { 416 type Target = P; 417 418 fn deref(&self) -> &Self::Target { 419 &self.inner 420 } 421 } 422 423 #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] 424 pub struct TransferPayto<P> { 425 inner: P, 426 pub name: CompactString, 427 pub amount: Option<Amount>, 428 pub subject: Option<CompactString>, 429 } 430 431 impl<P: PaytoImpl> TransferPayto<P> { 432 pub fn new(inner: P, name: &str, amount: Option<Amount>, subject: Option<&str>) -> Self { 433 Self { 434 inner, 435 name: CompactString::new(name), 436 amount, 437 subject: subject.map(CompactString::new), 438 } 439 } 440 441 pub fn as_uri(&self) -> PaytoURI { 442 self.inner 443 .as_transfer_uri(&self.name, self.amount.as_ref(), self.subject.as_deref()) 444 } 445 446 pub fn into_inner(self) -> P { 447 self.inner 448 } 449 } 450 451 impl<P: PaytoImpl> TryFrom<&PaytoURI> for TransferPayto<P> { 452 type Error = PaytoErr; 453 454 fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> { 455 let payto = P::parse(value)?; 456 let query: TransferQuery = value.query()?; 457 Ok(Self { 458 inner: payto, 459 name: query.receiver_name, 460 amount: query.amount, 461 subject: query.message, 462 }) 463 } 464 } 465 466 impl<P: PaytoImpl> std::fmt::Display for TransferPayto<P> { 467 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 468 std::fmt::Display::fmt(&self.as_uri(), f) 469 } 470 } 471 472 impl<P: PaytoImpl> FromStr for TransferPayto<P> { 473 type Err = PaytoErr; 474 475 fn from_str(s: &str) -> Result<Self, Self::Err> { 476 let raw: PaytoURI = s.parse()?; 477 Self::try_from(&raw) 478 } 479 } 480 481 impl<P: PaytoImpl> Deref for TransferPayto<P> { 482 type Target = P; 483 484 fn deref(&self) -> &Self::Target { 485 &self.inner 486 } 487 } 488 489 #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] 490 pub struct ParsedPayto<P> { 491 inner: P, 492 pub name: Option<CompactString>, 493 pub amount: Option<Amount>, 494 pub subject: Option<CompactString>, 495 pub ch_qrr: Option<CompactString>, 496 } 497 498 impl<P: PaytoImpl> ParsedPayto<P> { 499 pub fn new( 500 inner: P, 501 name: Option<&str>, 502 amount: Option<Amount>, 503 subject: Option<&str>, 504 ch_qrr: Option<&str>, 505 ) -> Self { 506 Self { 507 inner, 508 name: name.map(CompactString::new), 509 amount, 510 subject: subject.map(CompactString::new), 511 ch_qrr: ch_qrr.map(CompactString::new), 512 } 513 } 514 515 pub fn as_uri(&self) -> PaytoURI { 516 self.inner 517 .as_uri() 518 .with_query([("receiver-name", &self.name)]) 519 .with_query([("amount", self.amount)]) 520 .with_query([("message", &self.subject)]) 521 .with_query([("ch-qrr", &self.ch_qrr)]) 522 } 523 524 pub fn into_inner(self) -> P { 525 self.inner 526 } 527 } 528 529 impl<P: PaytoImpl> TryFrom<&PaytoURI> for ParsedPayto<P> { 530 type Error = PaytoErr; 531 532 fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> { 533 let payto = P::parse(value)?; 534 let query: ParsedQuery = value.query()?; 535 Ok(Self { 536 inner: payto, 537 name: query.receiver_name, 538 amount: query.amount, 539 subject: query.message, 540 ch_qrr: query.ch_qrr, 541 }) 542 } 543 } 544 545 impl<P: PaytoImpl> std::fmt::Display for ParsedPayto<P> { 546 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 547 std::fmt::Display::fmt(&self.as_uri(), f) 548 } 549 } 550 551 impl<P: PaytoImpl> FromStr for ParsedPayto<P> { 552 type Err = PaytoErr; 553 554 fn from_str(s: &str) -> Result<Self, Self::Err> { 555 let raw: PaytoURI = s.parse()?; 556 Self::try_from(&raw) 557 } 558 } 559 560 impl<P: PaytoImpl> Deref for ParsedPayto<P> { 561 type Target = P; 562 563 fn deref(&self) -> &Self::Target { 564 &self.inner 565 } 566 } 567 568 #[cfg(test)] 569 mod test { 570 use std::str::FromStr as _; 571 572 use crate::types::{ 573 amount::amount, 574 iban::IBAN, 575 payto::{FullPayto, ParsedPayto, Payto, TransferPayto}, 576 }; 577 578 #[test] 579 pub fn parse() { 580 let iban = IBAN::from_str("FR1420041010050500013M02606").unwrap(); 581 582 // Simple payto 583 let simple_payto = Payto::new(iban); 584 assert_eq!( 585 simple_payto, 586 Payto::from_str(&format!("payto://iban/{iban}")).unwrap() 587 ); 588 assert_eq!( 589 simple_payto, 590 Payto::try_from(&simple_payto.as_uri()).unwrap() 591 ); 592 assert_eq!( 593 simple_payto, 594 Payto::from_str(&simple_payto.as_uri().to_string()).unwrap() 595 ); 596 597 // Full payto 598 let full_payto = FullPayto::new(iban, "John Smith"); 599 assert_eq!( 600 full_payto, 601 FullPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith")).unwrap() 602 ); 603 assert_eq!( 604 full_payto, 605 FullPayto::try_from(&full_payto.as_uri()).unwrap() 606 ); 607 assert_eq!( 608 full_payto, 609 FullPayto::from_str(&full_payto.as_uri().to_string()).unwrap() 610 ); 611 assert_eq!(simple_payto, full_payto.clone().into()); 612 613 // Transfer simple payto 614 let transfer_payto = TransferPayto::new(iban, "John Smith", None, None); 615 assert_eq!( 616 transfer_payto, 617 TransferPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith")) 618 .unwrap() 619 ); 620 assert_eq!( 621 transfer_payto, 622 TransferPayto::try_from(&transfer_payto.as_uri()).unwrap() 623 ); 624 assert_eq!( 625 transfer_payto, 626 TransferPayto::from_str(&transfer_payto.as_uri().to_string()).unwrap() 627 ); 628 assert_eq!(full_payto, transfer_payto.clone().into()); 629 630 // Transfer full payto 631 let transfer_payto = TransferPayto::new( 632 iban, 633 "John Smith", 634 Some(amount("EUR:12")), 635 Some("Wire transfer subject"), 636 ); 637 assert_eq!( 638 transfer_payto, 639 TransferPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith&amount=EUR:12&message=Wire+transfer+subject")) 640 .unwrap() 641 ); 642 assert_eq!( 643 transfer_payto, 644 TransferPayto::try_from(&transfer_payto.as_uri()).unwrap() 645 ); 646 assert_eq!( 647 transfer_payto, 648 TransferPayto::from_str(&transfer_payto.as_uri().to_string()).unwrap() 649 ); 650 assert_eq!(full_payto, transfer_payto.clone().into()); 651 652 // Parsed simple payto 653 let transfer_payto = ParsedPayto::new(iban, None, None, None, None); 654 assert_eq!( 655 transfer_payto, 656 ParsedPayto::from_str(&format!("payto://iban/{iban}")).unwrap() 657 ); 658 assert_eq!( 659 transfer_payto, 660 ParsedPayto::try_from(&transfer_payto.as_uri()).unwrap() 661 ); 662 assert_eq!( 663 transfer_payto, 664 ParsedPayto::from_str(&transfer_payto.as_uri().to_string()).unwrap() 665 ); 666 667 // Parsed full payto 668 let transfer_payto = ParsedPayto::new( 669 iban, 670 Some("John Smith"), 671 Some(amount("EUR:12")), 672 Some("Wire transfer subject"), 673 Some("REFERENCE"), 674 ); 675 assert_eq!( 676 transfer_payto, 677 ParsedPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith&amount=EUR:12&message=Wire+transfer+subject&ch-qrr=REFERENCE")) 678 .unwrap() 679 ); 680 assert_eq!( 681 transfer_payto, 682 ParsedPayto::try_from(&transfer_payto.as_uri()).unwrap() 683 ); 684 assert_eq!( 685 transfer_payto, 686 ParsedPayto::from_str(&transfer_payto.as_uri().to_string()).unwrap() 687 ); 688 689 // Malformed 690 let malformed = FullPayto::<IBAN>::from_str( 691 "payto://iban/CH0400766000103138557?receiver-name=NYM%20Technologies%SA", 692 ) 693 .unwrap(); 694 assert_eq!(malformed.as_ref().to_string(), "CH0400766000103138557"); 695 assert_eq!(malformed.name, "NYM Technologies%SA"); 696 } 697 }