taler-rust

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

commit 4646afa5b78271c3294210aabec911554416787f
parent 614d4763dcf81c22d28812a0a294c011320be872
Author: Antoine A <>
Date:   Tue,  1 Jul 2025 19:10:54 +0200

common: clean code and optimize amount logic

Diffstat:
MCargo.lock | 12++++++------
Mcommon/taler-api/src/db.rs | 10+++++-----
Mcommon/taler-api/tests/common/db.rs | 16++++++++++------
Mcommon/taler-api/tests/common/mod.rs | 10+++++-----
Mcommon/taler-api/tests/security.rs | 10++++++++--
Mcommon/taler-common/src/types/amount.rs | 49++++++++++++++++++++++++++-----------------------
Mcommon/taler-test-utils/src/routine.rs | 99+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mtaler-magnet-bank/src/api.rs | 2+-
Mtaler-magnet-bank/src/constant.rs | 6+++++-
Mtaler-magnet-bank/src/db.rs | 18+++++++++---------
10 files changed, 124 insertions(+), 108 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1692,9 +1692,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.20" +version = "0.12.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ "base64", "bytes", @@ -1924,9 +1924,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf65a400f8f66fb7b0552869ad70157166676db75ed8181f8104ea91cf9d0b42" +checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" dependencies = [ "serde", "serde_derive", @@ -1935,9 +1935,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81679d9ed988d5e9a5e6531dc3f2c28efbd639cbd1dfb628df08edea6004da77" +checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" dependencies = [ "darling", "proc-macro2", diff --git a/common/taler-api/src/db.rs b/common/taler-api/src/db.rs @@ -25,7 +25,7 @@ use taler_common::{ api_common::SafeU64, api_params::{History, Page}, types::{ - amount::{Amount, Decimal}, + amount::{Amount, Currency, Decimal}, base32::Base32, iban::IBAN, payto::PaytoURI, @@ -184,8 +184,8 @@ pub trait TypeHelper { 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>; + fn try_get_amount(&self, index: &str, currency: &Currency) -> sqlx::Result<Amount>; + fn try_get_amount_i(&self, index: usize, currency: &Currency) -> sqlx::Result<Amount>; } impl TypeHelper for PgRow { @@ -215,7 +215,7 @@ impl TypeHelper for PgRow { self.try_get_map(index, |s: &str| s.parse()) } - fn try_get_amount(&self, index: &str, currency: &str) -> sqlx::Result<Amount> { + fn try_get_amount(&self, index: &str, currency: &Currency) -> sqlx::Result<Amount> { let val_idx = format!("{index}_val"); let frac_idx = format!("{index}_frac"); let val_idx = val_idx.as_str(); @@ -226,7 +226,7 @@ impl TypeHelper for PgRow { Ok(Amount::new(currency, val, frac)) } - fn try_get_amount_i(&self, index: usize, currency: &str) -> sqlx::Result<Amount> { + fn try_get_amount_i(&self, index: usize, currency: &Currency) -> sqlx::Result<Amount> { let val = self.try_get_u64(index)?; let frac = self.try_get_u32(index + 1)?; diff --git a/common/taler-api/tests/common/db.rs b/common/taler-api/tests/common/db.rs @@ -24,7 +24,11 @@ use taler_common::{ IncomingBankTransaction, OutgoingBankTransaction, TransferListStatus, TransferRequest, TransferResponse, TransferState, TransferStatus, }, - types::{amount::Amount, payto::PaytoURI, timestamp::Timestamp}, + types::{ + amount::{Amount, Currency}, + payto::PaytoURI, + timestamp::Timestamp, + }, }; use tokio::sync::watch::{Receiver, Sender}; @@ -83,7 +87,7 @@ pub async fn transfer_page( db: &PgPool, status: &Option<TransferState>, params: &Page, - currency: &str, + currency: &Currency, ) -> sqlx::Result<Vec<TransferListStatus>> { page( db, @@ -123,7 +127,7 @@ pub async fn transfer_page( pub async fn transfer_by_id( db: &PgPool, id: u64, - currency: &str, + currency: &Currency, ) -> sqlx::Result<Option<TransferStatus>> { sqlx::query( " @@ -158,7 +162,7 @@ pub async fn transfer_by_id( pub async fn outgoing_revenue( db: &PgPool, params: &History, - currency: &str, + currency: &Currency, listen: impl FnOnce() -> Receiver<i64>, ) -> sqlx::Result<Vec<OutgoingBankTransaction>> { history( @@ -238,7 +242,7 @@ pub async fn add_incoming( pub async fn incoming_history( db: &PgPool, params: &History, - currency: &str, + currency: &Currency, listen: impl FnOnce() -> Receiver<i64>, ) -> sqlx::Result<Vec<IncomingBankTransaction>> { history( @@ -296,7 +300,7 @@ pub async fn incoming_history( pub async fn revenue_history( db: &PgPool, params: &History, - currency: &str, + currency: &Currency, listen: impl FnOnce() -> Receiver<i64>, ) -> sqlx::Result<Vec<RevenueIncomingBankTransaction>> { history( diff --git a/common/taler-api/tests/common/mod.rs b/common/taler-api/tests/common/mod.rs @@ -33,7 +33,7 @@ use taler_common::{ TransferState, TransferStatus, }, error_code::ErrorCode, - types::{payto::payto, timestamp::Timestamp}, + types::{amount::Currency, payto::payto, timestamp::Timestamp}, }; use taler_test_utils::db_test_setup_manual; use tokio::sync::watch::Sender; @@ -42,7 +42,7 @@ pub mod db; /// Taler API implementation for tests pub struct TestApi { - currency: String, + currency: Currency, pool: PgPool, outgoing_channel: Sender<i64>, incoming_channel: Sender<i64>, @@ -50,7 +50,7 @@ pub struct TestApi { impl TalerApi for TestApi { fn currency(&self) -> &str { - &self.currency + self.currency.as_ref() } fn implementation(&self) -> Option<&str> { @@ -178,7 +178,7 @@ impl Revenue for TestApi { } } -pub async fn test_api(pool: PgPool, currency: String) -> Router { +pub async fn test_api(pool: PgPool, currency: Currency) -> Router { let outgoing_channel = Sender::new(0); let incoming_channel = Sender::new(0); let wg = TestApi { @@ -200,7 +200,7 @@ pub async fn test_api(pool: PgPool, currency: String) -> Router { pub async fn setup() -> (Router, PgPool) { let pool = db_test_setup_manual("../../database-versioning".as_ref(), "taler-api").await; - let api = test_api(pool.clone(), "EUR".to_string()).await; + let api = test_api(pool.clone(), "EUR".parse().unwrap()).await; (api.finalize(), pool) } diff --git a/common/taler-api/tests/security.rs b/common/taler-api/tests/security.rs @@ -20,7 +20,12 @@ use taler_api::constants::MAX_BODY_LENGTH; use taler_common::{ api_wire::{TransferRequest, TransferResponse}, error_code::ErrorCode, - types::{amount::Amount, base32::Base32, payto::payto, url}, + types::{ + amount::{Amount, Currency}, + base32::Base32, + payto::payto, + url, + }, }; use taler_test_utils::server::TestServer as _; @@ -29,9 +34,10 @@ mod common; #[tokio::test] async fn body_parsing() { let (server, _) = setup().await; + let eur: Currency = "EUR".parse().unwrap(); let normal_body = TransferRequest { request_uid: Base32::rand(), - amount: Amount::zero("EUR"), + amount: Amount::zero(&eur), exchange_base_url: url("https://test.com"), wtid: Base32::rand(), credit_account: payto("payto:://test"), diff --git a/common/taler-common/src/types/amount.rs b/common/taler-common/src/types/amount.rs @@ -216,20 +216,19 @@ pub struct Amount { } impl Amount { - pub fn new_decimal(currency: impl AsRef<str>, decimal: Decimal) -> Self { - let currency = currency.as_ref().parse().expect("Invalid currency"); - (currency, decimal).into() + pub fn new_decimal(currency: &Currency, decimal: Decimal) -> Self { + (currency.clone(), decimal).into() } - pub fn new(currency: impl AsRef<str>, val: u64, frac: u32) -> Self { + pub fn new(currency: &Currency, val: u64, frac: u32) -> Self { Self::new_decimal(currency, Decimal { val, frac }) } - pub fn max(currency: impl AsRef<str>) -> Self { + pub fn max(currency: &Currency) -> Self { Self::new_decimal(currency, Decimal::max()) } - pub fn zero(currency: impl AsRef<str>) -> Self { + pub fn zero(currency: &Currency) -> Self { Self::new_decimal(currency, Decimal::zero()) } @@ -327,22 +326,24 @@ fn test_amount_parse() { assert!(amount.is_err(), "invalid {} got {:?}", str, &amount); } + let eur: Currency = "EUR".parse().unwrap(); + let local: Currency = "LOCAL".parse().unwrap(); let valid_amounts: Vec<(&str, &str, Amount)> = vec![ - ("EUR:4", "EUR:4", Amount::new("EUR", 4, 0)), // without fraction + ("EUR:4", "EUR:4", Amount::new(&eur, 4, 0)), // without fraction ( "EUR:0.02", "EUR:0.02", - Amount::new("EUR", 0, TALER_AMOUNT_FRAC_BASE / 100 * 2), + Amount::new(&eur, 0, TALER_AMOUNT_FRAC_BASE / 100 * 2), ), // leading zero fraction ( " EUR:4.12", "EUR:4.12", - Amount::new("EUR", 4, TALER_AMOUNT_FRAC_BASE / 100 * 12), + Amount::new(&eur, 4, TALER_AMOUNT_FRAC_BASE / 100 * 12), ), // leading space and fraction ( " LOCAL:4444.1000", "LOCAL:4444.1", - Amount::new("LOCAL", 4444, TALER_AMOUNT_FRAC_BASE / 10), + Amount::new(&local, 4444, TALER_AMOUNT_FRAC_BASE / 10), ), // local currency ]; for (raw, expected, goal) in valid_amounts { @@ -365,13 +366,14 @@ fn test_amount_parse() { #[test] fn test_amount_add() { + let eur: Currency = "EUR".parse().unwrap(); assert_eq!( - Amount::max("EUR").try_add(&Amount::zero("EUR")), - Some(Amount::max("EUR")) + Amount::max(&eur).try_add(&Amount::zero(&eur)), + Some(Amount::max(&eur)) ); assert_eq!( - Amount::zero("EUR").try_add(&Amount::zero("EUR")), - Some(Amount::zero("EUR")) + Amount::zero(&eur).try_add(&Amount::zero(&eur)), + Some(Amount::zero(&eur)) ); assert_eq!( amount("EUR:6.41").try_add(&amount("EUR:4.69")), @@ -379,7 +381,7 @@ fn test_amount_add() { ); assert_eq!( amount(format!("EUR:{MAX_VALUE}")).try_add(&amount("EUR:0.99999999")), - Some(Amount::max("EUR")) + Some(Amount::max(&eur)) ); assert_eq!( @@ -387,7 +389,7 @@ fn test_amount_add() { None ); assert_eq!( - Amount::new("EUR", u64::MAX, 0).try_add(&amount("EUR:1")), + Amount::new(&eur, u64::MAX, 0).try_add(&amount("EUR:1")), None ); assert_eq!( @@ -399,22 +401,23 @@ fn test_amount_add() { #[test] fn test_amount_normalize() { + let eur: Currency = "EUR".parse().unwrap(); assert_eq!( - Amount::new("EUR", 4, 2 * FRAC_BASE).normalize(), + Amount::new(&eur, 4, 2 * FRAC_BASE).normalize(), Some(amount("EUR:6")) ); assert_eq!( - Amount::new("EUR", 4, 2 * FRAC_BASE + 1).normalize(), + Amount::new(&eur, 4, 2 * FRAC_BASE + 1).normalize(), Some(amount("EUR:6.00000001")) ); assert_eq!( - Amount::new("EUR", MAX_VALUE, FRAC_BASE - 1).normalize(), - Some(Amount::new("EUR", MAX_VALUE, FRAC_BASE - 1)) + Amount::new(&eur, MAX_VALUE, FRAC_BASE - 1).normalize(), + Some(Amount::new(&eur, MAX_VALUE, FRAC_BASE - 1)) ); - assert_eq!(Amount::new("EUR", u64::MAX, FRAC_BASE).normalize(), None); - assert_eq!(Amount::new("EUR", MAX_VALUE, FRAC_BASE).normalize(), None); + assert_eq!(Amount::new(&eur, u64::MAX, FRAC_BASE).normalize(), None); + assert_eq!(Amount::new(&eur, MAX_VALUE, FRAC_BASE).normalize(), None); - for amount in [Amount::max("EUR"), Amount::zero("EUR")] { + for amount in [Amount::max(&eur), Amount::zero(&eur)] { assert_eq!(amount.clone().normalize(), Some(amount)) } } diff --git a/common/taler-test-utils/src/routine.rs b/common/taler-test-utils/src/routine.rs @@ -281,25 +281,24 @@ pub async fn transfer_routine( let transfer_request = json!({ "request_uid": HashCode::rand(), "amount": default_amount, - "exchange_base_url": "http://exchange.taler", + "exchange_base_url": "http://exchange.taler/", "wtid": ShortHashCode::rand(), "credit_account": credit_account, }); // Check empty db { - let res = server.get("/taler-wire-gateway/transfers").await; - if res.is_implemented() { - res.assert_no_content(); - - server - .get(&format!( - "/taler-wire-gateway/transfers?status={}", - default_status.as_ref() - )) - .await - .assert_no_content(); - } + server + .get("/taler-wire-gateway/transfers") + .await + .assert_no_content(); + server + .get(&format!( + "/taler-wire-gateway/transfers?status={}", + default_status.as_ref() + )) + .await + .assert_no_content(); } // Check create transfer @@ -359,23 +358,21 @@ pub async fn transfer_routine( .assert_ok_json::<TransferResponse>(); // Check OK - let res = server + let tx = server .get(&format!("/taler-wire-gateway/transfers/{}", resp.row_id)) - .await; - if res.is_implemented() { - let tx = res.assert_ok_json::<TransferStatus>(); - assert_eq!(default_status, tx.status); - assert_eq!(default_amount, tx.amount); - assert_eq!("http://exchange.taler/", tx.origin_exchange_url); - assert_eq!(wtid, tx.wtid); - assert_eq!(resp.timestamp, tx.timestamp); - - // Check unknown transaction - server - .get("/taler-wire-gateway/transfers/42") - .await - .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND); - } + .await + .assert_ok_json::<TransferStatus>(); + assert_eq!(default_status, tx.status); + assert_eq!(default_amount, tx.amount); + assert_eq!("http://exchange.taler/", tx.origin_exchange_url); + assert_eq!(wtid, tx.wtid); + assert_eq!(resp.timestamp, tx.timestamp); + + // Check unknown transaction + server + .get("/taler-wire-gateway/transfers/42") + .await + .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND); } // Check transfer page @@ -391,21 +388,21 @@ pub async fn transfer_routine( .assert_ok_json::<TransferResponse>(); } { - let res = server.get("/taler-wire-gateway/transfers").await; - if res.is_implemented() { - let list = res.assert_ok_json::<TransferList>(); - assert_eq!(list.transfers.len(), 6); - assert_eq!( - list, - server - .get(&format!( - "/taler-wire-gateway/transfers?status={}", - default_status.as_ref() - )) - .await - .assert_ok_json::<TransferList>() - ); - } + let list = server + .get("/taler-wire-gateway/transfers") + .await + .assert_ok_json::<TransferList>(); + assert_eq!(list.transfers.len(), 6); + assert_eq!( + list, + server + .get(&format!( + "/taler-wire-gateway/transfers?status={}", + default_status.as_ref() + )) + .await + .assert_ok_json::<TransferList>() + ) } // Pagination test @@ -511,7 +508,7 @@ async fn add_incoming_routine( } /// Test standard behavior of the revenue endpoints -pub async fn revenue_routine(server: &Router, debit_acount: &PaytoURI) { +pub async fn revenue_routine(server: &Router, debit_acount: &PaytoURI, kyc: bool) { let currency = &get_currency(server).await; routine_history( @@ -525,7 +522,7 @@ pub async fn revenue_routine(server: &Router, debit_acount: &PaytoURI) { }, 2, |server, i| async move { - if i % 2 == 0 { + if i % 2 == 0 || !kyc { server .post("/taler-wire-gateway/admin/add-incoming") .json(&json!({ @@ -554,7 +551,7 @@ pub async fn revenue_routine(server: &Router, debit_acount: &PaytoURI) { } /// Test standard behavior of the admin add incoming endpoints -pub async fn admin_add_incoming_routine(server: &Router, debit_acount: &PaytoURI) { +pub async fn admin_add_incoming_routine(server: &Router, debit_acount: &PaytoURI, kyc: bool) { let currency = &get_currency(server).await; // History @@ -574,7 +571,7 @@ pub async fn admin_add_incoming_routine(server: &Router, debit_acount: &PaytoURI }, 2, |server, i| async move { - if i % 2 == 0 { + if i % 2 == 0 || !kyc { server .post("/taler-wire-gateway/admin/add-incoming") .json(&json!({ @@ -602,6 +599,8 @@ pub async fn admin_add_incoming_routine(server: &Router, debit_acount: &PaytoURI .await; // Add incoming reserve add_incoming_routine(server, currency, IncomingType::reserve, debit_acount).await; - // Add incoming kyc - add_incoming_routine(server, currency, IncomingType::kyc, debit_acount).await; + if kyc { + // Add incoming kyc + add_incoming_routine(server, currency, IncomingType::kyc, debit_acount).await; + } } diff --git a/taler-magnet-bank/src/api.rs b/taler-magnet-bank/src/api.rs @@ -75,7 +75,7 @@ impl MagnetApi { impl TalerApi for MagnetApi { fn currency(&self) -> &str { - CURRENCY + CURRENCY.as_ref() } fn implementation(&self) -> Option<&str> { diff --git a/taler-magnet-bank/src/constant.rs b/taler-magnet-bank/src/constant.rs @@ -14,4 +14,8 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -pub const CURRENCY: &str = "HUF"; +use std::sync::LazyLock; + +use taler_common::types::amount::Currency; + +pub static CURRENCY: LazyLock<Currency> = LazyLock::new(|| "HUF".parse().unwrap()); diff --git a/taler-magnet-bank/src/db.rs b/taler-magnet-bank/src/db.rs @@ -363,7 +363,7 @@ pub async fn transfer_page<'a>( Ok(TransferListStatus { row_id: r.try_get_safeu64(0)?, status: r.try_get(1)?, - amount: r.try_get_amount_i(2, CURRENCY)?, + amount: r.try_get_amount_i(2, &CURRENCY)?, credit_account: r.try_get_iban(4)?.as_full_payto(r.try_get(5)?), timestamp: r.try_get_timestamp(6)?, }) @@ -403,7 +403,7 @@ pub async fn outgoing_history( |r: PgRow| { Ok(OutgoingBankTransaction { row_id: r.try_get_safeu64(0)?, - amount: r.try_get_amount_i(1, CURRENCY)?, + amount: r.try_get_amount_i(1, &CURRENCY)?, 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)?, @@ -446,14 +446,14 @@ pub async fn incoming_history( Ok(match r.try_get(0)? { IncomingType::reserve => IncomingBankTransaction::Reserve { row_id: r.try_get_safeu64(1)?, - amount: r.try_get_amount_i(2, CURRENCY)?, + amount: r.try_get_amount_i(2, &CURRENCY)?, 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)?, + amount: r.try_get_amount_i(2, &CURRENCY)?, 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)?, @@ -497,7 +497,7 @@ pub async fn revenue_history( Ok(RevenueIncomingBankTransaction { row_id: r.try_get_safeu64(0)?, date: r.try_get_timestamp(1)?, - amount: r.try_get_amount_i(2, CURRENCY)?, + amount: r.try_get_amount_i(2, &CURRENCY)?, credit_fee: None, debit_account: r.try_get_iban(4)?.as_full_payto(r.try_get(5)?), subject: r.try_get(6)?, @@ -533,7 +533,7 @@ pub async fn transfer_by_id<'a>( Ok(TransferStatus { status: r.try_get(0)?, status_msg: r.try_get(1)?, - amount: r.try_get_amount_i(2, CURRENCY)?, + amount: r.try_get_amount_i(2, &CURRENCY)?, origin_exchange_url: r.try_get(4)?, wtid: r.try_get_base32(5)?, credit_account: r.try_get_iban(6)?.as_full_payto(r.try_get(7)?), @@ -560,7 +560,7 @@ pub async fn pending_batch<'a>( .try_map(|r: PgRow| { Ok(Initiated { id: r.try_get_u64(0)?, - amount: r.try_get_amount_i(1, CURRENCY)?, + amount: r.try_get_amount_i(1, &CURRENCY)?, subject: r.try_get(3)?, creditor: FullHuPayto::new(r.try_get_parse(4)?, r.try_get(5)?), }) @@ -1251,7 +1251,7 @@ mod test { &mut db, &TransferRequest { request_uid: HashCode::rand(), - amount: amount(format!("{CURRENCY}:{}", i + 1)), + amount: amount(format!("{}:{}", *CURRENCY, i + 1)), exchange_base_url: url("https://exchange.test.com/"), wtid: ShortHashCode::rand(), credit_account: payto( @@ -1275,7 +1275,7 @@ mod test { &mut db, &TransferRequest { request_uid: HashCode::rand(), - amount: amount(format!("{CURRENCY}:{}", i + 1)), + amount: amount(format!("{}:{}", *CURRENCY, i + 1)), exchange_base_url: url("https://exchange.test.com/"), wtid: ShortHashCode::rand(), credit_account: payto(