commit 39c74e88e8f9c0ddff1ce085880906bc0c4f181c
parent bc5b8d15033975f777cc2bf938ea6bc729a703b6
Author: Antoine A <>
Date: Sat, 23 May 2026 15:02:30 +0200
bank: add monitor endpoint
Diffstat:
3 files changed, 204 insertions(+), 54 deletions(-)
diff --git a/crates/libeufin-bank/src/api.rs b/crates/libeufin-bank/src/api.rs
@@ -25,9 +25,11 @@ use axum::{
response::{IntoResponse, Redirect, Response},
routing::get,
};
-use serde::Serialize;
+use serde::{Deserialize, Serialize};
use sqlx::PgPool;
-use taler_api::{api::TalerRouter as _, notification::NotificationChannel};
+use taler_api::{
+ api::TalerRouter as _, error::ApiResult, extract::Query, notification::NotificationChannel,
+};
use taler_common::{
api::LibtoolVersion,
types::amount::{Amount, Currency},
@@ -50,8 +52,9 @@ use crate::{
wire::wire_api,
withdrawal::{WithdrawalStatus, withdrawal_api},
},
+ auth::AdminRAuth,
config::{BankCfg, CurrencySpecification},
- db::notification_listener,
+ db::{monitor, notification_listener},
};
pub mod account;
@@ -127,6 +130,49 @@ pub struct Config<'a> {
pub max_wire_transfer_amount: Amount,
}
+#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, sqlx::Type)]
+#[sqlx(type_name = "stat_timeframe_enum")]
+#[allow(non_camel_case_types)]
+pub enum Timeframe {
+ #[default]
+ hour,
+ day,
+ month,
+ year,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct MonitorParams {
+ #[serde(default)]
+ pub timeframe: Timeframe,
+ pub date_s: Option<u64>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(tag = "type")]
+pub enum MonitorResponse {
+ #[serde(rename = "no-conversions", rename_all = "camelCase")]
+ Simple {
+ taler_in_count: u64,
+ taler_in_volume: Amount,
+ taler_out_count: u64,
+ taler_out_volume: Amount,
+ },
+ #[serde(rename = "with-conversions", rename_all = "camelCase")]
+ Conversion {
+ cashin_count: u64,
+ cashin_regional_volume: Amount,
+ cashin_fiat_volume: Amount,
+ cashout_count: u64,
+ cashout_regional_volume: Amount,
+ cashout_fiat_volume: Amount,
+ taler_in_count: u64,
+ taler_in_volume: Amount,
+ taler_out_count: u64,
+ taler_out_volume: Amount,
+ },
+}
+
async fn config(State(state): State<Arc<BankState>>) -> Response {
Json(Config {
name: (),
@@ -160,6 +206,24 @@ pub fn bank_api(state: Arc<BankState>) -> Router {
} else {
router
}
+ .route(
+ "/monitor",
+ get(
+ async |Query(params): Query<MonitorParams>,
+ _: AdminRAuth,
+ State(state): State<Arc<BankState>>| {
+ ApiResult::Ok(Json(
+ monitor(
+ &state.db,
+ &state.cfg.regional_currency,
+ state.cfg.fiat_currency(),
+ ¶ms,
+ )
+ .await?,
+ ))
+ },
+ ),
+ )
.merge(token_api())
.merge(account_api())
.merge(tx_api())
@@ -207,7 +271,7 @@ pub mod test {
use crate::{
CONFIG_SOURCE,
api::{
- BankState,
+ BankState, MonitorResponse,
account::{
AccountData, Balance, CreditDebitInfo, create_admin_account, rand_iban_payto,
},
@@ -798,4 +862,26 @@ pub mod test {
}
}
}
+
+ #[tokio::test]
+ async fn corebank() {
+ let ctx = bank_setup().await;
+
+ ctx.auth_routine(Method::GET, "/monitor", Auth::Admin).await;
+
+ ctx.get("/config").await.assert_ok();
+
+ ctx.get_admin("/monitor?timeframe=day&which=25")
+ .await
+ .assert_ok_json::<MonitorResponse>();
+ ctx.get_admin("/monitor?timeframe=day=which=25")
+ .await
+ .assert_error(ErrorCode::GENERIC_PARAMETER_MALFORMED);
+
+ ctx.fill_cashout_info("customer").await;
+ ctx.cashout("KUDOS:1").await;
+ ctx.get_admin("/monitor")
+ .await
+ .assert_ok_json::<MonitorResponse>();
+ }
}
diff --git a/crates/libeufin-bank/src/db.rs b/crates/libeufin-bank/src/db.rs
@@ -19,12 +19,17 @@
use std::time::Duration;
-use sqlx::PgPool;
-use taler_api::notification::NotificationChannel;
+use jiff::Timestamp;
+use sqlx::{PgPool, postgres::PgRow};
+use taler_api::{db::TypeHelper, notification::NotificationChannel, serialized};
+use taler_common::types::amount::{Amount, Currency};
use tokio::join;
use uuid::Uuid;
-use crate::{api::withdrawal::WithdrawalStatus, config::BankCfg};
+use crate::{
+ api::{MonitorParams, MonitorResponse, withdrawal::WithdrawalStatus},
+ config::BankCfg,
+};
pub mod account;
pub mod cashout;
@@ -96,3 +101,85 @@ pub async fn notification_listener(
);
Ok(())
}
+
+pub async fn monitor(
+ db: &PgPool,
+ regional: &Currency,
+ fiat: Option<&Currency>,
+ params: &MonitorParams,
+) -> sqlx::Result<MonitorResponse> {
+ let timestamp = if let Some(s) = params.date_s {
+ Timestamp::from_second(s as i64).unwrap()
+ } else {
+ Timestamp::now()
+ }
+ .to_string();
+ Ok(serialized!(
+ sqlx::query(
+ "
+ SELECT
+ cashin_count
+ ,cashin_regional_volume
+ ,cashin_fiat_volume
+ ,cashout_count
+ ,cashout_regional_volume
+ ,cashout_fiat_volume
+ ,taler_in_count
+ ,taler_in_volume
+ ,taler_out_count
+ ,taler_out_volume
+ FROM stats_get_frame($1::timestamp,$2::stat_timeframe_enum)
+ ",
+ )
+ .bind(×tamp)
+ .bind(params.timeframe)
+ .try_map(|r: PgRow| {
+ Ok(if let Some(fiat) = fiat {
+ MonitorResponse::Conversion {
+ cashin_count: r.try_get_u64("cashin_count")?,
+ cashin_regional_volume: r.try_get_amount("cashin_regional_volume", regional)?,
+ cashin_fiat_volume: r.try_get_amount("cashin_fiat_volume", fiat)?,
+ cashout_count: r.try_get_u64("cashout_count")?,
+ cashout_regional_volume: r
+ .try_get_amount("cashout_regional_volume", regional)?,
+ cashout_fiat_volume: r.try_get_amount("cashout_fiat_volume", fiat)?,
+ taler_in_count: r.try_get_u64("taler_in_count")?,
+ taler_in_volume: r.try_get_amount("taler_in_volume", regional)?,
+ taler_out_count: r.try_get_u64("taler_out_count")?,
+ taler_out_volume: r.try_get_amount("taler_out_volume", regional)?,
+ }
+ } else {
+ MonitorResponse::Simple {
+ taler_in_count: r.try_get_u64("taler_in_count")?,
+ taler_in_volume: r.try_get_amount("taler_in_volume", regional)?,
+ taler_out_count: r.try_get_u64("taler_out_count")?,
+ taler_out_volume: r.try_get_amount("taler_out_volume", regional)?,
+ }
+ })
+ })
+ .fetch_optional(db)
+ )?
+ .unwrap_or_else(|| {
+ if let Some(fiat) = fiat {
+ MonitorResponse::Conversion {
+ cashin_count: 0,
+ cashin_regional_volume: Amount::zero(regional),
+ cashin_fiat_volume: Amount::zero(fiat),
+ cashout_count: 0,
+ cashout_regional_volume: Amount::zero(regional),
+ cashout_fiat_volume: Amount::zero(fiat),
+ taler_in_count: 0,
+ taler_in_volume: Amount::zero(regional),
+ taler_out_count: 0,
+ taler_out_volume: Amount::zero(regional),
+ }
+ } else {
+ MonitorResponse::Simple {
+ taler_in_count: 0,
+ taler_in_volume: Amount::zero(regional),
+ taler_out_count: 0,
+ taler_out_volume: Amount::zero(regional),
+ }
+ }
+ }))
+}
diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql
@@ -1834,59 +1834,36 @@ COMMENT ON FUNCTION tan_challenge_try IS 'Try to confirm a challenge, return tru
CREATE FUNCTION stats_get_frame(
IN date TIMESTAMP,
- IN in_timeframe stat_timeframe_enum,
- OUT cashin_count INT8,
- OUT cashin_regional_volume taler_amount,
- OUT cashin_fiat_volume taler_amount,
- OUT cashout_count INT8,
- OUT cashout_regional_volume taler_amount,
- OUT cashout_fiat_volume taler_amount,
- OUT taler_in_count INT8,
- OUT taler_in_volume taler_amount,
- OUT taler_out_count INT8,
- OUT taler_out_volume taler_amount
+ IN in_timeframe stat_timeframe_enum
)
-LANGUAGE plpgsql AS $$
-BEGIN
- date = date_trunc(in_timeframe::text, date);
+RETURNS TABLE (
+ cashin_count INT8,
+ cashin_regional_volume taler_amount,
+ cashin_fiat_volume taler_amount,
+ cashout_count INT8,
+ cashout_regional_volume taler_amount,
+ cashout_fiat_volume taler_amount,
+ taler_in_count INT8,
+ taler_in_volume taler_amount,
+ taler_out_count INT8,
+ taler_out_volume taler_amount
+)
+LANGUAGE sql AS $$
SELECT
- s.cashin_count
- ,(s.cashin_regional_volume).val
- ,(s.cashin_regional_volume).frac
- ,(s.cashin_fiat_volume).val
- ,(s.cashin_fiat_volume).frac
- ,s.cashout_count
- ,(s.cashout_regional_volume).val
- ,(s.cashout_regional_volume).frac
- ,(s.cashout_fiat_volume).val
- ,(s.cashout_fiat_volume).frac
- ,s.taler_in_count
- ,(s.taler_in_volume).val
- ,(s.taler_in_volume).frac
- ,s.taler_out_count
- ,(s.taler_out_volume).val
- ,(s.taler_out_volume).frac
- INTO
cashin_count
- ,cashin_regional_volume.val
- ,cashin_regional_volume.frac
- ,cashin_fiat_volume.val
- ,cashin_fiat_volume.frac
+ ,cashin_regional_volume
+ ,cashin_fiat_volume
,cashout_count
- ,cashout_regional_volume.val
- ,cashout_regional_volume.frac
- ,cashout_fiat_volume.val
- ,cashout_fiat_volume.frac
+ ,cashout_regional_volume
+ ,cashout_fiat_volume
,taler_in_count
- ,taler_in_volume.val
- ,taler_in_volume.frac
+ ,taler_in_volume
,taler_out_count
- ,taler_out_volume.val
- ,taler_out_volume.frac
- FROM bank_stats AS s
- WHERE s.timeframe = in_timeframe
- AND s.start_time = date;
-END $$;
+ ,taler_out_volume
+ FROM bank_stats
+ WHERE timeframe = in_timeframe
+ AND start_time = date_trunc(in_timeframe::text, date)
+$$;
CREATE PROCEDURE stats_register_payment(
IN name TEXT,