libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit 5ede102532d7995f8e953ab18c7941ef69348780
parent a90ee04123141b0544288e1dba3506829fc102a7
Author: Antoine A <>
Date:   Fri,  4 Sep 2026 17:58:05 +0200

bank: more fixes

Diffstat:
MCargo.lock | 12++++++------
Mlibeufin-bank/src/api.rs | 2+-
Mlibeufin-bank/src/api/cashout.rs | 6+++---
Mlibeufin-bank/src/api/conversion.rs | 2+-
Mlibeufin-bank/src/api/tx.rs | 2+-
Mlibeufin-bank/src/api/withdrawal.rs | 14+++++---------
Mlibeufin-bank/src/db.rs | 14+++++++-------
Mlibeufin-bank/src/db/conversion.rs | 2+-
Mlibeufin-bank/src/db/gc.rs | 12++++++------
Mlibeufin-bank/src/db/withdrawal.rs | 8++++----
Mlibeufin-bank/src/lib.rs | 6+++---
Mlibeufin-nexus/src/db/exchange.rs | 6+++---
12 files changed, 41 insertions(+), 45 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1349,7 +1349,7 @@ dependencies = [ [[package]] name = "http-client" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#14f571a7e85caf215a7e9995914026f064f488fe" +source = "git+git://git.taler.net/taler-rust.git/#f56f65eed7b2e7d83602ddafd78f707b9e137b68" dependencies = [ "compact_str", "futures-util", @@ -3451,7 +3451,7 @@ dependencies = [ [[package]] name = "taler-api" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#14f571a7e85caf215a7e9995914026f064f488fe" +source = "git+git://git.taler.net/taler-rust.git/#f56f65eed7b2e7d83602ddafd78f707b9e137b68" dependencies = [ "aws-lc-rs", "axum", @@ -3479,12 +3479,12 @@ dependencies = [ [[package]] name = "taler-build" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#14f571a7e85caf215a7e9995914026f064f488fe" +source = "git+git://git.taler.net/taler-rust.git/#f56f65eed7b2e7d83602ddafd78f707b9e137b68" [[package]] name = "taler-common" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#14f571a7e85caf215a7e9995914026f064f488fe" +source = "git+git://git.taler.net/taler-rust.git/#f56f65eed7b2e7d83602ddafd78f707b9e137b68" dependencies = [ "anyhow", "aws-lc-rs", @@ -3514,7 +3514,7 @@ dependencies = [ [[package]] name = "taler-macros" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#14f571a7e85caf215a7e9995914026f064f488fe" +source = "git+git://git.taler.net/taler-rust.git/#f56f65eed7b2e7d83602ddafd78f707b9e137b68" dependencies = [ "proc-macro2", "quote", @@ -3524,7 +3524,7 @@ dependencies = [ [[package]] name = "taler-test-utils" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#14f571a7e85caf215a7e9995914026f064f488fe" +source = "git+git://git.taler.net/taler-rust.git/#f56f65eed7b2e7d83602ddafd78f707b9e137b68" dependencies = [ "aws-lc-rs", "axum", diff --git a/libeufin-bank/src/api.rs b/libeufin-bank/src/api.rs @@ -81,7 +81,7 @@ pub struct BankState { pub taler_out_channel: NotificationChannel<u64, i64>, pub taler_in_channel: NotificationChannel<u64, i64>, pub revenue_channel: NotificationChannel<u64, i64>, - pub withdrawal_channel: NotificationChannel<Uuid, WithdrawalStatus>, + pub withdrawal_channel: NotificationChannel<Uuid, Option<WithdrawalStatus>>, } impl BankState { diff --git a/libeufin-bank/src/api/cashout.rs b/libeufin-bank/src/api/cashout.rs @@ -41,7 +41,7 @@ use taler_common::{ use crate::{ api::BankState, - auth::{AdminRAuth, UserRWAuth}, + auth::{AdminRAuth, UserRAuth}, db::cashout::{CreationResult, create, get_for_user, page_all, page_for_user}, mfa::{CashoutOp, MfaReq}, }; @@ -170,7 +170,7 @@ pub fn cashout_api(state: Arc<BankState>) -> Router<Arc<BankState>> { ) .get( async |Query(params): Query<PageParams>, - auth: UserRWAuth, + auth: UserRAuth, State(state): State<Arc<BankState>>| { let params = params.check()?; @@ -187,7 +187,7 @@ pub fn cashout_api(state: Arc<BankState>) -> Router<Arc<BankState>> { "/accounts/{username}/cashouts/{id}", get( async |Path((_, id)): Path<((), u64)>, - auth: UserRWAuth, + auth: UserRAuth, State(state): State<Arc<BankState>>| { match get_for_user( &state.db, diff --git a/libeufin-bank/src/api/conversion.rs b/libeufin-bank/src/api/conversion.rs @@ -705,7 +705,7 @@ pub mod test { .await .assert_ok_json(); assert_eq!(res.amount_debit, amount("KUDOS:1")); - assert_eq!(res.amount_credit, amount("EUR:1.25")); + assert_eq!(res.amount_credit, amount("EUR:1.24")); // Check conversion from let res: ConversionResponse = ctx diff --git a/libeufin-bank/src/api/tx.rs b/libeufin-bank/src/api/tx.rs @@ -128,7 +128,7 @@ pub fn tx_api() -> Router<Arc<BankState>> { Err(failure_code(ErrorCode::BANK_ADMIN_CREDITOR)) } TxResult::UnknownDebtor => { - Err(failure_code(ErrorCode::BANK_UNKNOWN_DEBTOR)) + Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT)) } TxResult::BothPartySame => Err(failure_code(ErrorCode::BANK_SAME_ACCOUNT)), TxResult::BalanceInsufficient => Err(failure( diff --git a/libeufin-bank/src/api/withdrawal.rs b/libeufin-bank/src/api/withdrawal.rs @@ -20,10 +20,7 @@ use std::sync::Arc; use axum::{ - Json, Router, - extract::State, - response::{IntoResponse, NoContent}, - routing::{get, post}, + Json, Router, extract::State, http::StatusCode, response::{IntoResponse, NoContent}, routing::{get, post}, }; use compact_str::CompactString; use jiff::Timestamp; @@ -56,11 +53,10 @@ use crate::{ payto::{BankPayto, FullBankPayto}, }; -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)] #[sqlx(type_name = "TEXT")] #[allow(non_camel_case_types)] pub enum WithdrawalStatus { - #[default] pending, aborted, selected, @@ -383,7 +379,7 @@ pub fn withdrawal_api() -> Router<Arc<BankState>> { Err(failure_code(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT)) } SelectionResult::UnknownAccount => { - Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT)) + Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT).with_status(StatusCode::CONFLICT)) } SelectionResult::AccountIsNotExchange => { Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE)) @@ -462,7 +458,7 @@ mod test { use std::time::Duration; - use axum::http::Method; + use axum::http::{Method, StatusCode}; use serde::de::DeserializeOwned; use taler_common::{ api::EddsaPublicKey, @@ -776,7 +772,7 @@ mod test { "selected_exchange": ctx.unknown_payto })) .await - .assert_error(ErrorCode::BANK_UNKNOWN_ACCOUNT); + .assert_error_status(ErrorCode::BANK_UNKNOWN_ACCOUNT, StatusCode::CONFLICT); // Not exchange ctx.post(format!("/taler-integration/withdrawal-operation/{uuid}")) .json(json!({ diff --git a/libeufin-bank/src/db.rs b/libeufin-bank/src/db.rs @@ -108,7 +108,7 @@ pub async fn notification_listener( taler_out_channel: NotificationChannel<u64, i64>, taler_in_channel: NotificationChannel<u64, i64>, revenue_channel: NotificationChannel<u64, i64>, - withdrawal_channel: NotificationChannel<Uuid, WithdrawalStatus>, + withdrawal_channel: NotificationChannel<Uuid, Option<WithdrawalStatus>>, ) -> sqlx::Result<()> { join!( async { @@ -138,7 +138,7 @@ pub async fn notification_listener( taler_in_channel.dispatch(&account, row); }, "bank_withdrawal_status" => (uuid: Uuid, status: WithdrawalStatus) { - withdrawal_channel.dispatch(&uuid, status); + withdrawal_channel.dispatch(&uuid, Some(status)); } ) } @@ -153,7 +153,7 @@ pub async fn monitor( params: &MonitorParams, ) -> sqlx::Result<MonitorResponse> { let timestamp = if let Some(s) = params.date_s { - Timestamp::from_second(s as i64).unwrap() + Timestamp::from_second(s as i64).unwrap_or(Timestamp::MAX) } else { Timestamp::now() } @@ -759,16 +759,16 @@ mod test { monitor_taler_in(6, "KUDOS:78.84").await; ctx.cashout("3").await; - monitor_cashout(1, "KUDOS:3", "EUR:3.78").await; + monitor_cashout(1, "KUDOS:3", "EUR:3.77").await; ctx.cashout("7.6").await; - monitor_cashout(2, "KUDOS:10.6", "EUR:13.35").await; + monitor_cashout(2, "KUDOS:10.6", "EUR:13.34").await; ctx.cashout("12.3").await; - monitor_cashout(3, "KUDOS:22.9", "EUR:28.85").await; + monitor_cashout(3, "KUDOS:22.9", "EUR:28.83").await; monitor_taler_in(6, "KUDOS:78.84").await; monitor_taler_out(3, "KUDOS:82.5").await; monitor_cashin(3, "KUDOS:55.94", "EUR:70").await; - monitor_cashout(3, "KUDOS:22.9", "EUR:28.85").await; + monitor_cashout(3, "KUDOS:22.9", "EUR:28.83").await; } #[tokio::test] diff --git a/libeufin-bank/src/db/conversion.rs b/libeufin-bank/src/db/conversion.rs @@ -94,7 +94,7 @@ pub async fn update_config(db: &PgPool, cfg: &ConversionRate) -> sqlx::Result<() .bind(cfg.cashout_fee) .bind(cfg.cashout_tiny_amount) .bind(cfg.cashout_min_amount) - .bind(cfg.cashin_rounding_mode) + .bind(cfg.cashout_rounding_mode) .execute(db) )?; Ok(()) diff --git a/libeufin-bank/src/db/gc.rs b/libeufin-bank/src/db/gc.rs @@ -26,9 +26,9 @@ use taler_api::db::BindHelper; /** Run garbage collection */ pub async fn collect( db: &PgPool, - abort_after: &Timestamp, - clean_after: &Timestamp, - delete_after: &Timestamp, + abort_before: &Timestamp, + clean_before: &Timestamp, + delete_before: &Timestamp, ) -> sqlx::Result<()> { // Abort pending operations sqlx::query( @@ -37,7 +37,7 @@ pub async fn collect( SELECT FROM prepared_transfers WHERE prepared_transfers.withdrawal_id=taler_withdrawal_operations.withdrawal_id ) " - ).bind_timestamp(abort_after).execute(db).await?; + ).bind_timestamp(abort_before).execute(db).await?; // Clean aborted operations, expired challenges and expired tokens for stm in [ @@ -47,12 +47,12 @@ pub async fn collect( "DELETE FROM tan_challenges WHERE expiration_date < $1", "DELETE FROM bearer_tokens WHERE expiration_time < $1" ] { - sqlx::query(stm).bind_timestamp(clean_after).execute(db).await?; + sqlx::query(stm).bind_timestamp(clean_before).execute(db).await?; } // Delete old bank transactions, linked operations are deleted by CASCADE sqlx::query("DELETE FROM bank_account_transactions WHERE transaction_date < $1") - .bind_timestamp(delete_after) + .bind_timestamp(delete_before) .execute(db) .await?; diff --git a/libeufin-bank/src/db/withdrawal.rs b/libeufin-bank/src/db/withdrawal.rs @@ -311,14 +311,14 @@ pub async fn poll_info( db: &PgPool, ctx: &PaytoCtx, currency: &Currency, - channel: &NotificationChannel<Uuid, WithdrawalStatus>, + channel: &NotificationChannel<Uuid, Option<WithdrawalStatus>>, params: &Status, uuid: Uuid, ) -> sqlx::Result<Option<WithdrawalPublicInfo>> { pooling( &params.polling, || channel.subscribe(uuid), - |init| *init != params.status, + |it| it.is_some_and(|v| v != params.status), async || serialized!( sqlx::query( @@ -369,7 +369,7 @@ pub async fn poll_status( db: &PgPool, ctx: &PaytoCtx, currency: &Currency, - channel: &NotificationChannel<Uuid, WithdrawalStatus>, + channel: &NotificationChannel<Uuid, Option<WithdrawalStatus>>, params: &Status, uuid: Uuid, wire: WireMethod, @@ -378,7 +378,7 @@ pub async fn poll_status( pooling( &params.polling, || channel.subscribe(uuid), - |init| *init != params.status, + |it| it.is_some_and(|v| v != params.status), async || serialized!( sqlx::query( diff --git a/libeufin-bank/src/lib.rs b/libeufin-bank/src/lib.rs @@ -484,9 +484,9 @@ pub async fn run(cfg: &Config, cmd: Cmd) -> anyhow::Result<()> { let now = Zoned::now(); collect( &db, - &(now.clone() + cfg.gc_abort_after).timestamp(), - &(now.clone() + cfg.gc_clean_after).timestamp(), - &(now + cfg.gc_delete_after).timestamp(), + &(now.clone() - cfg.gc_abort_after).timestamp(), + &(now.clone() - cfg.gc_clean_after).timestamp(), + &(now - cfg.gc_delete_after).timestamp(), ) .await?; } diff --git a/libeufin-nexus/src/db/exchange.rs b/libeufin-nexus/src/db/exchange.rs @@ -72,7 +72,7 @@ pub async fn outgoing_history( amount: r.try_get_amount("amount", currency)?, debit_fee: r .try_get_opt_amount("debit_fee", currency)? - .filter(|it| it.is_zero()), + .filter(|it| !it.is_zero()), credit_account: r.try_get_payto("credit_payto")?, date: r.try_get_timestamp("execution_time")?.into(), exchange_base_url: r.try_get_url("exchange_base_url")?, @@ -118,7 +118,7 @@ pub async fn incoming_history( |r: PgRow| { let credit_fee = r .try_get_opt_amount("credit_fee", currency)? - .filter(|it| it.is_zero()); + .filter(|it| !it.is_zero()); Ok(match r.try_get_parse("type")? { IncomingType::reserve => IncomingBankTransaction::Reserve { row_id: r.try_get_u64("incoming_transaction_id")?, @@ -179,7 +179,7 @@ pub async fn revenue_history( amount: r.try_get_amount("amount", currency)?, credit_fee: r .try_get_opt_amount("credit_fee", currency)? - .filter(|it| it.is_zero()), + .filter(|it| !it.is_zero()), debit_account: r.try_get_payto("debit_payto")?, date: r.try_get_timestamp("execution_time")?.into(), subject: r.try_get("subject")?,