commit 4646afa5b78271c3294210aabec911554416787f
parent 614d4763dcf81c22d28812a0a294c011320be872
Author: Antoine A <>
Date: Tue, 1 Jul 2025 19:10:54 +0200
common: clean code and optimize amount logic
Diffstat:
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(