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