commit 7a64f76dc2b923b730b5d38c24c00901538a7653
parent 3d298ba6af0174cabf1a811c27a784d9129f30b0
Author: Antoine A <>
Date: Thu, 13 Feb 2025 16:10:34 +0100
common: add transfer payto
Diffstat:
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,