taler-rust

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

commit 9ca55a895159377e8a0f9f1069599a5bc5c0c019
parent 1508d487199536c1af70f8e19c086b811e05a534
Author: Antoine A <>
Date:   Tue,  4 Feb 2025 14:00:06 +0100

common: better payto abstraction

Diffstat:
MCargo.lock | 42+++++++++++++++++++++---------------------
Mcommon/taler-api/src/db.rs | 23+++++++++++++++++------
Mcommon/taler-api/tests/common/db.rs | 4++--
Mcommon/taler-common/src/api_revenue.rs | 6+++---
Mcommon/taler-common/src/api_wire.rs | 26+++++++++++++-------------
Mcommon/taler-common/src/config.rs | 4++--
Mcommon/taler-common/src/types/payto.rs | 192+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mcommon/taler-common/src/types/timestamp.rs | 11+++++++++++
Mcommon/taler-test-utils/src/routine.rs | 10+++++-----
Mtaler-magnet-bank/src/adapter.rs | 6+++---
Mtaler-magnet-bank/src/db.rs | 49+++++++++----------------------------------------
Mtaler-magnet-bank/src/dev.rs | 33+++++++++------------------------
Mtaler-magnet-bank/src/lib.rs | 89++++++++++++++++++++++++++++++++++++-------------------------------------------
Mtaler-magnet-bank/src/main.rs | 8++++----
Mtaler-magnet-bank/src/worker.rs | 5+----
15 files changed, 299 insertions(+), 209 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -286,9 +286,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" [[package]] name = "bytesize" @@ -359,9 +359,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.27" +version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" +checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff" dependencies = [ "clap_builder", "clap_derive", @@ -381,9 +381,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.24" +version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" +checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" dependencies = [ "heck", "proc-macro2", @@ -1393,9 +1393,9 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jiff" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c607c728e28764fecde611a2764a3a5db19ae21dcec46f292244f5cc5c085a81" +checksum = "c04ef77ae73f3cf50510712722f0c4e8b46f5aaa1bf5ffad2ae213e6495e78e5" dependencies = [ "log", "portable-atomic", @@ -1606,9 +1606,9 @@ checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" [[package]] name = "openssl" -version = "0.10.69" +version = "0.10.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e534d133a060a3c19daec1eb3e98ec6f4685978834f2dbadfe2ec215bab64e" +checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6" dependencies = [ "bitflags", "cfg-if", @@ -1638,9 +1638,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.104" +version = "0.9.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc" dependencies = [ "cc", "libc", @@ -1861,7 +1861,7 @@ checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.0", - "zerocopy 0.8.14", + "zerocopy 0.8.15", ] [[package]] @@ -1900,7 +1900,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" dependencies = [ "getrandom 0.3.1", - "zerocopy 0.8.14", + "zerocopy 0.8.15", ] [[package]] @@ -2487,9 +2487,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.96" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", @@ -3367,11 +3367,11 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.14" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468" +checksum = "a1e101d4bc320b6f9abb68846837b70e25e380ca2f467ab494bf29fcc435fcc3" dependencies = [ - "zerocopy-derive 0.8.14", + "zerocopy-derive 0.8.15", ] [[package]] @@ -3387,9 +3387,9 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.8.14" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1" +checksum = "03a73df1008145cd135b3c780d275c57c3e6ba8324a41bd5e0008fe167c3bc7c" dependencies = [ "proc-macro2", "quote", diff --git a/common/taler-api/src/db.rs b/common/taler-api/src/db.rs @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -use std::time::Duration; +use std::{str::FromStr, time::Duration}; use sqlx::{ error::BoxDynError, postgres::PgRow, query::Query, Decode, Error, PgExecutor, PgPool, @@ -28,7 +28,7 @@ use taler_common::{ amount::{Amount, Decimal}, base32::Base32, iban::IBAN, - payto::Payto, + payto::PaytoURI, timestamp::Timestamp, }, }; @@ -153,6 +153,10 @@ pub trait TypeHelper { index: I, map: M, ) -> sqlx::Result<R>; + fn try_get_parse<'r, I: sqlx::ColumnIndex<Self>, E: Into<BoxDynError>, T: FromStr<Err = E>>( + &'r self, + index: I, + ) -> sqlx::Result<T>; fn try_get_timestamp<I: sqlx::ColumnIndex<Self>>(&self, index: I) -> sqlx::Result<Timestamp> { self.try_get_map(index, Timestamp::from_sql_micros) } @@ -172,13 +176,13 @@ pub trait TypeHelper { self.try_get_map(index, |slice: &[u8]| Base32::try_from(slice)) } fn try_get_url<I: sqlx::ColumnIndex<Self>>(&self, index: I) -> sqlx::Result<Url> { - self.try_get_map(index, Url::parse) + self.try_get_parse(index) } - fn try_get_payto<I: sqlx::ColumnIndex<Self>>(&self, index: I) -> sqlx::Result<Payto> { - self.try_get_map(index, |s: &str| s.parse()) + fn try_get_payto<I: sqlx::ColumnIndex<Self>>(&self, index: I) -> sqlx::Result<PaytoURI> { + self.try_get_parse(index) } fn try_get_iban<I: sqlx::ColumnIndex<Self>>(&self, index: I) -> sqlx::Result<IBAN> { - self.try_get_map(index, |s: &str| s.parse()) + self.try_get_parse(index) } fn try_get_amount(&self, index: &str, currency: &str) -> sqlx::Result<Amount>; fn try_get_amount_i(&self, index: usize, currency: &str) -> sqlx::Result<Amount>; @@ -204,6 +208,13 @@ impl TypeHelper for PgRow { }) } + fn try_get_parse<I: sqlx::ColumnIndex<Self>, E: Into<BoxDynError>, T: FromStr<Err = E>>( + &self, + index: I, + ) -> sqlx::Result<T> { + self.try_get_map(index, |s: &str| s.parse()) + } + fn try_get_amount(&self, index: &str, currency: &str) -> sqlx::Result<Amount> { let val_idx = format!("{index}_val"); let frac_idx = format!("{index}_frac"); diff --git a/common/taler-api/tests/common/db.rs b/common/taler-api/tests/common/db.rs @@ -24,7 +24,7 @@ use taler_common::{ IncomingBankTransaction, OutgoingBankTransaction, TransferListStatus, TransferRequest, TransferResponse, TransferState, TransferStatus, }, - types::{amount::Amount, payto::Payto, timestamp::Timestamp}, + types::{amount::Amount, payto::PaytoURI, timestamp::Timestamp}, }; use tokio::sync::watch::{Receiver, Sender}; @@ -200,7 +200,7 @@ pub enum AddIncomingResult { pub async fn add_incoming( db: &PgPool, amount: &Amount, - debit_account: &Payto, + debit_account: &PaytoURI, subject: &str, timestamp: &Timestamp, kind: IncomingType, diff --git a/common/taler-common/src/api_revenue.rs b/common/taler-common/src/api_revenue.rs @@ -16,7 +16,7 @@ //! Type for the Taler Wire Gateway HTTP API <https://docs.taler.net/core/api-bank-wire.html#taler-wire-gateway-http-api> -use crate::types::{amount::Amount, payto::Payto, timestamp::Timestamp}; +use crate::types::{amount::Amount, payto::PaytoURI, timestamp::Timestamp}; use super::api_common::SafeU64; use serde::{Deserialize, Serialize}; @@ -34,7 +34,7 @@ pub struct RevenueConfig<'a> { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct RevenueIncomingHistory { pub incoming_transactions: Vec<RevenueIncomingBankTransaction>, - pub credit_account: Payto, + pub credit_account: PaytoURI, } /// <https://docs.taler.net/core/api-bank-revenue.html#tsref-type-RevenueIncomingBankTransaction> @@ -44,6 +44,6 @@ pub struct RevenueIncomingBankTransaction { pub date: Timestamp, pub amount: Amount, pub credit_fee: Option<Amount>, - pub debit_account: Payto, + pub debit_account: PaytoURI, pub subject: String, } diff --git a/common/taler-common/src/api_wire.rs b/common/taler-common/src/api_wire.rs @@ -18,7 +18,7 @@ use url::Url; -use crate::types::{amount::Amount, payto::Payto, timestamp::Timestamp}; +use crate::types::{amount::Amount, payto::PaytoURI, timestamp::Timestamp}; use super::api_common::{EddsaPublicKey, HashCode, SafeU64, ShortHashCode, WadId}; use serde::{Deserialize, Serialize}; @@ -46,14 +46,14 @@ pub struct TransferRequest { pub amount: Amount, pub exchange_base_url: Url, pub wtid: ShortHashCode, - pub credit_account: Payto, + pub credit_account: PaytoURI, } /// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-TransferList> #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct TransferList { pub transfers: Vec<TransferListStatus>, - pub debit_account: Payto, + pub debit_account: PaytoURI, } /// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-TransferListStatus> @@ -62,7 +62,7 @@ pub struct TransferListStatus { pub row_id: SafeU64, pub status: TransferState, pub amount: Amount, - pub credit_account: Payto, + pub credit_account: PaytoURI, pub timestamp: Timestamp, } @@ -74,7 +74,7 @@ pub struct TransferStatus { pub amount: Amount, pub origin_exchange_url: String, pub wtid: ShortHashCode, - pub credit_account: Payto, + pub credit_account: PaytoURI, pub timestamp: Timestamp, } @@ -82,7 +82,7 @@ pub struct TransferStatus { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OutgoingHistory { pub outgoing_transactions: Vec<OutgoingBankTransaction>, - pub debit_account: Payto, + pub debit_account: PaytoURI, } /// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-OutgoingBankTransaction> @@ -91,7 +91,7 @@ pub struct OutgoingBankTransaction { pub row_id: SafeU64, pub date: Timestamp, pub amount: Amount, - pub credit_account: Payto, + pub credit_account: PaytoURI, pub wtid: ShortHashCode, pub exchange_base_url: Url, } @@ -99,7 +99,7 @@ pub struct OutgoingBankTransaction { /// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-IncomingHistory> #[derive(Debug, Clone, Serialize, Deserialize)] pub struct IncomingHistory { - pub credit_account: Payto, + pub credit_account: PaytoURI, pub incoming_transactions: Vec<IncomingBankTransaction>, } @@ -111,7 +111,7 @@ pub enum IncomingBankTransaction { row_id: SafeU64, date: Timestamp, amount: Amount, - debit_account: Payto, + debit_account: PaytoURI, reserve_pub: EddsaPublicKey, }, #[serde(rename = "WAD")] @@ -119,7 +119,7 @@ pub enum IncomingBankTransaction { row_id: SafeU64, date: Timestamp, amount: Amount, - debit_account: Payto, + debit_account: PaytoURI, origin_exchange_url: Url, wad_id: WadId, }, @@ -128,7 +128,7 @@ pub enum IncomingBankTransaction { row_id: SafeU64, date: Timestamp, amount: Amount, - debit_account: Payto, + debit_account: PaytoURI, account_pub: EddsaPublicKey, }, } @@ -138,7 +138,7 @@ pub enum IncomingBankTransaction { pub struct AddIncomingRequest { pub amount: Amount, pub reserve_pub: EddsaPublicKey, - pub debit_account: Payto, + pub debit_account: PaytoURI, } /// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-AddIncomingResponse> @@ -153,7 +153,7 @@ pub struct AddIncomingResponse { pub struct AddKycauthRequest { pub amount: Amount, pub account_pub: EddsaPublicKey, - pub debit_account: Payto, + pub debit_account: PaytoURI, } /// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-AddKycauthResponse> diff --git a/common/taler-common/src/config.rs b/common/taler-common/src/config.rs @@ -26,7 +26,7 @@ use url::Url; use crate::types::{ amount::{Amount, Currency}, - payto::Payto, + payto::PaytoURI, }; pub mod parser { @@ -767,7 +767,7 @@ impl<'cfg, 'arg> Section<'cfg, 'arg> { } /** Access [option] as payto */ - pub fn payto(&self, option: &'arg str) -> Value<'arg, Payto> { + pub fn payto(&self, option: &'arg str) -> Value<'arg, PaytoURI> { self.parse("payto", option) } diff --git a/common/taler-common/src/types/payto.rs b/common/taler-common/src/types/payto.rs @@ -15,39 +15,48 @@ */ use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_with::{DeserializeFromStr, SerializeDisplay}; use std::{ fmt::{Debug, Display}, + ops::Deref, str::FromStr, }; use url::Url; -use super::iban::{ParseBicError, ParseIbanError, BIC, IBAN}; +use super::{ + amount::Amount, + iban::{ParseBicError, ParseIbanError, BIC, IBAN}, +}; /// Parse a payto URI, panic if malformed -pub fn payto(url: impl AsRef<str>) -> Payto { +pub fn payto(url: impl AsRef<str>) -> PaytoURI { url.as_ref().parse().expect("invalid payto") } +pub trait PaytoImpl: Sized { + fn as_payto(&self) -> PaytoURI; + fn as_full_payto(&self, name: &str) -> PaytoURI { + self.as_payto().with_query([("receiver-name", name)]) + } + fn parse(uri: &PaytoURI) -> Result<Self, PaytoErr>; +} + /// A generic RFC 8905 payto URI #[derive( Debug, Clone, PartialEq, Eq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay, )] -pub struct Payto(Url); +pub struct PaytoURI(Url); -impl Payto { +impl PaytoURI { pub fn raw(&self) -> &str { self.0.as_str() } - pub fn full(self, name: impl AsRef<str>) -> Self { - self.with_query([("receiver-name", name.as_ref())]) - } - pub fn from_parts(domain: &str, path: impl Display) -> Self { payto(format!("payto://{domain}{path}")) } - pub fn query<Q: DeserializeOwned>(&self) -> Result<Q, PaytoErr> { + fn query<Q: DeserializeOwned>(&self) -> Result<Q, PaytoErr> { let query = self.0.query().unwrap_or_default().as_bytes(); let de = serde_urlencoded::Deserializer::new(url::form_urlencoded::parse(query)); serde_path_to_error::deserialize(de).map_err(PaytoErr::Query) @@ -64,13 +73,13 @@ impl Payto { } } -impl AsRef<Url> for Payto { +impl AsRef<Url> for PaytoURI { fn as_ref(&self) -> &Url { &self.0 } } -impl std::fmt::Display for Payto { +impl std::fmt::Display for PaytoURI { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(self.raw(), f) } @@ -98,7 +107,7 @@ impl PaytoErr { } } -impl FromStr for Payto { +impl FromStr for PaytoURI { type Err = PaytoErr; fn from_str(s: &str) -> Result<Self, Self::Err> { @@ -112,24 +121,19 @@ impl FromStr for Payto { } } +pub type IbanPayto = Payto<IbanBic>; +pub type FullIbanPayto = FullPayto<IbanPayto>; + #[derive(Debug, Clone, PartialEq, Eq)] -pub struct IbanPayto { +pub struct IbanBic { pub iban: IBAN, pub bic: Option<BIC>, } -impl IbanPayto { - pub fn as_payto(&self) -> Payto { - Payto::from_parts("iban", format_args!("/{}", self.iban)) - } - - pub fn as_full_payto(&self, name: &str) -> Payto { - self.as_payto().full(name) - } -} +const IBAN: &str = "iban"; #[derive(Debug, thiserror::Error)] -pub enum IbanPaytoErr { +pub enum IbanBicErr { #[error("missing IBAN in path")] MissingIban, #[error(transparent)] @@ -138,13 +142,13 @@ pub enum IbanPaytoErr { BIC(#[from] ParseBicError), } -const IBAN: &str = "iban"; - -impl TryFrom<&Payto> for IbanPayto { - type Error = PaytoErr; +impl PaytoImpl for IbanBic { + fn as_payto(&self) -> PaytoURI { + PaytoURI::from_parts(IBAN, format_args!("/{}", self.iban)) + } - fn try_from(value: &Payto) -> Result<Self, Self::Error> { - let url = value.as_ref(); + fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> { + let url = raw.as_ref(); if url.domain() != Some(IBAN) { return Err(PaytoErr::UnsupportedKind( IBAN, @@ -152,10 +156,10 @@ impl TryFrom<&Payto> for IbanPayto { )); } let Some(mut segments) = url.path_segments() else { - return Err(PaytoErr::custom(IbanPaytoErr::MissingIban)); + return Err(PaytoErr::custom(IbanBicErr::MissingIban)); }; let Some(first) = segments.next() else { - return Err(PaytoErr::custom(IbanPaytoErr::MissingIban)); + return Err(PaytoErr::custom(IbanBicErr::MissingIban)); }; let (iban, bic) = match segments.next() { Some(second) => ( @@ -169,9 +173,131 @@ impl TryFrom<&Payto> for IbanPayto { } } +impl PaytoImpl for IBAN { + fn as_payto(&self) -> PaytoURI { + PaytoURI::from_parts(IBAN, format_args!("/{}", self)) + } + + fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> { + let payto = IbanBic::parse(raw)?; + Ok(payto.iban) + } +} + /// Full payto query #[derive(Debug, Clone, Deserialize)] -pub struct FullQuery { +struct FullQuery { #[serde(rename = "receiver-name")] - pub receiver_name: String, + receiver_name: String, +} + +/// Transfer payto query +// TODO TransferPayto +#[derive(Debug, Clone, Deserialize)] +struct TransferQuery { + #[serde(flatten)] + full: FullQuery, + amount: Option<Amount>, + #[serde(rename = "message")] + subject: Option<String>, +} + +#[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] +pub struct Payto<P>(P); + +impl<P: PaytoImpl> Payto<P> { + pub fn as_payto(&self) -> PaytoURI { + self.0.as_payto() + } + + pub fn into_inner(self) -> P { + self.0 + } +} + +impl<P: PaytoImpl> TryFrom<&PaytoURI> for Payto<P> { + type Error = PaytoErr; + + fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> { + Ok(Self(P::parse(value)?)) + } +} + +impl<P: PaytoImpl> std::fmt::Display for Payto<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 Payto<P> { + type Err = PaytoErr; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let payto: PaytoURI = s.parse()?; + Self::try_from(&payto) + } +} + +impl<P: PaytoImpl> Deref for Payto<P> { + type Target = P; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] +pub struct FullPayto<P> { + inner: P, + pub name: String, +} + +impl<P: PaytoImpl> FullPayto<P> { + pub fn new(inner: P, name: String) -> Self { + Self { inner, name } + } + + pub fn as_payto(&self) -> PaytoURI { + self.inner.as_full_payto(&self.name) + } + + pub fn into_inner(self) -> P { + self.inner + } +} + +impl<P: PaytoImpl> TryFrom<&PaytoURI> for FullPayto<P> { + type Error = PaytoErr; + + fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> { + let payto = P::parse(value)?; + let query: FullQuery = value.query()?; + Ok(Self { + inner: payto, + name: query.receiver_name, + }) + } +} + +impl<P: PaytoImpl> std::fmt::Display for FullPayto<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 FullPayto<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 FullPayto<P> { + type Target = P; + + fn deref(&self) -> &Self::Target { + &self.inner + } } diff --git a/common/taler-common/src/types/timestamp.rs b/common/taler-common/src/types/timestamp.rs @@ -16,6 +16,7 @@ use std::fmt::Display; +use jiff::{civil::Time, tz::TimeZone}; use serde::{de::Error, ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer}; use serde_json::Value; @@ -119,3 +120,13 @@ impl From<jiff::Timestamp> for Timestamp { Self::Time(time) } } + +impl From<jiff::civil::Date> for Timestamp { + fn from(date: jiff::civil::Date) -> Self { + date.to_datetime(Time::midnight()) + .to_zoned(TimeZone::UTC) + .unwrap() + .timestamp() + .into() + } +} diff --git a/common/taler-test-utils/src/routine.rs b/common/taler-test-utils/src/routine.rs @@ -35,7 +35,7 @@ use taler_common::{ TransferStatus, }, error_code::ErrorCode, - types::{amount::amount, base32::Base32, payto::Payto, url}, + types::{amount::amount, base32::Base32, payto::PaytoURI, url}, }; use tokio::time::sleep; @@ -267,7 +267,7 @@ async fn get_currency(server: &TestServer) -> String { pub async fn transfer_routine( server: &TestServer, default_status: TransferState, - credit_account: &Payto, + credit_account: &PaytoURI, ) { let currency = &get_currency(server).await; let default_amount = amount(format!("{currency}:42")); @@ -420,7 +420,7 @@ async fn add_incoming_routine( server: &TestServer, currency: &str, kind: IncomingType, - debit_acount: &Payto, + debit_acount: &PaytoURI, ) { let (path, key) = match kind { IncomingType::reserve => ("/taler-wire-gateway/admin/add-incoming", "reserve_pub"), @@ -491,7 +491,7 @@ async fn add_incoming_routine( } /// Test standard behavior of the revenue endpoints -pub async fn revenue_routine(server: &TestServer, debit_acount: &Payto) { +pub async fn revenue_routine(server: &TestServer, debit_acount: &PaytoURI) { let currency = &get_currency(server).await; routine_history( @@ -534,7 +534,7 @@ pub async fn revenue_routine(server: &TestServer, debit_acount: &Payto) { } /// Test standard behavior of the admin add incoming endpoints -pub async fn admin_add_incoming_routine(server: &TestServer, debit_acount: &Payto) { +pub async fn admin_add_incoming_routine(server: &TestServer, debit_acount: &PaytoURI) { let currency = &get_currency(server).await; // History diff --git a/taler-magnet-bank/src/adapter.rs b/taler-magnet-bank/src/adapter.rs @@ -29,7 +29,7 @@ use taler_common::{ TransferState, TransferStatus, }, error_code::ErrorCode, - types::{payto::Payto, timestamp::Timestamp}, + types::{payto::PaytoURI, timestamp::Timestamp}, }; use tokio::sync::watch::Sender; @@ -41,7 +41,7 @@ use crate::{ pub struct MagnetApi { pub pool: sqlx::PgPool, - pub payto: Payto, + pub payto: PaytoURI, pub in_channel: Sender<i64>, pub taler_in_channel: Sender<i64>, pub out_channel: Sender<i64>, @@ -49,7 +49,7 @@ pub struct MagnetApi { } impl MagnetApi { - pub async fn start(pool: sqlx::PgPool, payto: Payto) -> Self { + pub async fn start(pool: sqlx::PgPool, payto: PaytoURI) -> Self { let in_channel = Sender::new(0); let taler_in_channel = Sender::new(0); let out_channel = Sender::new(0); diff --git a/taler-magnet-bank/src/db.rs b/taler-magnet-bank/src/db.rs @@ -28,11 +28,11 @@ use taler_common::{ IncomingBankTransaction, OutgoingBankTransaction, TransferListStatus, TransferRequest, TransferState, TransferStatus, }, - types::{amount::Amount, iban::IBAN, payto::IbanPayto, timestamp::Timestamp}, + types::{amount::Amount, payto::PaytoImpl as _, timestamp::Timestamp}, }; use tokio::sync::watch::{Receiver, Sender}; -use crate::{constant::CURRENCY, HuIban, MagnetPayto}; +use crate::{constant::CURRENCY, MagnetPayto}; pub async fn notification_listener( pool: PgPool, @@ -328,11 +328,7 @@ pub async fn transfer_page<'a>( row_id: r.try_get_safeu64(0)?, status: r.try_get(1)?, amount: r.try_get_amount_i(2, CURRENCY)?, - credit_account: IbanPayto { - iban: r.try_get_iban(4)?, - bic: None, - } - .as_full_payto(r.try_get(5)?), + credit_account: r.try_get_iban(4)?.as_full_payto(r.try_get(5)?), timestamp: r.try_get_timestamp(6)?, }) }, @@ -372,11 +368,7 @@ pub async fn outgoing_history( Ok(OutgoingBankTransaction { row_id: r.try_get_safeu64(0)?, amount: r.try_get_amount_i(1, CURRENCY)?, - credit_account: IbanPayto { - iban: r.try_get_iban(3)?, - bic: None, - } - .as_full_payto(r.try_get(4)?), + credit_account: r.try_get_iban(3)?.as_full_payto(r.try_get(4)?), date: r.try_get_timestamp(5)?, exchange_base_url: r.try_get_url(6)?, wtid: r.try_get_base32(7)?, @@ -419,22 +411,14 @@ pub async fn incoming_history( IncomingType::reserve => IncomingBankTransaction::Reserve { row_id: r.try_get_safeu64(1)?, amount: r.try_get_amount_i(2, CURRENCY)?, - debit_account: IbanPayto { - iban: r.try_get_iban(4)?, - bic: None, - } - .as_full_payto(r.try_get(5)?), + debit_account: r.try_get_iban(4)?.as_full_payto(r.try_get(5)?), date: r.try_get_timestamp(6)?, reserve_pub: r.try_get_base32(7)?, }, IncomingType::kyc => IncomingBankTransaction::Kyc { row_id: r.try_get_safeu64(1)?, amount: r.try_get_amount_i(2, CURRENCY)?, - debit_account: IbanPayto { - iban: r.try_get_iban(4)?, - bic: None, - } - .as_full_payto(r.try_get(5)?), + debit_account: r.try_get_iban(4)?.as_full_payto(r.try_get(5)?), date: r.try_get_timestamp(6)?, account_pub: r.try_get_base32(7)?, }, @@ -479,11 +463,7 @@ pub async fn revenue_history( date: r.try_get_timestamp(1)?, amount: r.try_get_amount_i(2, CURRENCY)?, credit_fee: None, - debit_account: IbanPayto { - iban: r.try_get_iban(4)?, - bic: None, - } - .as_full_payto(r.try_get(5)?), + debit_account: r.try_get_iban(4)?.as_full_payto(r.try_get(5)?), subject: r.try_get(6)?, }) }, @@ -520,11 +500,7 @@ pub async fn transfer_by_id<'a>( amount: r.try_get_amount_i(2, CURRENCY)?, origin_exchange_url: r.try_get(4)?, wtid: r.try_get_base32(5)?, - credit_account: IbanPayto { - iban: r.try_get_iban(6)?, - bic: None, - } - .as_full_payto(r.try_get(7)?), + credit_account: r.try_get_iban(6)?.as_full_payto(r.try_get(7)?), timestamp: r.try_get_timestamp(8)?, }) }) @@ -550,14 +526,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 { - iban: r.try_get_map(4, |s: &str| { - let iban: IBAN = s.parse()?; - let it = HuIban::try_from(iban)?; - anyhow::Ok(it) - })?, - name: r.try_get(5)?, - }, + creditor: MagnetPayto::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 @@ -18,11 +18,7 @@ use clap::ValueEnum; use jiff::Zoned; use taler_common::{ config::Config, - types::{ - amount::Amount, - iban::IBAN, - payto::{FullQuery, IbanPayto, Payto}, - }, + types::{amount::Amount, iban::IBAN, payto::PaytoImpl}, }; use tracing::info; @@ -31,7 +27,7 @@ use crate::{ keys, magnet::{AuthClient, Direction}, worker::{extract_tx_info, Tx}, - MagnetPayto, + HuPayto, MagnetPayto, }; #[derive(Debug, Clone, PartialEq, Eq, ValueEnum)] @@ -48,15 +44,15 @@ pub enum DevCmd { /// Print account info Accounts, Tx { - account: Payto, + account: HuPayto, #[clap(long, short, value_enum, default_value_t = DirArg::Both)] direction: DirArg, }, Transfer { #[clap(long)] - debtor: Payto, + debtor: HuPayto, #[clap(long)] - creditor: Payto, + creditor: MagnetPayto, #[clap(long)] amount: Amount, #[clap(long)] @@ -75,18 +71,12 @@ pub async fn dev(cfg: Config, cmd: DevCmd) -> anyhow::Result<()> { for partner in res.partners { for account in partner.bank_accounts { let iban: IBAN = account.iban.parse()?; - let payto = IbanPayto { iban, bic: None }; - info!( - "{} {} {}", - account.code, - account.currency.symbol, - payto.as_full_payto(&partner.partner.name) - ); + let payto = iban.as_full_payto(&partner.partner.name); + info!("{} {} {}", account.code, account.currency.symbol, payto); } } } DevCmd::Tx { account, direction } => { - let account = MagnetPayto::try_from(&account)?; let dir = match direction { DirArg::Incoming => Direction::Incoming, DirArg::Outgoing => Direction::Outgoing, @@ -95,9 +85,7 @@ pub async fn dev(cfg: Config, cmd: DevCmd) -> anyhow::Result<()> { // Register incoming let mut next = None; loop { - let page = client - .page_tx(dir, 5, account.bban(), &next, &None) - .await?; + let page = client.page_tx(dir, 5, account.bban(), &next, &None).await?; next = page.next; for item in page.list { let tx = extract_tx_info(item.tx); @@ -117,9 +105,6 @@ pub async fn dev(cfg: Config, cmd: DevCmd) -> anyhow::Result<()> { amount, subject, } => { - let full: FullQuery = creditor.query()?; - let debtor = MagnetPayto::try_from(&debtor)?; - let creditor = MagnetPayto::try_from(&creditor)?; let debtor = client.account(debtor.bban()).await?; let now = Zoned::now(); let date = now.date(); @@ -130,7 +115,7 @@ pub async fn dev(cfg: Config, cmd: DevCmd) -> anyhow::Result<()> { amount.val as f64, &subject, &date, - &full.receiver_name, + &creditor.name, creditor.bban(), ) .await? diff --git a/taler-magnet-bank/src/lib.rs b/taler-magnet-bank/src/lib.rs @@ -14,11 +14,11 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -use std::borrow::Cow; +use std::{borrow::Cow, str::FromStr}; use taler_common::types::{ - iban::IBAN, - payto::{FullQuery, IbanPayto, Payto, PaytoErr}, + iban::{IbanErrorKind, ParseIbanError, IBAN}, + payto::{FullPayto, IbanPayto, Payto, PaytoErr, PaytoImpl, PaytoURI}, }; pub mod adapter; @@ -92,6 +92,10 @@ impl HuIban { let bban = self.0.bban(); bban.strip_suffix("00000000").unwrap_or(bban) } + + pub fn iban(&self) -> &str { + self.0.as_ref() + } } #[derive(Debug, thiserror::Error)] @@ -104,6 +108,14 @@ pub enum HuIbanErr { BbanSize(usize), #[error("invalid checkum for {0} expected {1} got {2}")] Checksum(&'static str, u8, u8), + #[error(transparent)] + Iban(IbanErrorKind), +} + +impl From<ParseIbanError> for HuIbanErr { + fn from(value: ParseIbanError) -> Self { + Self::Iban(value.kind) + } } impl HuIbanErr { @@ -121,63 +133,46 @@ impl TryFrom<IBAN> for HuIban { return Err(HuIbanErr::CountryCode(country_code.to_owned())); } - dbg!(iban.country_code(), iban.bban()); - Self::check_bban(iban.bban())?; Ok(Self(iban)) } } -/// Parse a magnet payto URI, panic if malformed -pub fn magnet_payto(url: impl AsRef<str>) -> MagnetPayto { - let payto: Payto = url.as_ref().parse().expect("invalid payto"); - (&payto).try_into().expect("invalid magnet payto") -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MagnetPayto { - pub iban: HuIban, - pub name: String, -} - -impl MagnetPayto { - pub fn as_payto(&self) -> Payto { - Payto::from_parts("iban", format_args!("/{}", self.iban.0)).full(&self.name) - } - - pub fn iban(&self) -> &str { - self.iban.0.as_ref() +impl PaytoImpl for HuIban { + fn as_payto(&self) -> PaytoURI { + PaytoURI::from_parts("iban", format_args!("/{}", self.0)) } - pub fn bban(&self) -> &str { - self.iban.bban() + fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> { + let iban_payto = IbanPayto::try_from(raw).map_err(PaytoErr::custom)?; + HuIban::try_from(iban_payto.into_inner().iban).map_err(PaytoErr::custom) } } -impl std::fmt::Display for MagnetPayto { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.as_payto().fmt(f) +impl FromStr for HuIban { + type Err = HuIbanErr; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let iban: IBAN = s.parse()?; + Self::try_from(iban) } } -impl TryFrom<&Payto> for MagnetPayto { - type Error = PaytoErr; - - fn try_from(value: &Payto) -> Result<Self, Self::Error> { - let iban_payto = IbanPayto::try_from(value).map_err(PaytoErr::custom)?; - let hu_iban = HuIban::try_from(iban_payto.iban).map_err(PaytoErr::custom)?; - let full: FullQuery = value.query()?; - Ok(Self { - iban: hu_iban, - name: full.receiver_name, - }) - } +/// Parse a magnet payto URI, panic if malformed +pub fn magnet_payto(url: impl AsRef<str>) -> MagnetPayto { + url.as_ref().parse().expect("invalid magnet payto") } +pub type MagnetPayto = FullPayto<HuIban>; +pub type HuPayto = Payto<HuIban>; + #[cfg(test)] mod test { - use taler_common::types::payto::{payto, IbanPayto}; + use taler_common::types::{ + iban::IBAN, + payto::{payto, Payto, PaytoImpl}, + }; use crate::HuIban; @@ -198,16 +193,12 @@ mod test { ), ] { // Parsing - let iban_payto: IbanPayto = (&valid).try_into().unwrap(); - let hu_payto: HuIban = iban_payto.iban.try_into().unwrap(); + let iban_payto: Payto<IBAN> = (&valid).try_into().unwrap(); + let hu_payto: HuIban = iban_payto.into_inner().try_into().unwrap(); assert_eq!(hu_payto.bban(), account); // Roundtrip let iban = HuIban::from_bban(&account).unwrap(); - let payto = IbanPayto { - iban: iban.0, - bic: None, - } - .as_payto(); + let payto = iban.as_payto(); assert_eq!(payto, valid); } } diff --git a/taler-magnet-bank/src/main.rs b/taler-magnet-bank/src/main.rs @@ -23,7 +23,7 @@ use taler_common::{ cli::ConfigCmd, config::{parser::ConfigSource, Config}, taler_main, - types::payto::{payto, Payto}, + types::payto::{payto, PaytoURI}, CommonArgs, }; use taler_magnet_bank::{ @@ -34,7 +34,7 @@ use taler_magnet_bank::{ keys, magnet::AuthClient, worker::Worker, - MagnetPayto, + HuPayto, }; pub fn long_version() -> &'static str { @@ -78,7 +78,7 @@ enum Command { #[clap(long, short)] transient: bool, // TODO account in config - account: Payto, + account: PaytoURI, }, /// Run taler-magnet-bank HTTP server Serve { @@ -143,7 +143,7 @@ async fn app(args: Args, cfg: Config) -> anyhow::Result<()> { let client = reqwest::Client::new(); let client = AuthClient::new(&client, &cfg.api_url, &cfg.consumer).upgrade(&keys.access_token); - let account = MagnetPayto::try_from(&account)?; + let account = HuPayto::try_from(&account)?; let account = client.account(account.bban()).await?; let mut db = pool.acquire().await?.detach(); // TODO run in loop and handle errors diff --git a/taler-magnet-bank/src/worker.rs b/taler-magnet-bank/src/worker.rs @@ -226,10 +226,7 @@ pub fn extract_tx_info(tx: Transaction) -> Tx { } else { HuIban::from_bban(&tx.counter_account).unwrap() }; - let counter_account = MagnetPayto { - iban, - name: tx.counter_name, - }; + let counter_account = MagnetPayto::new(iban, tx.counter_name); if tx.amount.is_sign_positive() { Tx::In(TxIn { code: tx.code,