[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-rust] branch master updated (1f36e0e -> 9ca55a8)
From: |
Admin |
Subject: |
[taler-rust] branch master updated (1f36e0e -> 9ca55a8) |
Date: |
Tue, 04 Feb 2025 14:00:33 +0100 |
This is an automated email from the git hooks/post-receive script.
antoine pushed a change to branch master
in repository taler-rust.
from 1f36e0e common: IBAN & BIC parser
new 1508d48 magnet-bank: use iban payto with hungarian BBAN checks
new 9ca55a8 common: better payto abstraction
The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
Cargo.lock | 46 +++----
common/taler-api/src/db.rs | 25 +++-
common/taler-api/tests/common/db.rs | 4 +-
common/taler-common/src/api_revenue.rs | 6 +-
common/taler-common/src/api_wire.rs | 26 ++--
common/taler-common/src/config.rs | 4 +-
common/taler-common/src/types.rs | 2 +-
common/taler-common/src/types/iban.rs | 139 +++++++++++++-------
common/taler-common/src/types/payto.rs | 195 ++++++++++++++++++++++++----
common/taler-common/src/types/timestamp.rs | 11 ++
common/taler-common/src/types/utils.rs | 22 ++++
common/taler-test-utils/src/routine.rs | 10 +-
taler-magnet-bank/src/adapter.rs | 6 +-
taler-magnet-bank/src/db.rs | 93 +++++---------
taler-magnet-bank/src/dev.rs | 37 ++----
taler-magnet-bank/src/lib.rs | 199 +++++++++++++++++++++++------
taler-magnet-bank/src/magnet.rs | 22 ++--
taler-magnet-bank/src/main.rs | 18 ++-
taler-magnet-bank/src/worker.rs | 37 +++---
taler-magnet-bank/tests/api.rs | 23 ++--
20 files changed, 624 insertions(+), 301 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 3f37124..8d666b9 100644
--- 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"
@@ -304,9 +304,9 @@ checksum =
"37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
-version = "1.2.10"
+version = "1.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229"
+checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf"
dependencies = [
"shlex",
]
@@ -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
index b5308d0..1a06e15 100644
--- 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,
@@ -27,7 +27,8 @@ use taler_common::{
types::{
amount::{Amount, Decimal},
base32::Base32,
- payto::Payto,
+ iban::IBAN,
+ payto::PaytoURI,
timestamp::Timestamp,
},
};
@@ -152,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)
}
@@ -171,10 +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_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>;
@@ -200,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
index e878cdd..36fd649 100644
--- 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
index 77e0a6f..2b620cf 100644
--- 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
index 76b7f2a..f424149 100644
--- 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
index 7bbd587..d4e66e5 100644
--- 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.rs b/common/taler-common/src/types.rs
index b17056a..fd1d769 100644
--- a/common/taler-common/src/types.rs
+++ b/common/taler-common/src/types.rs
@@ -19,7 +19,7 @@ pub mod base32;
pub mod iban;
pub mod payto;
pub mod timestamp;
-mod utils;
+pub mod utils;
use url::Url;
diff --git a/common/taler-common/src/types/iban.rs
b/common/taler-common/src/types/iban.rs
index cfdfa15..53d2dc8 100644
--- a/common/taler-common/src/types/iban.rs
+++ b/common/taler-common/src/types/iban.rs
@@ -16,6 +16,7 @@
use std::{
fmt::{Debug, Display},
+ ops::Deref,
str::FromStr,
};
@@ -24,15 +25,26 @@ use super::utils::InlineStr;
const MAX_IBAN_SIZE: usize = 34;
const MAX_BIC_SIZE: usize = 11;
-#[derive(Clone, PartialEq, Eq, serde_with::DeserializeFromStr,
serde_with::SerializeDisplay)]
+/// Parse an IBAN, panic if malformed
+pub fn iban(iban: impl AsRef<str>) -> IBAN {
+ iban.as_ref().parse().expect("invalid IBAN")
+}
+
+/// Parse an BIC, panic if malformed
+pub fn bic(bic: impl AsRef<str>) -> BIC {
+ bic.as_ref().parse().expect("invalid BIC")
+}
+
+#[derive(
+ Debug, Clone, PartialEq, Eq, serde_with::DeserializeFromStr,
serde_with::SerializeDisplay,
+)]
/// International Bank Account Number (IBAN)
pub struct IBAN(InlineStr<MAX_IBAN_SIZE>);
impl IBAN {
/// Compute IBAN checksum
- fn iban_checksum(s: &str) -> u32 {
- s.as_bytes()
- .iter()
+ fn iban_checksum(s: &[u8]) -> u8 {
+ (s.iter()
.cycle()
.skip(4)
.take(s.len())
@@ -47,7 +59,47 @@ impl IBAN {
}
checksum
})
- % 97
+ % 97) as u8
+ }
+
+ pub fn from_parts(country: &str, bban: &str) -> Self {
+ assert_eq!(country.len(), 2);
+ // Create a iban with an empty digit check
+ let mut iban = InlineStr::from_iter(
+ country
+ .as_bytes()
+ .iter()
+ .copied()
+ .chain("00".as_bytes().iter().copied())
+ .chain(bban.as_bytes().iter().copied()),
+ )
+ .unwrap();
+ // Compute check digit
+ let check_digit = 98 - Self::iban_checksum(iban.deref());
+
+ // And insert it
+ unsafe {
+ // SAFETY: we only insert ASCII digits
+ let buf = iban.deref_mut();
+ buf[3] = check_digit % 10 + b'0';
+ buf[2] = check_digit / 10 + b'0';
+ }
+ Self(iban)
+ }
+
+ pub fn country_code(&self) -> &str {
+ // SAFETY len >= 4
+ unsafe { self.as_ref().get_unchecked(0..2) }
+ }
+
+ pub fn check_digit(&self) -> &str {
+ // SAFETY len >= 4
+ unsafe { self.as_ref().get_unchecked(2..4) }
+ }
+
+ pub fn bban(&self) -> &str {
+ // SAFETY len >= 5
+ unsafe { self.as_ref().get_unchecked(4..) }
}
}
@@ -61,14 +113,14 @@ impl AsRef<str> for IBAN {
pub enum IbanErrorKind {
#[error("contains illegal characters (only 0-9A-Z allowed)")]
Invalid,
- #[error("contains invalid contry code")]
+ #[error("contains invalid country code")]
CountryCode,
#[error("contains invalid check digit")]
CheckDigit,
- #[error("too long (max {MAX_IBAN_SIZE} chars)")]
- Big,
+ #[error("too long expected max {MAX_IBAN_SIZE} chars got {0}")]
+ Big(usize),
#[error("checksum expected 1 got {0}")]
- Checksum(u32),
+ Checksum(u8),
}
#[derive(Debug, thiserror::Error)]
@@ -82,35 +134,32 @@ impl FromStr for IBAN {
type Err = ParseIbanError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
- (|| {
- let bytes: &[u8] = s.as_bytes();
- if !bytes
+ let bytes: &[u8] = s.as_bytes();
+ if !bytes
+ .iter()
+ .all(|b| b.is_ascii_whitespace() || b.is_ascii_alphanumeric())
+ {
+ Err(IbanErrorKind::Invalid)
+ } else if let Some(inlined) = InlineStr::from_iter(
+ bytes
.iter()
- .all(|b| b.is_ascii_whitespace() || b.is_ascii_alphanumeric())
- {
- return Err(IbanErrorKind::Invalid);
- }
- let Some(inlined) = InlineStr::from_iter(
- bytes
- .iter()
- .filter_map(|b|
(!b.is_ascii_whitespace()).then_some(b.to_ascii_uppercase())),
- ) else {
- return Err(IbanErrorKind::Big);
- };
- let str = inlined.as_ref();
- let bytes = str.as_bytes();
- if bytes.len() < 2 ||
!bytes[0..2].iter().all(u8::is_ascii_uppercase) {
- return Err(IbanErrorKind::CountryCode);
- } else if bytes.len() < 4 ||
!bytes[2..4].iter().all(u8::is_ascii_digit) {
- return Err(IbanErrorKind::CheckDigit);
- }
- let checksum = Self::iban_checksum(str);
- if checksum != 1 {
- Err(IbanErrorKind::Checksum(checksum))
+ .filter_map(|b|
(!b.is_ascii_whitespace()).then_some(b.to_ascii_uppercase())),
+ ) {
+ if inlined.len() < 2 ||
!inlined[0..2].iter().all(u8::is_ascii_uppercase) {
+ Err(IbanErrorKind::CountryCode)
+ } else if inlined.len() < 4 ||
!inlined[2..4].iter().all(u8::is_ascii_digit) {
+ Err(IbanErrorKind::CheckDigit)
} else {
- Ok(Self(inlined))
+ let checksum = Self::iban_checksum(&inlined);
+ if checksum != 1 {
+ Err(IbanErrorKind::Checksum(checksum))
+ } else {
+ Ok(Self(inlined))
+ }
}
- })()
+ } else {
+ Err(IbanErrorKind::Big(bytes.len()))
+ }
.map_err(|kind| ParseIbanError {
iban: s.to_owned(),
kind,
@@ -118,12 +167,6 @@ impl FromStr for IBAN {
}
}
-impl Debug for IBAN {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- Debug::fmt(&self.as_ref(), f)
- }
-}
-
impl Display for IBAN {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.as_ref(), f)
@@ -131,7 +174,9 @@ impl Display for IBAN {
}
/// Bank Identifier Code (BIC)
-#[derive(Clone, PartialEq, Eq, serde_with::DeserializeFromStr,
serde_with::SerializeDisplay)]
+#[derive(
+ Debug, Clone, PartialEq, Eq, serde_with::DeserializeFromStr,
serde_with::SerializeDisplay,
+)]
pub struct BIC(InlineStr<MAX_BIC_SIZE>);
impl BIC {
@@ -209,12 +254,6 @@ impl FromStr for BIC {
}
}
-impl Debug for BIC {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- Debug::fmt(&self.as_ref(), f)
- }
-}
-
impl Display for BIC {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.as_ref(), f)
@@ -235,8 +274,12 @@ fn parse_iban() {
"PL61109010140000071219812874", // Poland
"NO9386011117947", // Norway
] {
+ // Parsing
let iban = IBAN::from_str(&valid).unwrap();
assert_eq!(iban.to_string(), valid);
+ // Roundtrip
+ let from_parts = IBAN::from_parts(iban.country_code(), iban.bban());
+ assert_eq!(from_parts.to_string(), valid);
}
for (invalid, err) in [
diff --git a/common/taler-common/src/types/payto.rs
b/common/taler-common/src/types/payto.rs
index 20f2d7b..8b9584b 100644
--- a/common/taler-common/src/types/payto.rs
+++ b/common/taler-common/src/types/payto.rs
@@ -15,49 +15,71 @@
*/
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 query<Q: DeserializeOwned>(&self) -> Result<Q, PaytoErr> {
+ pub fn from_parts(domain: &str, path: impl Display) -> Self {
+ payto(format!("payto://{domain}{path}"))
+ }
+
+ 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)
}
- pub fn from_parts(domain_path: impl Display, query: impl Serialize) ->
Self {
- let query = serde_urlencoded::to_string(query).unwrap();
- payto(format!("payto://{domain_path}?{query}"))
+ fn with_query(mut self, query: impl Serialize) -> Self {
+ let mut urlencoder = self.0.query_pairs_mut();
+ query
+ .serialize(serde_urlencoded::Serializer::new(&mut urlencoder))
+ .unwrap();
+ let _ = urlencoder.finish();
+ drop(urlencoder);
+ self
}
}
-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)
}
@@ -85,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> {
@@ -99,14 +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>,
}
+const IBAN: &str = "iban";
+
#[derive(Debug, thiserror::Error)]
-pub enum IbanPaytoErr {
+pub enum IbanBicErr {
#[error("missing IBAN in path")]
MissingIban,
#[error(transparent)]
@@ -115,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,
@@ -129,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) => (
@@ -146,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
index f773b22..d2e95f3 100644
--- 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-common/src/types/utils.rs
b/common/taler-common/src/types/utils.rs
index 6dc34af..282257d 100644
--- a/common/taler-common/src/types/utils.rs
+++ b/common/taler-common/src/types/utils.rs
@@ -14,6 +14,8 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+use std::{fmt::Debug, ops::Deref};
+
#[derive(Clone, PartialEq, Eq)]
pub struct InlineStr<const LEN: usize> {
/// Len of ascii string in buf
@@ -53,6 +55,11 @@ impl<const LEN: usize> InlineStr<LEN> {
buf,
})
}
+
+ pub unsafe fn deref_mut(&mut self) -> &mut [u8] {
+ // SAFETY: len <= LEN
+ unsafe { self.buf.get_unchecked_mut(..self.len as usize) }
+ }
}
impl<const LEN: usize> AsRef<str> for InlineStr<LEN> {
@@ -62,3 +69,18 @@ impl<const LEN: usize> AsRef<str> for InlineStr<LEN> {
unsafe {
std::str::from_utf8_unchecked(self.buf.get_unchecked(..self.len as usize)) }
}
}
+
+impl<const LEN: usize> Debug for InlineStr<LEN> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ Debug::fmt(&self.as_ref(), f)
+ }
+}
+
+impl<const LEN: usize> Deref for InlineStr<LEN> {
+ type Target = [u8];
+
+ fn deref(&self) -> &Self::Target {
+ // SAFETY: len <= LEN
+ unsafe { self.buf.get_unchecked(..self.len as usize) }
+ }
+}
diff --git a/common/taler-test-utils/src/routine.rs
b/common/taler-test-utils/src/routine.rs
index 4bc5e6b..51cccc3 100644
--- 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
index 98f423a..5931034 100644
--- 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
index bc0c5b0..009761d 100644
--- a/taler-magnet-bank/src/db.rs
+++ b/taler-magnet-bank/src/db.rs
@@ -28,7 +28,7 @@ use taler_common::{
IncomingBankTransaction, OutgoingBankTransaction, TransferListStatus,
TransferRequest,
TransferState, TransferStatus,
},
- types::{amount::Amount, timestamp::Timestamp},
+ types::{amount::Amount, payto::PaytoImpl as _, timestamp::Timestamp},
};
use tokio::sync::watch::{Receiver, Sender};
@@ -165,7 +165,7 @@ pub async fn register_tx_in_admin(db: &PgPool, tx:
&TxInAdmin) -> sqlx::Result<A
)
.bind_amount(&tx.amount)
.bind(&tx.subject)
- .bind(&tx.debtor.number)
+ .bind(tx.debtor.iban())
.bind(&tx.debtor.name)
.bind_timestamp(&tx.timestamp)
.bind(tx.metadata.ty())
@@ -199,7 +199,7 @@ pub async fn register_tx_in(
.bind(tx.code as i64)
.bind_amount(&tx.amount)
.bind(&tx.subject)
- .bind(&tx.debtor.number)
+ .bind(tx.debtor.iban())
.bind(&tx.debtor.name)
.bind_timestamp(&tx.timestamp)
.bind(subject.as_ref().map(|it| it.ty()))
@@ -233,7 +233,7 @@ pub async fn register_tx_out(
.bind(tx.code as i64)
.bind_amount(&tx.amount)
.bind(&tx.subject)
- .bind(&tx.creditor.number)
+ .bind(tx.creditor.iban())
.bind(&tx.creditor.name)
.bind_timestamp(&tx.timestamp)
.bind(subject.as_ref().map(|it| it.0.as_ref()))
@@ -274,7 +274,7 @@ pub async fn make_transfer<'a>(
.bind(&subject)
.bind_amount(&req.amount)
.bind(req.exchange_base_url.as_str())
- .bind(&creditor.number)
+ .bind(creditor.iban())
.bind(&creditor.name)
.bind_timestamp(timestamp)
.try_map(|r: PgRow| {
@@ -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: MagnetPayto {
- number: r.try_get(4)?,
- name: r.try_get(4)?,
- }
- .as_payto(),
+ 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: MagnetPayto {
- number: r.try_get(3)?,
- name: r.try_get(4)?,
- }
- .as_payto(),
+ 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: MagnetPayto {
- number: r.try_get(4)?,
- name: r.try_get(5)?,
- }
- .as_payto(),
+ 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: MagnetPayto {
- number: r.try_get(4)?,
- name: r.try_get(5)?,
- }
- .as_payto(),
+ 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: MagnetPayto {
- number: r.try_get(4)?,
- name: r.try_get(5)?,
- }
- .as_payto(),
+ 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: MagnetPayto {
- number: r.try_get(6)?,
- name: r.try_get(7)?,
- }
- .as_payto(),
+ credit_account: r.try_get_iban(6)?.as_full_payto(r.try_get(7)?),
timestamp: r.try_get_timestamp(8)?,
})
})
@@ -550,10 +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 {
- number: r.try_get(4)?,
- name: r.try_get(5)?,
- },
+ creditor: MagnetPayto::new(r.try_get_parse(4)?, r.try_get(5)?),
})
})
.fetch_all(db)
@@ -624,7 +597,7 @@ mod test {
self, make_transfer, register_tx_in, register_tx_in_admin,
register_tx_out,
AddIncomingResult, RegisteredTx, TransferResult, TxIn, TxOut,
},
- MagnetPayto,
+ magnet_payto,
};
use super::TxInAdmin;
@@ -659,10 +632,9 @@ mod test {
code: code,
amount: amount("EUR:10"),
subject: "subject".to_owned(),
- debtor: MagnetPayto {
- number: "number".to_owned(),
- name: "name".to_owned(),
- },
+ debtor: magnet_payto(
+
"payto://iban/HU30162000031000163100000000?receiver-name=name",
+ ),
timestamp: Timestamp::now_stable(),
};
// Insert
@@ -779,10 +751,7 @@ mod test {
let tx = TxInAdmin {
amount: amount("EUR:10"),
subject: "subject".to_owned(),
- debtor: MagnetPayto {
- number: "number".to_owned(),
- name: "name".to_owned(),
- },
+ debtor:
magnet_payto("payto://iban/HU30162000031000163100000000?receiver-name=name"),
timestamp: Timestamp::now_stable(),
metadata: IncomingSubject::Reserve(EddsaPublicKey::rand()),
};
@@ -862,10 +831,9 @@ mod test {
code,
amount: amount("EUR:10"),
subject: "subject".to_owned(),
- creditor: MagnetPayto {
- number: "number".to_owned(),
- name: "name".to_owned(),
- },
+ creditor: magnet_payto(
+
"payto://iban/HU30162000031000163100000000?receiver-name=name",
+ ),
timestamp: Timestamp::now_stable(),
};
// Insert
@@ -970,12 +938,9 @@ mod test {
amount: amount("EUR:10"),
exchange_base_url: url("https://exchange.test.com/";),
wtid: ShortHashCode::rand(),
- credit_account: payto("payto://magnet-bank/todo"),
- };
- let payto = MagnetPayto {
- number: "number".to_owned(),
- name: "name".to_owned(),
+ credit_account:
payto("payto://iban/HU02162000031000164800000000?receiver-name=name"),
};
+ let payto =
magnet_payto("payto://iban/HU30162000031000163100000000?receiver-name=name");
let timestamp = Timestamp::now_stable();
// Insert
assert_eq!(
@@ -1077,10 +1042,8 @@ mod test {
async fn batch() {
let (mut db, _) = setup().await;
let start = Timestamp::now();
- let magnet_payto = MagnetPayto {
- number: "number".to_owned(),
- name: "name".to_owned(),
- };
+ let magnet_payto =
+
magnet_payto("payto://iban/HU30162000031000163100000000?receiver-name=name");
// Empty db
let pendings = db::pending_batch(&mut db, &start)
@@ -1097,7 +1060,9 @@ mod test {
amount: amount(format!("{CURRENCY}:{}", i + 1)),
exchange_base_url: url("https://exchange.test.com/";),
wtid: ShortHashCode::rand(),
- credit_account: payto("payto://magnet-bank/todo"),
+ credit_account: payto(
+
"payto://iban/HU02162000031000164800000000?receiver-name=name",
+ ),
},
&magnet_payto,
&&Timestamp::now(),
@@ -1119,7 +1084,9 @@ mod test {
amount: amount(format!("{CURRENCY}:{}", i + 1)),
exchange_base_url: url("https://exchange.test.com/";),
wtid: ShortHashCode::rand(),
- credit_account: payto("payto://magnet-bank/todo"),
+ credit_account: payto(
+
"payto://iban/HU02162000031000164800000000?receiver-name=name",
+ ),
},
&magnet_payto,
&Timestamp::now(),
diff --git a/taler-magnet-bank/src/dev.rs b/taler-magnet-bank/src/dev.rs
index b9c3e00..69e14b0 100644
--- a/taler-magnet-bank/src/dev.rs
+++ b/taler-magnet-bank/src/dev.rs
@@ -18,10 +18,7 @@ use clap::ValueEnum;
use jiff::Zoned;
use taler_common::{
config::Config,
- types::{
- amount::Amount,
- payto::{FullQuery, Payto},
- },
+ types::{amount::Amount, iban::IBAN, payto::PaytoImpl},
};
use tracing::info;
@@ -30,7 +27,7 @@ use crate::{
keys,
magnet::{AuthClient, Direction},
worker::{extract_tx_info, Tx},
- MagnetPayto,
+ HuPayto, MagnetPayto,
};
#[derive(Debug, Clone, PartialEq, Eq, ValueEnum)]
@@ -47,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)]
@@ -73,16 +70,13 @@ pub async fn dev(cfg: Config, cmd: DevCmd) ->
anyhow::Result<()> {
let res = client.list_accounts().await?;
for partner in res.partners {
for account in partner.bank_accounts {
- let payto = MagnetPayto {
- number: account.number,
- name: partner.partner.name.clone(),
- };
- info!("{} {} {payto}", account.code,
account.currency.symbol);
+ let iban: IBAN = account.iban.parse()?;
+ 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,
@@ -91,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.number, &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);
@@ -113,10 +105,7 @@ 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.number).await?;
+ let debtor = client.account(debtor.bban()).await?;
let now = Zoned::now();
let date = now.date();
@@ -126,8 +115,8 @@ pub async fn dev(cfg: Config, cmd: DevCmd) ->
anyhow::Result<()> {
amount.val as f64,
&subject,
&date,
- &full.receiver_name,
- &creditor.number,
+ &creditor.name,
+ creditor.bban(),
)
.await?
.tx;
@@ -138,7 +127,7 @@ pub async fn dev(cfg: Config, cmd: DevCmd) ->
anyhow::Result<()> {
init.code,
init.amount,
&date,
- &creditor.number,
+ creditor.bban(),
)
.await?;
}
diff --git a/taler-magnet-bank/src/lib.rs b/taler-magnet-bank/src/lib.rs
index e2c43ee..3ea407b 100644
--- a/taler-magnet-bank/src/lib.rs
+++ b/taler-magnet-bank/src/lib.rs
@@ -14,7 +14,12 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-use taler_common::types::payto::{FullQuery, Payto, PaytoErr};
+use std::{borrow::Cow, str::FromStr};
+
+use taler_common::types::{
+ iban::{IbanErrorKind, ParseIbanError, IBAN},
+ payto::{FullPayto, IbanPayto, Payto, PaytoErr, PaytoImpl, PaytoURI},
+};
pub mod adapter;
pub mod config;
@@ -30,59 +35,171 @@ pub mod failure_injection {
}
}
+pub const MAX_MAGNET_BBAN_SIZE: usize = 24;
+
#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct MagnetPayto {
- pub number: String,
- pub name: String,
-}
+pub struct HuIban(IBAN);
-impl MagnetPayto {
- pub fn as_payto(&self) -> Payto {
- Payto::from_parts(
- format_args!("{MAGNET_BANK}/{}", self.number),
- [("receiver-name", &self.name)],
- )
+impl HuIban {
+ pub fn checksum(b: &[u8]) -> Result<(), (u8, u8)> {
+ let expected_digit = b[7] - b'0';
+ let sum = ((b[0] - b'0') * 9) as u16
+ + ((b[1] - b'0') * 7) as u16
+ + ((b[2] - b'0') * 3) as u16
+ + ((b[3] - b'0') * 1) as u16
+ + ((b[4] - b'0') * 9) as u16
+ + ((b[5] - b'0') * 7) as u16
+ + ((b[6] - b'0') * 3) as u16;
+ let modulo = ((10 - (sum % 10)) % 10) as u8;
+ if expected_digit != modulo {
+ Err((expected_digit, modulo))
+ } else {
+ Ok(())
+ }
+ }
+
+ fn check_bban(bban: &str) -> Result<(), HuIbanErr> {
+ let bban = bban.strip_suffix("00000000").unwrap_or(bban).as_bytes();
+ if bban.len() != 16 && bban.len() != 24 {
+ return Err(HuIbanErr::BbanSize(bban.len()));
+ } else if !bban.iter().all(u8::is_ascii_digit) {
+ return Err(HuIbanErr::Invalid);
+ }
+ Self::checksum(&bban[..8]).map_err(|e|
HuIbanErr::checksum("bank-branch number", e))?;
+ if bban.len() == 16 {
+ Self::checksum(&bban[8..]).map_err(|e|
HuIbanErr::checksum("account number", e))?;
+ } else {
+ Self::checksum(&bban[8..16])
+ .map_err(|e| HuIbanErr::checksum("account number first group",
e))?;
+ Self::checksum(&bban[16..])
+ .map_err(|e| HuIbanErr::checksum("account number second
group", e))?;
+ }
+ Ok(())
+ }
+
+ pub fn from_bban(bban: &str) -> Result<Self, HuIbanErr> {
+ Self::check_bban(bban)?;
+ let full_bban = if bban.len() == 16 {
+ Cow::Owned(format!("{bban}00000000"))
+ } else {
+ Cow::Borrowed(bban)
+ };
+ let iban = IBAN::from_parts("HU", &full_bban);
+ Ok(Self(iban))
}
-}
-impl std::fmt::Display for MagnetPayto {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- self.as_payto().fmt(f)
+ pub fn bban(&self) -> &str {
+ 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)]
-pub enum MagnetPaytoErr {
- #[error("missing Magnet Bank account number in path")]
- MissingAccount,
+pub enum HuIbanErr {
+ #[error("contains illegal characters (only 0-9 allowed)")]
+ Invalid,
+ #[error("expected an hungarian IBAN starting with HU got {0}")]
+ CountryCode(String),
+ #[error("invalid length expected 16 or 24 chars got {0}")]
+ 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)
+ }
}
-const MAGNET_BANK: &str = "magnet-bank";
+impl HuIbanErr {
+ fn checksum(part: &'static str, (expected, checksum): (u8, u8)) -> Self {
+ Self::Checksum(part, expected, checksum)
+ }
+}
-impl TryFrom<&Payto> for MagnetPayto {
- type Error = PaytoErr;
+impl TryFrom<IBAN> for HuIban {
+ type Error = HuIbanErr;
- fn try_from(value: &Payto) -> Result<Self, Self::Error> {
- let url = value.as_ref();
- if url.domain() != Some(MAGNET_BANK) {
- return Err(PaytoErr::UnsupportedKind(
- MAGNET_BANK,
- url.domain().unwrap_or_default().to_owned(),
- ));
+ fn try_from(iban: IBAN) -> Result<Self, Self::Error> {
+ let country_code = iban.country_code();
+ if country_code != "HU" {
+ return Err(HuIbanErr::CountryCode(country_code.to_owned()));
}
- let Some(mut segments) = url.path_segments() else {
- return Err(PaytoErr::custom(MagnetPaytoErr::MissingAccount));
- };
- let Some(account) = segments.next() else {
- return Err(PaytoErr::custom(MagnetPaytoErr::MissingAccount));
- };
- if segments.next().is_some() {
- return Err(PaytoErr::TooLong(MAGNET_BANK));
+
+ Self::check_bban(iban.bban())?;
+
+ Ok(Self(iban))
+ }
+}
+
+impl PaytoImpl for HuIban {
+ fn as_payto(&self) -> PaytoURI {
+ PaytoURI::from_parts("iban", format_args!("/{}", self.0))
+ }
+
+ 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 FromStr for HuIban {
+ type Err = HuIbanErr;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let iban: IBAN = s.parse()?;
+ Self::try_from(iban)
+ }
+}
+
+/// 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::{
+ iban::IBAN,
+ payto::{payto, Payto, PaytoImpl},
+ };
+
+ use crate::HuIban;
+
+ #[test]
+ fn hu_iban() {
+ for (valid, account) in [
+ (
+ payto("payto://iban/HU30162000031000163100000000"),
+ "1620000310001631",
+ ),
+ (
+ payto("payto://iban/HU02162000031000164800000000"),
+ "1620000310001648",
+ ),
+ (
+ payto("payto://iban/HU60162000101006446300000000"),
+ "1620001010064463",
+ ),
+ ] {
+ // Parsing
+ 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 = iban.as_payto();
+ assert_eq!(payto, valid);
}
- let full: FullQuery = value.query()?;
- Ok(Self {
- number: account.to_owned(),
- name: full.receiver_name,
- })
}
}
diff --git a/taler-magnet-bank/src/magnet.rs b/taler-magnet-bank/src/magnet.rs
index 7edaca0..432ae5d 100644
--- a/taler-magnet-bank/src/magnet.rs
+++ b/taler-magnet-bank/src/magnet.rs
@@ -394,10 +394,10 @@ impl ApiClient<'_> {
.await
}
- pub async fn account(&self, account: &str) -> ApiResult<Account> {
+ pub async fn account(&self, bban: &str) -> ApiResult<Account> {
Ok(self
.client
-
.get(self.join(&format!("/RESTApi/resources/v2/bankszamla/{account}")))
+
.get(self.join(&format!("/RESTApi/resources/v2/bankszamla/{bban}")))
.oauth(self.consumer, Some(self.access), None)
.await
.magnet_json::<AccountWrapper>()
@@ -409,12 +409,12 @@ impl ApiClient<'_> {
&self,
direction: Direction,
limit: u16,
- account: &str,
+ bban: &str,
next: &Option<Next>,
status: &Option<TxStatus>,
) -> ApiResult<TransactionPage> {
let mut req = self.client.get(self.join(&format!(
- "/RESTApi/resources/v2/tranzakcio/paginator/{account}/{limit}"
+ "/RESTApi/resources/v2/tranzakcio/paginator/{bban}/{limit}"
)));
if let Some(next) = next {
req = req
@@ -443,7 +443,7 @@ impl ApiClient<'_> {
subject: &str,
date: &jiff::civil::Date,
creditor_name: &str,
- creditor_account: &str,
+ creditor_bban: &str,
) -> ApiResult<TxInfo> {
#[derive(Serialize)]
struct Req<'a> {
@@ -470,7 +470,7 @@ impl ApiClient<'_> {
subject,
date,
creditor_name,
- creditor_account,
+ creditor_account: creditor_bban,
})
.oauth(self.consumer, Some(self.access), None)
.await
@@ -482,11 +482,11 @@ impl ApiClient<'_> {
pub async fn sign_tx(
&self,
signing_key: &SigningKey,
- account: &str,
+ bban: &str,
tx_code: u64,
amount: f64,
date: &jiff::civil::Date,
- creditor: &str,
+ creditor_bban: &str,
) -> ApiResult<TxInfo> {
#[derive(Serialize)]
struct Req<'a> {
@@ -503,7 +503,7 @@ impl ApiClient<'_> {
signature: &'a str,
}
- let content: String =
format!("{tx_code};{account};{creditor};{amount};{date};");
+ let content: String =
format!("{tx_code};{bban};{creditor_bban};{amount};{date};");
let signature: DerSignature = signing_key.sign(content.as_bytes());
let encoded = BASE64_STANDARD.encode(signature.as_bytes());
Ok(self
@@ -511,8 +511,8 @@ impl ApiClient<'_> {
.put(self.join("/RESTApi/resources/v2/tranzakcio/alairas"))
.json(&Req {
tx_code,
- debtor: account,
- creditor,
+ debtor: bban,
+ creditor: creditor_bban,
amount,
date,
signature: &encoded,
diff --git a/taler-magnet-bank/src/main.rs b/taler-magnet-bank/src/main.rs
index 14f7f62..e5e23c5 100644
--- 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 {
@@ -115,7 +115,13 @@ async fn app(args: Args, cfg: Config) ->
anyhow::Result<()> {
let db = DbCfg::parse(&cfg)?;
let pool = PgPool::connect_with(db.cfg).await?;
let cfg = ServeCfg::parse(&cfg)?;
- let api = Arc::new(MagnetApi::start(pool,
payto("payto://magnet-bank/todo")).await);
+ let api = Arc::new(
+ MagnetApi::start(
+ pool,
+
payto("payto://iban/HU02162000031000164800000000?receiver-name=name"),
+ )
+ .await,
+ );
let mut builder = TalerApiBuilder::new();
if let Some(cfg) = cfg.wire_gateway {
builder = builder.wire_gateway(api.clone(), cfg.auth);
@@ -137,8 +143,8 @@ 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 = client.account(&account.number).await?;
+ 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
let mut worker = Worker {
diff --git a/taler-magnet-bank/src/worker.rs b/taler-magnet-bank/src/worker.rs
index e6592a4..5cfbdee 100644
--- a/taler-magnet-bank/src/worker.rs
+++ b/taler-magnet-bank/src/worker.rs
@@ -20,6 +20,7 @@ use sqlx::PgConnection;
use taler_api::subject::{self, parse_incoming_unstructured};
use taler_common::types::{
amount::{self},
+ iban::IBAN,
timestamp::Timestamp,
};
use tracing::{debug, info};
@@ -28,7 +29,7 @@ use crate::{
db::{self, AddIncomingResult, Initiated, TxIn, TxOut},
failure_injection::fail_point,
magnet::{error::ApiError, ApiClient, Direction, Transaction},
- MagnetPayto,
+ HuIban, MagnetPayto,
};
#[derive(Debug, thiserror::Error)]
@@ -168,7 +169,7 @@ impl Worker<'_> {
&tx.subject,
&date,
&tx.creditor.name,
- &tx.creditor.number,
+ tx.creditor.bban(),
)
.await?
.tx;
@@ -179,7 +180,7 @@ impl Worker<'_> {
db::initiated_submit_success(&mut *self.db, tx.id, &Timestamp::now(),
info.code).await?;
// Sign transaction
- self.sign_tx(info.code, info.amount, &date, &tx.creditor.number)
+ self.sign_tx(info.code, info.amount, &date, tx.creditor.bban())
.await?;
Ok(())
}
@@ -218,31 +219,29 @@ pub enum Tx {
pub fn extract_tx_info(tx: Transaction) -> Tx {
// TODO amount from f64 without allocations
let amount = amount::amount(format!("{}:{}", tx.currency,
tx.amount.abs()));
+ // TODO we should support non hungarian account and error handling
+ let iban = if tx.counter_account.starts_with("HU") {
+ let iban: IBAN = tx.counter_account.parse().unwrap();
+ HuIban::try_from(iban).unwrap()
+ } else {
+ HuIban::from_bban(&tx.counter_account).unwrap()
+ };
+ let counter_account = MagnetPayto::new(iban, tx.counter_name);
if tx.amount.is_sign_positive() {
- let tx = TxIn {
+ Tx::In(TxIn {
code: tx.code,
amount,
subject: tx.subject,
- // TODO this is our account, we only have the account number of
the debtor
- debtor: MagnetPayto {
- number: tx.counter_account,
- name: tx.counter_name,
- },
+ debtor: counter_account,
timestamp: Timestamp::from(tx.value_date),
- };
- Tx::In(tx)
+ })
} else {
- let tx = TxOut {
+ Tx::Out(TxOut {
code: tx.code,
amount,
subject: tx.subject,
- // TODO this is our account, we only have the account number of
the debtor
- creditor: MagnetPayto {
- number: tx.counter_account,
- name: tx.counter_name,
- },
+ creditor: counter_account,
timestamp: Timestamp::from(tx.value_date),
- };
- Tx::Out(tx)
+ })
}
}
diff --git a/taler-magnet-bank/tests/api.rs b/taler-magnet-bank/tests/api.rs
index 13bf038..e97c127 100644
--- a/taler-magnet-bank/tests/api.rs
+++ b/taler-magnet-bank/tests/api.rs
@@ -23,7 +23,7 @@ use taler_common::{
api_wire::{OutgoingHistory, TransferState},
types::{amount::amount, payto::payto, timestamp::Timestamp, url},
};
-use taler_magnet_bank::{adapter::MagnetApi, db, MagnetPayto};
+use taler_magnet_bank::{adapter::MagnetApi, db, magnet_payto};
use taler_test_utils::{
axum_test::TestServer,
db_test_setup,
@@ -33,7 +33,13 @@ use taler_test_utils::{
async fn setup() -> (TestServer, PgPool) {
let pool = db_test_setup().await;
db::db_init(&pool, false).await.unwrap();
- let api = Arc::new(MagnetApi::start(pool.clone(),
payto("payto://magnet-bank/todo")).await);
+ let api = Arc::new(
+ MagnetApi::start(
+ pool.clone(),
+
payto("payto://iban/HU02162000031000164800000000?receiver-name=name"),
+ )
+ .await,
+ );
let builder = TalerApiBuilder::new()
.wire_gateway(api.clone(), AuthMethod::None)
.revenue(api, AuthMethod::None)
@@ -49,7 +55,7 @@ async fn transfer() {
transfer_routine(
&server,
TransferState::pending,
- &payto("payto://magnet-bank/account?receiver-name=John+Smith"),
+ &payto("payto://iban/HU02162000031000164800000000?receiver-name=name"),
)
.await;
}
@@ -76,10 +82,9 @@ async fn outgoing_history() {
code: i as u64,
amount: amount("EUR:10"),
subject: "subject".to_owned(),
- creditor: MagnetPayto {
- number: "number".to_owned(),
- name: "name".to_owned(),
- },
+ creditor: magnet_payto(
+
"payto://iban/HU30162000031000163100000000?receiver-name=name",
+ ),
timestamp: Timestamp::now_stable(),
},
&Some(OutgoingSubject(
@@ -100,7 +105,7 @@ async fn admin_add_incoming() {
let (server, _) = setup().await;
admin_add_incoming_routine(
&server,
- &payto("payto://magnet-bank/account?receiver-name=John+Smith"),
+ &payto("payto://iban/HU02162000031000164800000000?receiver-name=name"),
)
.await;
}
@@ -110,7 +115,7 @@ async fn revenue() {
let (server, _) = setup().await;
revenue_routine(
&server,
- &payto("payto://magnet-bank/account?receiver-name=John+Smith"),
+ &payto("payto://iban/HU02162000031000164800000000?receiver-name=name"),
)
.await;
}
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [taler-rust] branch master updated (1f36e0e -> 9ca55a8),
Admin <=