commit 9ca55a895159377e8a0f9f1069599a5bc5c0c019
parent 1508d487199536c1af70f8e19c086b811e05a534
Author: Antoine A <>
Date: Tue, 4 Feb 2025 14:00:06 +0100
common: better payto abstraction
Diffstat:
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,