taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

commit 7a64f76dc2b923b730b5d38c24c00901538a7653
parent 3d298ba6af0174cabf1a811c27a784d9129f30b0
Author: Antoine A <>
Date:   Thu, 13 Feb 2025 16:10:34 +0100

common: add transfer payto

Diffstat:
MMakefile | 4++++
Mcommon/taler-common/src/types/payto.rs | 195+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mtaler-magnet-bank/src/adapter.rs | 8++++----
Mtaler-magnet-bank/src/db.rs | 14+++++++-------
Mtaler-magnet-bank/src/dev.rs | 26++++++++++++++++++--------
Mtaler-magnet-bank/src/lib.rs | 7++++---
Mtaler-magnet-bank/src/worker.rs | 4++--
7 files changed, 229 insertions(+), 29 deletions(-)

diff --git a/Makefile b/Makefile @@ -37,6 +37,10 @@ check: doc: cargo doc +.PHONY: deb +deb: + cargo deb -v -p taler-magnet-bank --deb-version=$(shell ./contrib/ci/version.sh) + .PHONY: ci ci: contrib/ci/run-all-jobs.sh \ No newline at end of file diff --git a/common/taler-common/src/types/payto.rs b/common/taler-common/src/types/payto.rs @@ -38,6 +38,16 @@ pub trait PaytoImpl: Sized { fn as_full_payto(&self, name: &str) -> PaytoURI { self.as_payto().with_query([("receiver-name", name)]) } + fn as_transfer_payto( + &self, + name: &str, + amount: Option<&Amount>, + subject: Option<&str>, + ) -> PaytoURI { + self.as_full_payto(name) + .with_query([("amount", amount)]) + .with_query([("message", subject)]) + } fn parse(uri: &PaytoURI) -> Result<Self, PaytoErr>; } @@ -192,20 +202,22 @@ struct FullQuery { } /// Transfer payto query -// TODO TransferPayto #[derive(Debug, Clone, Deserialize)] struct TransferQuery { - #[serde(flatten)] - full: FullQuery, + #[serde(rename = "receiver-name")] + receiver_name: String, amount: Option<Amount>, - #[serde(rename = "message")] - subject: Option<String>, + message: Option<String>, } #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] pub struct Payto<P>(P); impl<P: PaytoImpl> Payto<P> { + pub fn new(inner: P) -> Self { + Self(inner) + } + pub fn as_payto(&self) -> PaytoURI { self.0.as_payto() } @@ -266,6 +278,12 @@ impl<P: PaytoImpl> FullPayto<P> { } } +impl<P: PaytoImpl> Into<Payto<P>> for FullPayto<P> { + fn into(self) -> Payto<P> { + Payto(self.inner) + } +} + impl<P: PaytoImpl> TryFrom<&PaytoURI> for FullPayto<P> { type Error = PaytoErr; @@ -301,3 +319,170 @@ impl<P: PaytoImpl> Deref for FullPayto<P> { &self.inner } } + +#[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] +pub struct TransferPayto<P> { + inner: P, + pub name: String, + pub amount: Option<Amount>, + pub subject: Option<String>, +} + +impl<P: PaytoImpl> TransferPayto<P> { + pub fn new(inner: P, name: String, amount: Option<Amount>, subject: Option<String>) -> Self { + Self { + inner, + name, + amount, + subject, + } + } + + pub fn as_payto(&self) -> PaytoURI { + self.inner + .as_transfer_payto(&self.name, self.amount.as_ref(), self.subject.as_deref()) + } + + pub fn into_inner(self) -> P { + self.inner + } +} + +impl<P: PaytoImpl> Into<Payto<P>> for TransferPayto<P> { + fn into(self) -> Payto<P> { + Payto(self.inner) + } +} + +impl<P: PaytoImpl> Into<FullPayto<P>> for TransferPayto<P> { + fn into(self) -> FullPayto<P> { + FullPayto { + inner: self.inner, + name: self.name, + } + } +} + +impl<P: PaytoImpl> TryFrom<&PaytoURI> for TransferPayto<P> { + type Error = PaytoErr; + + fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> { + let payto = P::parse(value)?; + let query: TransferQuery = value.query()?; + Ok(Self { + inner: payto, + name: query.receiver_name, + amount: query.amount, + subject: query.message, + }) + } +} + +impl<P: PaytoImpl> std::fmt::Display for TransferPayto<P> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(&self.as_payto(), f) + } +} + +impl<P: PaytoImpl> FromStr for TransferPayto<P> { + type Err = PaytoErr; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let raw: PaytoURI = s.parse()?; + Self::try_from(&raw) + } +} + +impl<P: PaytoImpl> Deref for TransferPayto<P> { + type Target = P; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +#[cfg(test)] +mod test { + use std::str::FromStr as _; + + use crate::types::{ + amount::amount, + iban::IBAN, + payto::{FullPayto, Payto, TransferPayto}, + }; + + #[test] + pub fn parse() { + let iban = IBAN::from_str("FR1420041010050500013M02606").unwrap(); + + // Simple payto + let simple_payto = Payto::new(iban.clone()); + assert_eq!( + simple_payto, + Payto::from_str(&format!("payto://iban/{iban}")).unwrap() + ); + assert_eq!( + simple_payto, + Payto::try_from(&simple_payto.as_payto()).unwrap() + ); + assert_eq!( + simple_payto, + Payto::from_str(&simple_payto.as_payto().to_string()).unwrap() + ); + + // Full payto + let full_payto = FullPayto::new(iban.clone(), "John Smith".to_owned()); + assert_eq!( + full_payto, + FullPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith")).unwrap() + ); + assert_eq!( + full_payto, + FullPayto::try_from(&full_payto.as_payto()).unwrap() + ); + assert_eq!( + full_payto, + FullPayto::from_str(&full_payto.as_payto().to_string()).unwrap() + ); + assert_eq!(simple_payto, full_payto.clone().into()); + + // Transfer simple payto + let transfer_payto = TransferPayto::new(iban.clone(), "John Smith".to_owned(), None, None); + assert_eq!( + transfer_payto, + TransferPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith")) + .unwrap() + ); + assert_eq!( + transfer_payto, + TransferPayto::try_from(&transfer_payto.as_payto()).unwrap() + ); + assert_eq!( + transfer_payto, + TransferPayto::from_str(&transfer_payto.as_payto().to_string()).unwrap() + ); + assert_eq!(full_payto, transfer_payto.clone().into()); + + // Transfer full payto + let transfer_payto = TransferPayto::new( + iban.clone(), + "John Smith".to_owned(), + Some(amount("EUR:12")), + Some("Wire transfer subject".to_owned()), + ); + assert_eq!( + transfer_payto, + TransferPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith&amount=EUR:12&message=Wire+transfer+subject")) + .unwrap() + ); + assert_eq!( + transfer_payto, + TransferPayto::try_from(&transfer_payto.as_payto()).unwrap() + ); + assert_eq!( + transfer_payto, + TransferPayto::from_str(&transfer_payto.as_payto().to_string()).unwrap() + ); + assert_eq!(full_payto, transfer_payto.clone().into()); + } +} diff --git a/taler-magnet-bank/src/adapter.rs b/taler-magnet-bank/src/adapter.rs @@ -36,7 +36,7 @@ use tokio::sync::watch::Sender; use crate::{ constant::CURRENCY, db::{self, AddIncomingResult, TxInAdmin}, - MagnetPayto, + FullHuPayto, }; pub struct MagnetApi { @@ -85,7 +85,7 @@ impl TalerApi for MagnetApi { impl WireGateway for MagnetApi { async fn transfer(&self, req: TransferRequest) -> ApiResult<TransferResponse> { - let creditor = MagnetPayto::try_from(&req.credit_account)?; + let creditor = FullHuPayto::try_from(&req.credit_account)?; let result = db::make_transfer(&self.pool, &req, &creditor, &Timestamp::now()).await?; match result { db::TransferResult::Success { id, timestamp } => Ok(TransferResponse { @@ -139,7 +139,7 @@ impl WireGateway for MagnetApi { &self, req: AddIncomingRequest, ) -> ApiResult<AddIncomingResponse> { - let debtor = MagnetPayto::try_from(&req.debit_account)?; + let debtor = FullHuPayto::try_from(&req.debit_account)?; let res = db::register_tx_in_admin( &self.pool, &TxInAdmin { @@ -166,7 +166,7 @@ impl WireGateway for MagnetApi { } async fn add_incoming_kyc(&self, req: AddKycauthRequest) -> ApiResult<AddKycauthResponse> { - let debtor = MagnetPayto::try_from(&req.debit_account)?; + let debtor = FullHuPayto::try_from(&req.debit_account)?; let res = db::register_tx_in_admin( &self.pool, &TxInAdmin { diff --git a/taler-magnet-bank/src/db.rs b/taler-magnet-bank/src/db.rs @@ -32,7 +32,7 @@ use taler_common::{ }; use tokio::sync::watch::{Receiver, Sender}; -use crate::{constant::CURRENCY, MagnetPayto}; +use crate::{constant::CURRENCY, FullHuPayto}; pub async fn notification_listener( pool: PgPool, @@ -62,7 +62,7 @@ pub struct TxIn { pub code: u64, pub amount: Amount, pub subject: String, - pub debtor: MagnetPayto, + pub debtor: FullHuPayto, pub timestamp: Timestamp, } @@ -84,7 +84,7 @@ pub struct TxOut { pub code: u64, pub amount: Amount, pub subject: String, - pub creditor: MagnetPayto, + pub creditor: FullHuPayto, pub timestamp: Timestamp, } @@ -105,7 +105,7 @@ impl Display for TxOut { pub struct TxInAdmin { pub amount: Amount, pub subject: String, - pub debtor: MagnetPayto, + pub debtor: FullHuPayto, pub timestamp: Timestamp, pub metadata: IncomingSubject, } @@ -132,7 +132,7 @@ pub struct Initiated { pub id: u64, pub amount: Amount, pub subject: String, - pub creditor: MagnetPayto, + pub creditor: FullHuPayto, } impl Display for Initiated { @@ -248,7 +248,7 @@ pub enum TransferResult { pub async fn make_transfer<'a>( db: impl PgExecutor<'a>, req: &TransferRequest, - creditor: &MagnetPayto, + creditor: &FullHuPayto, timestamp: &Timestamp, ) -> sqlx::Result<TransferResult> { let subject = format!("{} {}", req.wtid, req.exchange_base_url); @@ -548,7 +548,7 @@ pub async fn pending_batch<'a>( id: r.try_get_u64(0)?, amount: r.try_get_amount_i(1, CURRENCY)?, subject: r.try_get(3)?, - creditor: MagnetPayto::new(r.try_get_parse(4)?, r.try_get(5)?), + creditor: FullHuPayto::new(r.try_get_parse(4)?, r.try_get(5)?), }) }) .fetch_all(db) diff --git a/taler-magnet-bank/src/dev.rs b/taler-magnet-bank/src/dev.rs @@ -14,6 +14,7 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +use anyhow::anyhow; use clap::ValueEnum; use jiff::Zoned; use taler_common::{ @@ -27,7 +28,7 @@ use crate::{ keys, magnet::{AuthClient, Direction}, worker::{extract_tx_info, Tx}, - HuPayto, MagnetPayto, + FullHuPayto, HuPayto, TransferHuPayto, }; #[derive(Debug, Clone, PartialEq, Eq, ValueEnum)] @@ -50,13 +51,13 @@ pub enum DevCmd { }, Transfer { #[clap(long)] - debtor: HuPayto, + debtor: TransferHuPayto, #[clap(long)] - creditor: MagnetPayto, + creditor: FullHuPayto, #[clap(long)] - amount: Amount, + amount: Option<Amount>, #[clap(long)] - subject: String, + subject: Option<String>, }, } @@ -107,13 +108,22 @@ pub async fn dev(cfg: Config, cmd: DevCmd) -> anyhow::Result<()> { amount, subject, } => { - let debtor = client.account(debtor.bban()).await?; + let account = client.account(debtor.bban()).await?; + let amount = debtor + .amount + .or(amount) + .ok_or_else(|| anyhow!("Missing amount"))?; + let subject = debtor + .subject + .or(subject) + .ok_or_else(|| anyhow!("Missing subject"))?; + let now = Zoned::now(); let date = now.date(); let init = client .init_tx( - debtor.code, + account.code, amount.val as f64, &subject, &date, @@ -124,7 +134,7 @@ pub async fn dev(cfg: Config, cmd: DevCmd) -> anyhow::Result<()> { client .sign_tx( &keys.signing_key, - &debtor.number, + &account.number, init.code, init.amount, &date, diff --git a/taler-magnet-bank/src/lib.rs b/taler-magnet-bank/src/lib.rs @@ -18,7 +18,7 @@ use std::{borrow::Cow, str::FromStr}; use taler_common::types::{ iban::{IbanErrorKind, ParseIbanError, IBAN}, - payto::{FullPayto, IbanPayto, Payto, PaytoErr, PaytoImpl, PaytoURI}, + payto::{FullPayto, IbanPayto, Payto, PaytoErr, PaytoImpl, PaytoURI, TransferPayto}, }; pub mod adapter; @@ -160,12 +160,13 @@ impl FromStr for HuIban { } /// Parse a magnet payto URI, panic if malformed -pub fn magnet_payto(url: impl AsRef<str>) -> MagnetPayto { +pub fn magnet_payto(url: impl AsRef<str>) -> FullHuPayto { url.as_ref().parse().expect("invalid magnet payto") } -pub type MagnetPayto = FullPayto<HuIban>; pub type HuPayto = Payto<HuIban>; +pub type FullHuPayto = FullPayto<HuIban>; +pub type TransferHuPayto = TransferPayto<HuIban>; #[cfg(test)] mod test { diff --git a/taler-magnet-bank/src/worker.rs b/taler-magnet-bank/src/worker.rs @@ -29,7 +29,7 @@ use crate::{ db::{self, AddIncomingResult, Initiated, TxIn, TxOut}, failure_injection::fail_point, magnet::{error::ApiError, ApiClient, Direction, Transaction}, - HuIban, MagnetPayto, + FullHuPayto, HuIban, }; #[derive(Debug, thiserror::Error)] @@ -225,7 +225,7 @@ pub fn extract_tx_info(tx: Transaction) -> Tx { } else { HuIban::from_bban(&tx.counter_account).unwrap() }; - let counter_account = MagnetPayto::new(iban, tx.counter_name); + let counter_account = FullHuPayto::new(iban, tx.counter_name); if tx.amount.is_sign_positive() { Tx::In(TxIn { code: tx.code,