commit f254d533d053b7f85c3caf1bc67346f2ccd105f2
parent cc7dc52133342763c69867fc3c47166f4ffe01b7
Author: Antoine A <>
Date: Thu, 21 May 2026 17:13:56 +0200
common: improve apis, test routines and error handling
Diffstat:
36 files changed, 1860 insertions(+), 1395 deletions(-)
diff --git a/common/taler-api/src/api.rs b/common/taler-api/src/api.rs
@@ -28,41 +28,48 @@ use axum::{
response::Response,
};
use revenue::Revenue;
-use taler_common::{error_code::ErrorCode, types::amount::Amount};
+use taler_common::{
+ error_code::ErrorCode,
+ types::amount::{Amount, Currency},
+};
use tokio::signal;
use tracing::{Level, info};
use wire::WireGateway;
use crate::{
Listener, Serve,
- api::transfer::PreparedTransfer,
+ api::prepared::PreparedTransfer,
auth::{AuthMethod, AuthMiddlewareState},
error::{ApiResult, LoggedError, failure, failure_code},
};
+pub mod prepared;
pub mod revenue;
-pub mod transfer;
pub mod wire;
pub use axum::Router;
+pub trait Validation {
+ fn check(&self, currency: &Currency) -> ApiResult<()>;
+}
+
+fn check_currency(currency: &Currency, amount: &Amount) -> ApiResult<()> {
+ if &amount.currency != currency {
+ Err(failure(
+ ErrorCode::GENERIC_CURRENCY_MISMATCH,
+ format!(
+ "wrong currency expected {} got {}",
+ currency, amount.currency
+ ),
+ ))
+ } else {
+ Ok(())
+ }
+}
+
pub trait TalerApi: Send + Sync + 'static {
- fn currency(&self) -> &str;
+ fn currency(&self) -> Currency;
fn implementation(&self) -> &'static str;
- fn check_currency(&self, amount: &Amount) -> ApiResult<()> {
- let currency = self.currency();
- if amount.currency.as_ref() != currency {
- Err(failure(
- ErrorCode::GENERIC_CURRENCY_MISMATCH,
- format!(
- "wrong currency expected {} got {}",
- currency, amount.currency
- ),
- ))
- } else {
- Ok(())
- }
- }
}
pub trait RouterUtils {
@@ -96,7 +103,7 @@ impl TalerRouter for Router {
}
fn prepared_transfer<T: PreparedTransfer>(self, api: Arc<T>) -> Self {
- self.nest("/taler-prepared-transfer", transfer::router(api))
+ self.nest("/taler-prepared-transfer", prepared::router(api))
}
fn revenue<T: Revenue>(self, api: Arc<T>, auth: AuthMethod) -> Self {
diff --git a/common/taler-api/src/api/prepared.rs b/common/taler-api/src/api/prepared.rs
@@ -0,0 +1,142 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2026 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+
+use std::{str::FromStr, sync::Arc};
+
+use axum::{
+ Json, Router,
+ extract::State,
+ http::StatusCode,
+ response::IntoResponse as _,
+ routing::{get, post},
+};
+use jiff::{SignedDuration, Timestamp};
+use taler_common::{
+ api::prepared::{
+ PreparedTransferConfig, RegistrationRequest, RegistrationResponse, SubjectFormat,
+ TransferSubject, Unregistration,
+ },
+ db::IncomingType,
+ error_code::ErrorCode,
+ types::amount::Currency,
+};
+
+use super::TalerApi;
+use crate::{
+ api::{Validation, check_currency},
+ constants::PREPARED_TRANSFER_API_VERSION,
+ crypto::check_eddsa_signature,
+ error::{ApiResult, failure, failure_code},
+ extract::Req,
+ subject::fmt_in_subject,
+};
+
+pub trait PreparedTransfer: TalerApi {
+ fn supported_formats(&self) -> &[SubjectFormat];
+ fn registration(
+ &self,
+ req: RegistrationRequest,
+ ) -> impl std::future::Future<Output = ApiResult<RegistrationResponse>> + Send;
+ fn unregistration(
+ &self,
+ req: Unregistration,
+ ) -> impl std::future::Future<Output = ApiResult<bool>> + Send;
+}
+
+impl Validation for RegistrationRequest {
+ fn check(&self, currency: &Currency) -> ApiResult<()> {
+ if !check_eddsa_signature(
+ &self.authorization_pub,
+ self.account_pub.as_ref(),
+ &self.authorization_sig,
+ ) {
+ return Err(failure_code(ErrorCode::BANK_BAD_SIGNATURE));
+ }
+ check_currency(currency, &self.credit_amount)
+ }
+}
+
+impl Validation for Unregistration {
+ fn check(&self, _: &Currency) -> ApiResult<()> {
+ let timestamp = Timestamp::from_str(&self.timestamp).map_err(|e| {
+ failure(ErrorCode::GENERIC_JSON_INVALID, e.to_string()).with_path("timestamp")
+ })?;
+ if timestamp.duration_until(Timestamp::now()) > SignedDuration::from_mins(5) {
+ return Err(failure_code(ErrorCode::BANK_OLD_TIMESTAMP));
+ }
+
+ if !check_eddsa_signature(
+ &self.authorization_pub,
+ self.timestamp.as_ref(),
+ &self.authorization_sig,
+ ) {
+ return Err(failure_code(ErrorCode::BANK_BAD_SIGNATURE));
+ }
+ Ok(())
+ }
+}
+
+pub fn simple_subject(req: RegistrationRequest) -> TransferSubject {
+ TransferSubject::Simple {
+ credit_amount: req.credit_amount,
+ subject: if req.authorization_pub == req.account_pub && !req.recurrent {
+ fmt_in_subject(req.r#type.into(), &req.account_pub).to_string()
+ } else {
+ fmt_in_subject(IncomingType::map, &req.authorization_pub).to_string()
+ },
+ }
+}
+
+pub fn router<I: PreparedTransfer>(state: Arc<I>) -> Router {
+ Router::new()
+ .route(
+ "/registration",
+ post(
+ async |State(state): State<Arc<I>>, Req(req): Req<RegistrationRequest>| {
+ req.check(&state.currency())?;
+ let res = state.registration(req).await?;
+ ApiResult::Ok(Json(res))
+ },
+ ),
+ )
+ .route(
+ "/unregistration",
+ post(
+ async |State(state): State<Arc<I>>, Req(req): Req<Unregistration>| {
+ req.check(&state.currency())?;
+ if state.unregistration(req).await? {
+ ApiResult::Ok(StatusCode::NO_CONTENT)
+ } else {
+ Err(failure_code(ErrorCode::BANK_TRANSACTION_NOT_FOUND))
+ }
+ },
+ ),
+ )
+ .route(
+ "/config",
+ get(async |State(state): State<Arc<I>>| {
+ Json(PreparedTransferConfig {
+ name: (),
+ version: PREPARED_TRANSFER_API_VERSION,
+ currency: state.currency(),
+ implementation: Some(state.implementation()),
+ supported_formats: state.supported_formats().to_vec(),
+ })
+ .into_response()
+ }),
+ )
+ .with_state(state)
+}
diff --git a/common/taler-api/src/api/revenue.rs b/common/taler-api/src/api/revenue.rs
@@ -17,9 +17,9 @@
use std::sync::Arc;
use axum::{Json, Router, extract::State, http::StatusCode, response::IntoResponse, routing::get};
-use taler_common::{
- api_params::{History, HistoryParams},
- api_revenue::{RevenueConfig, RevenueIncomingHistory},
+use taler_common::api::{
+ params::{History, HistoryParams},
+ revenue::{RevenueConfig, RevenueIncomingHistory},
};
use super::TalerApi;
@@ -56,7 +56,7 @@ pub fn router<I: Revenue>(state: Arc<I>, auth: AuthMethod) -> Router {
"/config",
get(async |State(state): State<Arc<I>>| {
Json(RevenueConfig {
- name: "taler-revenue",
+ name: (),
version: REVENUE_API_VERSION,
currency: state.currency(),
implementation: Some(state.implementation()),
diff --git a/common/taler-api/src/api/transfer.rs b/common/taler-api/src/api/transfer.rs
@@ -1,112 +0,0 @@
-/*
- This file is part of TALER
- Copyright (C) 2026 Taler Systems SA
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU Affero General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-*/
-
-use std::{str::FromStr, sync::Arc};
-
-use axum::{
- Json, Router,
- extract::State,
- http::StatusCode,
- response::IntoResponse as _,
- routing::{get, post},
-};
-use jiff::{SignedDuration, Timestamp};
-use taler_common::{
- api_transfer::{
- PreparedTransferConfig, RegistrationRequest, RegistrationResponse, SubjectFormat,
- Unregistration,
- },
- error_code::ErrorCode,
-};
-
-use super::TalerApi;
-use crate::{
- constants::PREPARED_TRANSFER_API_VERSION,
- crypto::check_eddsa_signature,
- error::{ApiResult, failure, failure_code},
- extract::Req,
-};
-
-pub trait PreparedTransfer: TalerApi {
- fn supported_formats(&self) -> &[SubjectFormat];
- fn registration(
- &self,
- req: RegistrationRequest,
- ) -> impl std::future::Future<Output = ApiResult<RegistrationResponse>> + Send;
- fn unregistration(
- &self,
- req: Unregistration,
- ) -> impl std::future::Future<Output = ApiResult<()>> + Send;
-}
-
-pub fn router<I: PreparedTransfer>(state: Arc<I>) -> Router {
- Router::new()
- .route(
- "/registration",
- post(
- async |State(state): State<Arc<I>>, Req(req): Req<RegistrationRequest>| {
- state.check_currency(&req.credit_amount)?;
- if !check_eddsa_signature(
- &req.authorization_pub,
- req.account_pub.as_ref(),
- &req.authorization_sig,
- ) {
- return Err(failure_code(ErrorCode::BANK_BAD_SIGNATURE));
- }
- let res = state.registration(req).await?;
- ApiResult::Ok(Json(res))
- },
- ),
- )
- .route(
- "/unregistration",
- post(
- async |State(state): State<Arc<I>>, Req(req): Req<Unregistration>| {
- let timestamp = Timestamp::from_str(&req.timestamp).map_err(|e| {
- failure(ErrorCode::GENERIC_JSON_INVALID, e.to_string())
- .with_path("timestamp")
- })?;
- if timestamp.duration_until(Timestamp::now()) > SignedDuration::from_mins(5) {
- return Err(failure_code(ErrorCode::BANK_OLD_TIMESTAMP));
- }
-
- if !check_eddsa_signature(
- &req.authorization_pub,
- req.timestamp.as_ref(),
- &req.authorization_sig,
- ) {
- return Err(failure_code(ErrorCode::BANK_BAD_SIGNATURE));
- }
- state.unregistration(req).await?;
- ApiResult::Ok(StatusCode::NO_CONTENT)
- },
- ),
- )
- .route(
- "/config",
- get(async |State(state): State<Arc<I>>| {
- Json(PreparedTransferConfig {
- name: "taler-prepared-transfer",
- version: PREPARED_TRANSFER_API_VERSION,
- currency: state.currency(),
- implementation: Some(state.implementation()),
- supported_formats: state.supported_formats().to_vec(),
- })
- .into_response()
- }),
- )
- .with_state(state)
-}
diff --git a/common/taler-api/src/api/wire.rs b/common/taler-api/src/api/wire.rs
@@ -25,21 +25,24 @@ use axum::{
};
use regex::Regex;
use taler_common::{
- api_params::{AccountParams, History, HistoryParams, Page, TransferParams},
- api_wire::{
- AccountInfo, AddIncomingRequest, AddIncomingResponse, AddKycauthRequest, AddMappedRequest,
- IncomingHistory, OutgoingHistory, TransferList, TransferRequest, TransferResponse,
- TransferState, TransferStatus, WireConfig,
+ api::{
+ params::{AccountParams, History, HistoryParams, Page, TransferParams},
+ wire::{
+ AccountInfo, AddIncomingRequest, AddIncomingResponse, AddKycauthRequest,
+ AddMappedRequest, IncomingHistory, OutgoingHistory, TransferList, TransferRequest,
+ TransferResponse, TransferState, TransferStatus, WireConfig,
+ },
},
error_code::ErrorCode,
+ types::amount::Currency,
};
use super::TalerApi;
use crate::{
- api::RouterUtils as _,
+ api::{RouterUtils as _, Validation, check_currency},
auth::AuthMethod,
constants::WIRE_GATEWAY_API_VERSION,
- error::{ApiResult, failure, failure_code, failure_status},
+ error::{ApiResult, bad_request, failure_code, failure_status},
extract::{Path, Query, Req},
};
@@ -94,8 +97,39 @@ pub trait WireGateway: TalerApi {
}
}
-static METADATA_PATTERN: LazyLock<Regex> =
- LazyLock::new(|| Regex::new("^[a-zA-Z0-9-.:]{1, 40}$").unwrap());
+impl Validation for TransferRequest {
+ fn check(&self, currency: &Currency) -> ApiResult<()> {
+ static METADATA_PATTERN: LazyLock<Regex> =
+ LazyLock::new(|| Regex::new("^[a-zA-Z0-9-.:]{1, 40}$").unwrap());
+ if let Some(metadata) = &self.metadata
+ && !METADATA_PATTERN.is_match(metadata)
+ {
+ return Err(bad_request(format_args!(
+ "metadata '{metadata}' is malformed, must match {}",
+ METADATA_PATTERN.as_str()
+ )));
+ }
+ check_currency(currency, &self.amount)
+ }
+}
+
+impl Validation for AddIncomingRequest {
+ fn check(&self, currency: &Currency) -> ApiResult<()> {
+ check_currency(currency, &self.amount)
+ }
+}
+
+impl Validation for AddKycauthRequest {
+ fn check(&self, currency: &Currency) -> ApiResult<()> {
+ check_currency(currency, &self.amount)
+ }
+}
+
+impl Validation for AddMappedRequest {
+ fn check(&self, currency: &Currency) -> ApiResult<()> {
+ check_currency(currency, &self.amount)
+ }
+}
pub fn router<I: WireGateway>(state: Arc<I>, auth: AuthMethod) -> Router {
Router::new()
@@ -103,18 +137,7 @@ pub fn router<I: WireGateway>(state: Arc<I>, auth: AuthMethod) -> Router {
"/transfer",
post(
async |State(state): State<Arc<I>>, Req(req): Req<TransferRequest>| {
- state.check_currency(&req.amount)?;
- if let Some(metadata) = &req.metadata
- && !METADATA_PATTERN.is_match(metadata)
- {
- return Err(failure(
- ErrorCode::GENERIC_JSON_INVALID,
- format!(
- "metadata '{metadata}' is malformed, must match {}",
- METADATA_PATTERN.as_str()
- ),
- ));
- }
+ req.check(&state.currency())?;
ApiResult::Ok(Json(state.transfer(req).await?))
},
),
@@ -138,10 +161,7 @@ pub fn router<I: WireGateway>(state: Arc<I>, auth: AuthMethod) -> Router {
get(async |State(state): State<Arc<I>>, Path(id): Path<u64>| {
match state.transfer_by_id(id).await? {
Some(it) => Ok(Json(it)),
- None => Err(failure(
- ErrorCode::BANK_TRANSACTION_NOT_FOUND,
- format!("Transfer '{id}' not found"),
- )),
+ None => Err(failure_code(ErrorCode::BANK_TRANSACTION_NOT_FOUND)),
}
}),
)
@@ -177,7 +197,7 @@ pub fn router<I: WireGateway>(state: Arc<I>, auth: AuthMethod) -> Router {
"/admin/add-incoming",
post(
async |State(state): State<Arc<I>>, Req(req): Req<AddIncomingRequest>| {
- state.check_currency(&req.amount)?;
+ req.check(&state.currency())?;
ApiResult::Ok(Json(state.add_incoming_reserve(req).await?))
},
),
@@ -186,7 +206,7 @@ pub fn router<I: WireGateway>(state: Arc<I>, auth: AuthMethod) -> Router {
"/admin/add-kycauth",
post(
async |State(state): State<Arc<I>>, Req(req): Req<AddKycauthRequest>| {
- state.check_currency(&req.amount)?;
+ req.check(&state.currency())?;
ApiResult::Ok(Json(state.add_incoming_kyc(req).await?))
},
),
@@ -195,7 +215,7 @@ pub fn router<I: WireGateway>(state: Arc<I>, auth: AuthMethod) -> Router {
"/admin/add-mapped",
post(
async |State(state): State<Arc<I>>, Req(req): Req<AddMappedRequest>| {
- state.check_currency(&req.amount)?;
+ req.check(&state.currency())?;
ApiResult::Ok(Json(state.add_incoming_mapped(req).await?))
},
),
@@ -217,7 +237,7 @@ pub fn router<I: WireGateway>(state: Arc<I>, auth: AuthMethod) -> Router {
"/config",
get(async |State(state): State<Arc<I>>| {
Json(WireConfig {
- name: "taler-wire-gateway",
+ name: (),
version: WIRE_GATEWAY_API_VERSION,
currency: state.currency(),
implementation: Some(state.implementation()),
diff --git a/common/taler-api/src/constants.rs b/common/taler-api/src/constants.rs
@@ -14,7 +14,9 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-pub const WIRE_GATEWAY_API_VERSION: &str = "5:0:0";
-pub const PREPARED_TRANSFER_API_VERSION: &str = "1:0:0";
-pub const REVENUE_API_VERSION: &str = "1:0:0";
+use taler_common::api::LibtoolVersion;
+
+pub const WIRE_GATEWAY_API_VERSION: LibtoolVersion = LibtoolVersion::new(5, 0, 0);
+pub const PREPARED_TRANSFER_API_VERSION: LibtoolVersion = LibtoolVersion::new(1, 0, 0);
+pub const REVENUE_API_VERSION: LibtoolVersion = LibtoolVersion::new(1, 0, 0);
pub const MAX_BODY_LENGTH: usize = 4 * 1024; // 4kB
diff --git a/common/taler-api/src/crypto.rs b/common/taler-api/src/crypto.rs
@@ -15,7 +15,7 @@
*/
use aws_lc_rs::signature::{self, Ed25519KeyPair, UnparsedPublicKey};
-use taler_common::api_common::{EddsaPublicKey, EddsaSignature};
+use taler_common::api::{EddsaPublicKey, EddsaSignature};
pub fn check_eddsa_signature(key: &EddsaPublicKey, msg: &[u8], sign: &EddsaSignature) -> bool {
UnparsedPublicKey::new(&signature::ED25519, key.as_ref())
diff --git a/common/taler-api/src/db.rs b/common/taler-api/src/db.rs
@@ -28,8 +28,7 @@ use sqlx::{
query::{Query, QueryScalar},
};
use taler_common::{
- api_common::SafeU64,
- api_params::{History, Page, Pooling},
+ api::params::{History, Page, Pooling},
types::{
amount::{Amount, Currency, Decimal},
iban::IBAN,
@@ -322,9 +321,6 @@ pub trait TypeHelper {
fn try_get_opt_u64<I: sqlx::ColumnIndex<Self>>(&self, index: I) -> sqlx::Result<Option<u64>> {
self.try_get_opt_map(index, |signed: i64| signed.try_into())
}
- fn try_get_safeu64<I: sqlx::ColumnIndex<Self>>(&self, index: I) -> sqlx::Result<SafeU64> {
- self.try_get_map(index, |signed: i64| SafeU64::try_from(signed))
- }
fn try_get_url<I: sqlx::ColumnIndex<Self>>(&self, index: I) -> sqlx::Result<Url> {
self.try_get_parse(index)
}
diff --git a/common/taler-api/src/error.rs b/common/taler-api/src/error.rs
@@ -23,7 +23,9 @@ use axum::{
response::{IntoResponse, Response},
};
use taler_common::{
- api_common::ErrorDetail, api_params::ParamsErr, error_code::ErrorCode, types::payto::PaytoErr,
+ api::{ErrorDetail, params::ParamsErr},
+ error_code::ErrorCode,
+ types::payto::PaytoErr,
};
pub type ApiResult<T> = Result<T, ApiError>;
@@ -125,9 +127,16 @@ impl From<ParamsErr> for ApiError {
impl From<serde_path_to_error::Error<serde_urlencoded::de::Error>> for ApiError {
fn from(value: serde_path_to_error::Error<serde_urlencoded::de::Error>) -> Self {
- failure(ErrorCode::GENERIC_PARAMETER_MALFORMED, value.inner())
- .with_path(value.path().to_string())
- .with_log(value.to_string())
+ let fmt = value.to_string();
+ if fmt.contains("missing") {
+ failure(ErrorCode::GENERIC_PARAMETER_MISSING, value.inner())
+ .with_path(value.path().to_string())
+ .with_log(fmt)
+ } else {
+ failure(ErrorCode::GENERIC_PARAMETER_MALFORMED, value.inner())
+ .with_path(value.path().to_string())
+ .with_log(fmt)
+ }
}
}
diff --git a/common/taler-api/src/extract.rs b/common/taler-api/src/extract.rs
@@ -15,9 +15,13 @@
*/
use axum::{
- body::{Body, Bytes},
+ body::{Body, Bytes, HttpBody},
extract::{FromRequest, FromRequestParts, Request},
- http::{HeaderMap, StatusCode, header, request::Parts},
+ http::{
+ HeaderMap, StatusCode,
+ header::{self, CONTENT_TYPE},
+ request::Parts,
+ },
};
use http_body_util::BodyExt as _;
use serde::de::DeserializeOwned;
@@ -156,6 +160,29 @@ impl<T: DeserializeOwned> TryFrom<&Bytes> for Req<T> {
}
#[derive(Debug, Clone, Copy, Default)]
+#[must_use]
+pub struct OptReq<T>(pub Option<T>);
+
+impl<T, S> FromRequest<S> for OptReq<T>
+where
+ T: DeserializeOwned,
+ S: Send + Sync,
+{
+ type Rejection = ApiError;
+
+ async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
+ let (parts, body) = req.into_parts();
+ if !parts.headers.contains_key(CONTENT_TYPE) && body.size_hint().exact() == Some(0) {
+ Ok(Self(None))
+ } else {
+ let bytes = decompressed_strict_body(&parts.headers, body).await?;
+ let req = Req::try_from(&bytes)?;
+ Ok(Self(Some(req.0)))
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, Default)]
pub struct Path<T: DeserializeOwned + Send>(pub T);
impl<T: serde::de::DeserializeOwned + Send, S: Sync + Send> FromRequestParts<S> for Path<T> {
diff --git a/common/taler-api/src/subject.rs b/common/taler-api/src/subject.rs
@@ -15,14 +15,14 @@
*/
use std::{
- fmt::{Debug, Write as _},
+ fmt::{Debug, Display, Write as _},
str::FromStr,
};
use aws_lc_rs::digest::{SHA256, digest};
use compact_str::CompactString;
use taler_common::{
- api_common::{EddsaPublicKey, ShortHashCode},
+ api::{EddsaPublicKey, ShortHashCode},
db::IncomingType,
encoding::base32::{Base32Error, CROCKFORD_ALPHABET},
types::url,
@@ -156,23 +156,27 @@ pub fn parse_outgoing(subject: &str) -> Result<OutgoingSubject, OutgoingSubjectE
}
/// Format an outgoing subject
-pub fn fmt_out_subject(wtid: &ShortHashCode, url: &Url, metadata: Option<&str>) -> String {
+pub fn fmt_out_subject(
+ wtid: &ShortHashCode,
+ url: impl AsRef<str>,
+ metadata: Option<&str>,
+) -> String {
let mut buf = String::new();
if let Some(metadata) = metadata {
buf.push_str(metadata);
buf.push(' ');
}
- write!(&mut buf, "{wtid} {url}").unwrap();
+ write!(&mut buf, "{wtid} {}", url.as_ref()).unwrap();
buf
}
/// Format an incoming subject
-pub fn fmt_in_subject(ty: IncomingType, key: &EddsaPublicKey) -> String {
- match ty {
- IncomingType::reserve => format!("{key}"),
- IncomingType::kyc => format!("KYC:{key}"),
- IncomingType::map => format!("MAP:{key}"),
- }
+pub fn fmt_in_subject(ty: IncomingType, key: &EddsaPublicKey) -> impl Display {
+ std::fmt::from_fn(move |f| match ty {
+ IncomingType::reserve => write!(f, "{key}"),
+ IncomingType::kyc => write!(f, "KYC:{key}"),
+ IncomingType::map => write!(f, "MAP:{key}"),
+ })
}
/**
@@ -344,7 +348,7 @@ mod test {
use std::str::FromStr as _;
use taler_common::{
- api_common::{EddsaPublicKey, ShortHashCode},
+ api::{EddsaPublicKey, ShortHashCode},
db::IncomingType,
types::url,
};
diff --git a/common/taler-api/src/test.rs b/common/taler-api/src/test.rs
@@ -7,10 +7,12 @@ use axum::{
use serde_json::json;
use sqlx::{PgPool, Row as _, postgres::PgRow};
use taler_common::{
- api_common::{HashCode, ShortHashCode},
- api_revenue::RevenueConfig,
- api_transfer::PreparedTransferConfig,
- api_wire::{OutgoingHistory, TransferRequest, TransferResponse, TransferState, WireConfig},
+ api::{
+ HashCode, ShortHashCode,
+ prepared::PreparedTransferConfig,
+ revenue::RevenueConfig,
+ wire::{TransferRequest, TransferResponse, TransferState, WireConfig},
+ },
db::IncomingType,
error_code::ErrorCode,
types::{
@@ -23,8 +25,8 @@ use taler_common::{
use taler_test_utils::{
db::db_test_setup_manual,
routine::{
- Status, admin_add_incoming_routine, in_history_routine, registration_routine,
- revenue_routine, routine_pagination, transfer_routine,
+ Status, admin_add_incoming_routine, in_history_routine, out_history_routine,
+ registration_routine, revenue_routine, transfer_routine,
},
server::TestServer,
tasks,
@@ -194,11 +196,9 @@ async fn transfer() {
#[tokio::test]
async fn outgoing_history() {
- let (server, _) = setup().await;
- let server = &server;
- routine_pagination::<OutgoingHistory>(
+ let (server, _) = &setup().await;
+ out_history_routine(
server,
- "/taler-wire-gateway/history/outgoing",
tasks!({
server
.post("/taler-wire-gateway/transfer")
@@ -212,6 +212,7 @@ async fn outgoing_history() {
.await
.assert_ok_json::<TransferResponse>();
}),
+ tasks!(),
)
.await;
}
@@ -225,13 +226,13 @@ async fn admin_add_incoming() {
#[tokio::test]
async fn in_history() {
let (server, _) = setup().await;
- in_history_routine(&server, &PAYTO, true).await;
+ in_history_routine(&server, &PAYTO, true, tasks!(), tasks!()).await;
}
#[tokio::test]
async fn revenue() {
let (server, _) = setup().await;
- revenue_routine(&server, &PAYTO, true).await;
+ revenue_routine(&server, &PAYTO, true, tasks!(), tasks!()).await;
}
#[tokio::test]
diff --git a/common/taler-api/src/test/api.rs b/common/taler-api/src/test/api.rs
@@ -17,15 +17,15 @@
use jiff::Timestamp;
use sqlx::PgPool;
use taler_common::{
- api_params::{History, Page},
- api_revenue::RevenueIncomingHistory,
- api_transfer::{
- RegistrationRequest, RegistrationResponse, SubjectFormat, TransferSubject, Unregistration,
- },
- api_wire::{
- AddIncomingRequest, AddIncomingResponse, AddKycauthRequest, AddMappedRequest,
- IncomingHistory, OutgoingHistory, TransferList, TransferRequest, TransferResponse,
- TransferState, TransferStatus,
+ api::{
+ params::{History, Page},
+ prepared::{RegistrationRequest, RegistrationResponse, SubjectFormat, Unregistration},
+ revenue::RevenueIncomingHistory,
+ wire::{
+ AddIncomingRequest, AddIncomingResponse, AddKycauthRequest, AddMappedRequest,
+ IncomingHistory, OutgoingHistory, TransferList, TransferRequest, TransferResponse,
+ TransferState, TransferStatus,
+ },
},
db::IncomingType,
error_code::ErrorCode,
@@ -38,9 +38,13 @@ use taler_common::{
use tokio::sync::watch::Sender;
use crate::{
- api::{TalerApi, revenue::Revenue, transfer::PreparedTransfer, wire::WireGateway},
- error::{ApiResult, failure, failure_code},
- subject::fmt_in_subject,
+ api::{
+ TalerApi,
+ prepared::{PreparedTransfer, simple_subject},
+ revenue::Revenue,
+ wire::WireGateway,
+ },
+ error::{ApiResult, failure_code},
test::db::{self, AddIncomingResult},
};
@@ -53,8 +57,8 @@ pub struct TestApi {
}
impl TalerApi for TestApi {
- fn currency(&self) -> &str {
- self.currency.as_ref()
+ fn currency(&self) -> Currency {
+ self.currency
}
fn implementation(&self) -> &'static str {
@@ -220,34 +224,17 @@ impl PreparedTransfer for TestApi {
async fn registration(&self, req: RegistrationRequest) -> ApiResult<RegistrationResponse> {
match db::transfer_register(&self.pool, &req).await? {
- db::RegistrationResult::Success => {
- let simple = TransferSubject::Simple {
- credit_amount: req.credit_amount,
- subject: if req.authorization_pub == req.account_pub && !req.recurrent {
- fmt_in_subject(req.r#type.into(), &req.account_pub)
- } else {
- fmt_in_subject(IncomingType::map, &req.authorization_pub)
- },
- };
- ApiResult::Ok(RegistrationResponse {
- subjects: vec![simple],
- expiration: TalerTimestamp::Never,
- })
- }
+ db::RegistrationResult::Success => ApiResult::Ok(RegistrationResponse {
+ subjects: vec![simple_subject(req)],
+ expiration: TalerTimestamp::Never,
+ }),
db::RegistrationResult::ReservePubReuse => {
ApiResult::Err(failure_code(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT))
}
}
}
- async fn unregistration(&self, req: Unregistration) -> ApiResult<()> {
- if !db::transfer_unregister(&self.pool, &req).await? {
- Err(failure(
- ErrorCode::BANK_TRANSACTION_NOT_FOUND,
- format!("Prepared transfer '{}' not found", req.authorization_pub),
- ))
- } else {
- Ok(())
- }
+ async fn unregistration(&self, req: Unregistration) -> ApiResult<bool> {
+ Ok(db::transfer_unregister(&self.pool, &req).await?)
}
}
diff --git a/common/taler-api/src/test/db.rs b/common/taler-api/src/test/db.rs
@@ -17,13 +17,15 @@
use jiff::Timestamp;
use sqlx::{PgPool, QueryBuilder, Row, postgres::PgRow};
use taler_common::{
- api_common::{EddsaPublicKey, SafeU64},
- api_params::{History, Page},
- api_revenue::RevenueIncomingBankTransaction,
- api_transfer::{RegistrationRequest, Unregistration},
- api_wire::{
- IncomingBankTransaction, OutgoingBankTransaction, TransferListStatus, TransferRequest,
- TransferResponse, TransferState, TransferStatus,
+ api::{
+ EddsaPublicKey,
+ params::{History, Page},
+ prepared::{RegistrationRequest, Unregistration},
+ revenue::RevenueIncomingBankTransaction,
+ wire::{
+ IncomingBankTransaction, OutgoingBankTransaction, TransferListStatus, TransferRequest,
+ TransferResponse, TransferState, TransferStatus,
+ },
},
db::IncomingType,
types::{
@@ -84,7 +86,7 @@ pub async fn transfer(db: &PgPool, req: &TransferRequest) -> sqlx::Result<Transf
TransferResult::WtidReuse
} else {
TransferResult::Success(TransferResponse {
- row_id: r.try_get_safeu64("out_transfer_row_id")?,
+ row_id: r.try_get_u64("out_transfer_row_id")?,
timestamp: r.try_get_timestamp("out_created_at")?.into(),
})
})
@@ -124,7 +126,7 @@ pub async fn transfer_page(
},
|r: PgRow| {
Ok(TransferListStatus {
- row_id: r.try_get_safeu64("transfer_id")?,
+ row_id: r.try_get_u64("transfer_id")?,
status: r.try_get("status")?,
amount: r.try_get_amount("amount", currency)?,
credit_account: r.try_get_payto("credit_payto")?,
@@ -208,7 +210,7 @@ pub async fn outgoing_revenue(
debit_fee: None,
wtid: r.try_get("wtid")?,
credit_account: r.try_get_payto("credit_payto")?,
- row_id: r.try_get_safeu64("transfer_id")?,
+ row_id: r.try_get_u64("transfer_id")?,
date: r.try_get_timestamp("created_at")?.into(),
exchange_base_url: r.try_get_url("exchange_base_url")?,
metadata: r.try_get("metadata")?,
@@ -219,7 +221,7 @@ pub async fn outgoing_revenue(
}
pub enum AddIncomingResult {
- Success { id: SafeU64, created_at: Timestamp },
+ Success { id: u64, created_at: Timestamp },
ReservePubReuse,
UnknownMapping,
MappingReuse,
@@ -254,7 +256,7 @@ pub async fn add_incoming(
AddIncomingResult::UnknownMapping
} else{
AddIncomingResult::Success {
- id: r.try_get_safeu64("out_tx_row_id")?,
+ id: r.try_get_u64("out_tx_row_id")?,
created_at: r.try_get_timestamp("out_created_at")?,
}
})
@@ -295,7 +297,7 @@ pub async fn incoming_history(
|r: PgRow| {
Ok(match r.try_get("type")? {
IncomingType::reserve => IncomingBankTransaction::Reserve {
- row_id: r.try_get_safeu64("tx_in_id")?,
+ row_id: r.try_get_u64("tx_in_id")?,
date: r.try_get_timestamp("created_at")?.into(),
amount: r.try_get_amount("amount", currency)?,
credit_fee: None,
@@ -305,7 +307,7 @@ pub async fn incoming_history(
authorization_sig: r.try_get("authorization_sig")?,
},
IncomingType::kyc => IncomingBankTransaction::Kyc {
- row_id: r.try_get_safeu64("tx_in_id")?,
+ row_id: r.try_get_u64("tx_in_id")?,
date: r.try_get_timestamp("created_at")?.into(),
amount: r.try_get_amount("amount", currency)?,
credit_fee: None,
@@ -348,7 +350,7 @@ pub async fn revenue_history(
},
|r: PgRow| {
Ok(RevenueIncomingBankTransaction {
- row_id: r.try_get_safeu64("tx_in_id")?,
+ row_id: r.try_get_u64("tx_in_id")?,
date: r.try_get_timestamp("created_at")?.into(),
amount: r.try_get_amount("amount", currency)?,
credit_fee: None,
diff --git a/common/taler-common/src/api.rs b/common/taler-common/src/api.rs
@@ -0,0 +1,248 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2024, 2025, 2026 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+
+use std::{borrow::Cow, fmt::Display, num::ParseIntError, ops::Deref, str::FromStr};
+
+use aws_lc_rs::{
+ error::KeyRejected,
+ signature::{self, Ed25519KeyPair, KeyPair as _, ParsedPublicKey},
+};
+use serde::{Deserialize, Deserializer, Serialize};
+use serde_json::value::RawValue;
+
+use crate::{encoding::base32::Base32Error, types::base32::Base32};
+
+pub mod params;
+pub mod prepared;
+pub mod revenue;
+pub mod wire;
+
+#[derive(
+ Debug,
+ Clone,
+ Copy,
+ PartialEq,
+ Eq,
+ PartialOrd,
+ Ord,
+ Hash,
+ serde_with::DeserializeFromStr,
+ serde_with::SerializeDisplay,
+)]
+pub struct LibtoolVersion {
+ pub current: u32,
+ pub revision: u32,
+ pub age: u32,
+}
+
+impl LibtoolVersion {
+ pub const fn new(current: u32, revision: u32, age: u32) -> Self {
+ assert!(age <= current);
+ Self {
+ current,
+ revision,
+ age,
+ }
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum LibtoolVersionError {
+ #[error("age exceeds current")]
+ AgeExceedsCurrent,
+ #[error("invalid format")]
+ InvalidFormat,
+ #[error(transparent)]
+ ParseIntError(#[from] ParseIntError),
+}
+
+impl FromStr for LibtoolVersion {
+ type Err = LibtoolVersionError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let mut parts = s.split(':');
+
+ let current = parts
+ .next()
+ .ok_or(LibtoolVersionError::InvalidFormat)?
+ .parse::<u32>()?;
+
+ let revision = match parts.next() {
+ Some(p) => p.parse::<u32>()?,
+ None => 0,
+ };
+
+ let age = match parts.next() {
+ Some(p) => p.parse::<u32>()?,
+ None => 0,
+ };
+
+ if parts.next().is_some() {
+ return Err(LibtoolVersionError::InvalidFormat);
+ }
+
+ Ok(Self::new(current, revision, age))
+ }
+}
+
+impl Display for LibtoolVersion {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}:{}:{}", self.current, self.revision, self.age)
+ }
+}
+
+/// <https://docs.taler.net/core/api-common.html#tsref-type-ErrorDetail>
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ErrorDetail {
+ pub code: u16,
+ pub hint: Option<Box<str>>,
+ pub detail: Option<Box<str>>,
+ pub parameter: Option<Box<str>>,
+ pub path: Option<Box<str>>,
+ pub offset: Option<Box<str>>,
+ pub index: Option<Box<str>>,
+ pub object: Option<Box<str>>,
+ pub currency: Option<Box<str>>,
+ pub type_expected: Option<Box<str>>,
+ pub type_actual: Option<Box<str>>,
+ pub extra: Option<Box<RawValue>>,
+}
+
+/// 64-byte hash code
+pub type HashCode = Base32<64>;
+/// 32-bytes hash code
+pub type ShortHashCode = Base32<32>;
+pub type WadId = Base32<24>;
+pub type EddsaSignature = Base32<64>;
+
+/// EdDSA and ECDHE public keys always point on Curve25519
+/// and represented using the standard 256 bits Ed25519 compact format,
+/// converted to Crockford Base32.
+#[derive(Clone, PartialEq, Eq)]
+pub struct EddsaPublicKey(Base32<32>);
+
+impl Deref for EddsaPublicKey {
+ type Target = Base32<32>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl Serialize for EddsaPublicKey {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ self.0.serialize(serializer)
+ }
+}
+
+impl<'de> Deserialize<'de> for EddsaPublicKey {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let raw = Cow::<str>::deserialize(deserializer)?;
+ Self::from_str(&raw).map_err(serde::de::Error::custom)
+ }
+}
+
+impl Display for EddsaPublicKey {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+impl std::fmt::Debug for EddsaPublicKey {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ Display::fmt(&self.0, f)
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum EddsaPublicKeyError {
+ #[error(transparent)]
+ Base32(#[from] Base32Error<32>),
+ #[error(transparent)]
+ Invalid(#[from] KeyRejected),
+}
+
+impl FromStr for EddsaPublicKey {
+ type Err = EddsaPublicKeyError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let encoded = Base32::<32>::from_str(s)?;
+ Self::try_from(encoded)
+ }
+}
+
+impl TryFrom<&[u8]> for EddsaPublicKey {
+ type Error = EddsaPublicKeyError;
+
+ fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
+ let encoded = Base32::try_from(value)?;
+ Self::try_from(encoded)
+ }
+}
+
+impl TryFrom<[u8; 32]> for EddsaPublicKey {
+ type Error = EddsaPublicKeyError;
+
+ fn try_from(value: [u8; 32]) -> Result<Self, Self::Error> {
+ let encoded = Base32::from(value);
+ Self::try_from(encoded)
+ }
+}
+
+impl TryFrom<Base32<32>> for EddsaPublicKey {
+ type Error = EddsaPublicKeyError;
+
+ fn try_from(value: Base32<32>) -> Result<Self, Self::Error> {
+ ParsedPublicKey::new(&signature::ED25519, value.as_ref())?;
+ Ok(Self(value))
+ }
+}
+
+impl EddsaPublicKey {
+ pub fn rand() -> EddsaPublicKey {
+ let signing_key = Ed25519KeyPair::generate().unwrap();
+ let bytes: [u8; 32] = signing_key.public_key().as_ref().try_into().unwrap();
+ Self(Base32::from(bytes))
+ }
+}
+
+impl sqlx::Type<sqlx::Postgres> for EddsaPublicKey {
+ fn type_info() -> sqlx::postgres::PgTypeInfo {
+ <Base32<32>>::type_info()
+ }
+}
+
+impl<'q> sqlx::Encode<'q, sqlx::Postgres> for EddsaPublicKey {
+ fn encode_by_ref(
+ &self,
+ buf: &mut sqlx::postgres::PgArgumentBuffer,
+ ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
+ self.0.encode_by_ref(buf)
+ }
+}
+
+impl<'r> sqlx::Decode<'r, sqlx::Postgres> for EddsaPublicKey {
+ fn decode(value: sqlx::postgres::PgValueRef<'r>) -> Result<Self, sqlx::error::BoxDynError> {
+ let raw = <Base32<32>>::decode(value)?;
+ Ok(Self(raw))
+ }
+}
diff --git a/common/taler-common/src/api/params.rs b/common/taler-common/src/api/params.rs
@@ -0,0 +1,171 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2024, 2025, 2026 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+
+use serde::Deserialize;
+use serde_with::{DisplayFromStr, serde_as};
+
+use crate::{api::wire::TransferState, types::payto::PaytoURI};
+
+#[derive(Debug, thiserror::Error)]
+#[error("Param '{param}' {reason}")]
+pub struct ParamsErr {
+ pub param: &'static str,
+ pub reason: String,
+}
+
+pub fn param_err(param: &'static str, reason: String) -> ParamsErr {
+ ParamsErr { param, reason }
+}
+
+#[serde_as]
+#[derive(Debug, Clone, Deserialize)]
+/// <https://docs.taler.net/core/api-common.html#row-id-pagination>
+pub struct PageParams {
+ #[serde_as(as = "Option<DisplayFromStr>")]
+ #[serde(alias = "delta")]
+ pub limit: Option<i64>,
+ #[serde_as(as = "Option<DisplayFromStr>")]
+ #[serde(alias = "start")]
+ pub offset: Option<i64>,
+}
+
+impl PageParams {
+ const MAX_PAGE_SIZE: i64 = 1024;
+
+ pub fn check(self) -> Result<Page, ParamsErr> {
+ Self::check_custom(self, Self::MAX_PAGE_SIZE)
+ }
+
+ pub fn check_custom(self, max_page_size: i64) -> Result<Page, ParamsErr> {
+ let limit = self.limit.unwrap_or(-20);
+ if limit == 0 {
+ return Err(param_err("limit", format!("must be non-zero got {limit}")));
+ } else if limit > max_page_size {
+ return Err(param_err(
+ "limit",
+ format!("must be <= {max_page_size} for {limit}"),
+ ));
+ }
+ if let Some(offset) = self.offset
+ && offset < 0
+ {
+ return Err(param_err(
+ "offset",
+ format!("must be positive got {offset}"),
+ ));
+ }
+
+ Ok(Page {
+ limit,
+ offset: self.offset,
+ })
+ }
+}
+
+#[derive(Debug)]
+pub struct Page {
+ pub limit: i64,
+ pub offset: Option<i64>,
+}
+
+impl Default for Page {
+ fn default() -> Self {
+ Self {
+ limit: 20,
+ offset: None,
+ }
+ }
+}
+
+impl Page {
+ pub fn backward(&self) -> bool {
+ self.limit < 0
+ }
+}
+
+#[serde_as]
+#[derive(Debug, Clone, Deserialize)]
+/// <https://docs.taler.net/core/api-common.html#long-polling>
+pub struct PoolingParams {
+ #[serde_as(as = "Option<DisplayFromStr>")]
+ #[serde(alias = "long_poll_ms")]
+ pub timeout_ms: Option<u64>,
+}
+
+#[derive(Debug, Default)]
+pub struct Pooling {
+ pub timeout_ms: Option<u64>,
+}
+
+impl PoolingParams {
+ pub const MAX_TIMEOUT_MS: u64 = 60 * 60 * 10; // 1H
+
+ pub fn check(self) -> Result<Pooling, ParamsErr> {
+ Self::check_custom(self, Self::MAX_TIMEOUT_MS)
+ }
+
+ pub fn check_custom(self, max_timeout_ms: u64) -> Result<Pooling, ParamsErr> {
+ let timeout_ms = self.timeout_ms.map(|it| it.min(max_timeout_ms));
+ Ok(Pooling { timeout_ms })
+ }
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct HistoryParams {
+ #[serde(flatten)]
+ pub pagination: PageParams,
+ #[serde(flatten)]
+ pub pooling: PoolingParams,
+}
+
+impl HistoryParams {
+ pub fn check(self) -> Result<History, ParamsErr> {
+ Self::check_custom(
+ self,
+ PageParams::MAX_PAGE_SIZE,
+ PoolingParams::MAX_TIMEOUT_MS,
+ )
+ }
+
+ pub fn check_custom(
+ self,
+ max_page_size: i64,
+ max_timeout_ms: u64,
+ ) -> Result<History, ParamsErr> {
+ Ok(History {
+ page: self.pagination.check_custom(max_page_size)?,
+ pooling: self.pooling.check_custom(max_timeout_ms)?,
+ })
+ }
+}
+
+#[derive(Debug, Default)]
+pub struct History {
+ pub page: Page,
+ pub pooling: Pooling,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct TransferParams {
+ #[serde(flatten)]
+ pub pagination: PageParams,
+ pub status: Option<TransferState>,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct AccountParams {
+ pub account: PaytoURI,
+}
diff --git a/common/taler-common/src/api/prepared.rs b/common/taler-common/src/api/prepared.rs
@@ -0,0 +1,115 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2026 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+
+//! Type for the Taler Wire Transfer Gateway HTTP API <https://docs.taler.net/core/api-bank-transfer.html#taler-prepared-transfer-http-api>
+
+use compact_str::CompactString;
+use serde::{Deserialize, Serialize};
+use taler_macros::api_config;
+use url::Url;
+
+use super::EddsaPublicKey;
+use crate::{
+ api::EddsaSignature,
+ db::IncomingType,
+ types::{
+ amount::{Amount, Currency},
+ timestamp::TalerTimestamp,
+ },
+};
+
+/// <https://docs.taler.net/core/api-bank-transfer.html#tsref-type-SubjectFormat>
+#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
+#[allow(non_camel_case_types)]
+pub enum SubjectFormat {
+ SIMPLE,
+ URI,
+ CH_QR_BILL,
+}
+
+/// <https://docs.taler.net/core/api-bank-transfer.html#tsref-type-PreparedTransferConfig>
+#[api_config("taler-prepared-transfer")]
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct PreparedTransferConfig<'a> {
+ pub currency: Currency,
+ pub supported_formats: Vec<SubjectFormat>,
+}
+
+#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
+#[allow(non_camel_case_types)]
+pub enum TransferType {
+ reserve,
+ kyc,
+}
+
+impl From<TransferType> for IncomingType {
+ fn from(value: TransferType) -> Self {
+ match value {
+ TransferType::reserve => IncomingType::reserve,
+ TransferType::kyc => IncomingType::kyc,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
+pub enum PublicKeyAlg {
+ EdDSA,
+}
+
+/// <https://docs.taler.net/core/api-bank-transfer.html#tsref-type-RegistrationRequest>
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct RegistrationRequest {
+ pub credit_amount: Amount,
+ pub r#type: TransferType,
+ pub alg: PublicKeyAlg,
+ pub account_pub: EddsaPublicKey,
+ pub authorization_pub: EddsaPublicKey,
+ pub authorization_sig: EddsaSignature,
+ pub recurrent: bool,
+}
+
+/// <https://docs.taler.net/core/api-bank-transfer.html#tsref-type-TransferSubject>
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(tag = "type")]
+pub enum TransferSubject {
+ #[serde(rename = "SIMPLE")]
+ Simple {
+ credit_amount: Amount,
+ subject: String,
+ },
+ #[serde(rename = "URI")]
+ Uri { credit_amount: Amount, uri: Url },
+ #[serde(rename = "CH_QR_BILL")]
+ QrBill {
+ credit_amount: Amount,
+ qr_reference_number: String,
+ },
+}
+
+/// <https://docs.taler.net/core/api-bank-transfer.html#tsref-type-RegistrationResponse>
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct RegistrationResponse {
+ pub subjects: Vec<TransferSubject>,
+ pub expiration: TalerTimestamp,
+}
+
+/// <https://docs.taler.net/core/api-bank-transfer.html#tsref-type-Unregistration>
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Unregistration {
+ pub timestamp: CompactString,
+ pub authorization_pub: EddsaPublicKey,
+ pub authorization_sig: EddsaSignature,
+}
diff --git a/common/taler-common/src/api/revenue.rs b/common/taler-common/src/api/revenue.rs
@@ -0,0 +1,51 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025, 2026 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+
+//! Type for the Taler Wire Gateway HTTP API <https://docs.taler.net/core/api-bank-wire.html#taler-wire-gateway-http-api>
+
+use serde::{Deserialize, Serialize};
+use taler_macros::api_config;
+
+use crate::types::{
+ amount::{Amount, Currency},
+ payto::PaytoURI,
+ timestamp::TalerTimestamp,
+};
+
+/// <https://docs.taler.net/core/api-bank-revenue.html#tsref-type-RevenueConfig>
+#[api_config("taler-revenue")]
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct RevenueConfig<'a> {
+ pub currency: Currency,
+}
+
+/// <https://docs.taler.net/core/api-bank-revenue.html#tsref-type-RevenueIncomingHistory>
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct RevenueIncomingHistory {
+ pub incoming_transactions: Vec<RevenueIncomingBankTransaction>,
+ pub credit_account: PaytoURI,
+}
+
+/// <https://docs.taler.net/core/api-bank-revenue.html#tsref-type-RevenueIncomingBankTransaction>
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct RevenueIncomingBankTransaction {
+ pub row_id: u64,
+ pub date: TalerTimestamp,
+ pub amount: Amount,
+ pub credit_fee: Option<Amount>,
+ 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
@@ -0,0 +1,210 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2024, 2025, 2026 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+
+//! Type for the Taler Wire Gateway HTTP API <https://docs.taler.net/core/api-bank-wire.html#taler-wire-gateway-http-api>
+
+use compact_str::CompactString;
+use serde::{Deserialize, Serialize};
+use taler_macros::api_config;
+use url::Url;
+
+use super::{EddsaPublicKey, HashCode, ShortHashCode, WadId};
+use crate::{
+ api::EddsaSignature,
+ types::{
+ amount::{Amount, Currency},
+ payto::PaytoURI,
+ timestamp::TalerTimestamp,
+ },
+};
+
+/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-WireConfig>
+#[api_config("taler-wire-gateway")]
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct WireConfig<'a> {
+ pub currency: Currency,
+ pub support_account_check: bool,
+}
+
+/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-TransferResponse>
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct TransferResponse {
+ pub timestamp: TalerTimestamp,
+ pub row_id: u64,
+}
+
+/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-TransferRequest>
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct TransferRequest {
+ pub request_uid: HashCode,
+ pub amount: Amount,
+ pub exchange_base_url: Url,
+ pub metadata: Option<CompactString>,
+ pub wtid: ShortHashCode,
+ 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: PaytoURI,
+}
+
+/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-TransferListStatus>
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct TransferListStatus {
+ pub row_id: u64,
+ pub status: TransferState,
+ pub amount: Amount,
+ pub credit_account: PaytoURI,
+ pub timestamp: TalerTimestamp,
+}
+
+/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-TransfertSatus>
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct TransferStatus {
+ pub status: TransferState,
+ pub status_msg: Option<String>,
+ pub amount: Amount,
+ pub origin_exchange_url: String,
+ pub metadata: Option<CompactString>,
+ pub wtid: ShortHashCode,
+ pub credit_account: PaytoURI,
+ pub timestamp: TalerTimestamp,
+}
+
+/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-OutgoingHistory>
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct OutgoingHistory {
+ pub outgoing_transactions: Vec<OutgoingBankTransaction>,
+ pub debit_account: PaytoURI,
+}
+
+/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-OutgoingBankTransaction>
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct OutgoingBankTransaction {
+ pub row_id: u64,
+ pub date: TalerTimestamp,
+ pub amount: Amount,
+ pub debit_fee: Option<Amount>,
+ pub credit_account: PaytoURI,
+ pub wtid: ShortHashCode,
+ pub exchange_base_url: Url,
+ pub metadata: Option<CompactString>,
+}
+
+/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-IncomingHistory>
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct IncomingHistory {
+ pub credit_account: PaytoURI,
+ pub incoming_transactions: Vec<IncomingBankTransaction>,
+}
+
+/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-IncomingBankTransaction>
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(tag = "type")]
+pub enum IncomingBankTransaction {
+ #[serde(rename = "RESERVE")]
+ Reserve {
+ row_id: u64,
+ date: TalerTimestamp,
+ amount: Amount,
+ credit_fee: Option<Amount>,
+ debit_account: PaytoURI,
+ reserve_pub: EddsaPublicKey,
+ authorization_pub: Option<EddsaPublicKey>,
+ authorization_sig: Option<EddsaSignature>,
+ },
+ #[serde(rename = "WAD")]
+ Wad {
+ row_id: u64,
+ date: TalerTimestamp,
+ amount: Amount,
+ debit_account: PaytoURI,
+ origin_exchange_url: Url,
+ wad_id: WadId,
+ },
+ #[serde(rename = "KYCAUTH")]
+ Kyc {
+ row_id: u64,
+ date: TalerTimestamp,
+ amount: Amount,
+ credit_fee: Option<Amount>,
+ debit_account: PaytoURI,
+ account_pub: EddsaPublicKey,
+ authorization_pub: Option<EddsaPublicKey>,
+ authorization_sig: Option<EddsaSignature>,
+ },
+}
+
+/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-AddIncomingRequest>
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct AddIncomingRequest {
+ pub amount: Amount,
+ pub reserve_pub: EddsaPublicKey,
+ pub debit_account: PaytoURI,
+}
+
+/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-AddIncomingResponse>
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct AddIncomingResponse {
+ pub row_id: u64,
+ pub timestamp: TalerTimestamp,
+}
+
+/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-AddKycauthRequest>
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct AddKycauthRequest {
+ pub amount: Amount,
+ pub account_pub: EddsaPublicKey,
+ pub debit_account: PaytoURI,
+}
+
+/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-AddMappedRequest>
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct AddMappedRequest {
+ pub amount: Amount,
+ pub authorization_pub: EddsaPublicKey,
+ pub debit_account: PaytoURI,
+}
+
+/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-AccountInfo>
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct AccountInfo {}
+
+#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, sqlx::Type)]
+#[allow(non_camel_case_types)]
+#[sqlx(type_name = "transfer_status")]
+pub enum TransferState {
+ pending,
+ transient_failure,
+ permanent_failure,
+ late_failure,
+ success,
+}
+
+impl AsRef<str> for TransferState {
+ fn as_ref(&self) -> &str {
+ match self {
+ TransferState::pending => "pending",
+ TransferState::transient_failure => "transient_failure",
+ TransferState::permanent_failure => "permanent_failure",
+ TransferState::late_failure => "late_failure",
+ TransferState::success => "success",
+ }
+ }
+}
diff --git a/common/taler-common/src/api_common.rs b/common/taler-common/src/api_common.rs
@@ -1,240 +0,0 @@
-/*
- This file is part of TALER
- Copyright (C) 2024, 2026 Taler Systems SA
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU Affero General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-*/
-
-use std::{borrow::Cow, fmt::Display, ops::Deref, str::FromStr};
-
-use aws_lc_rs::{
- error::KeyRejected,
- signature::{self, Ed25519KeyPair, KeyPair, ParsedPublicKey},
-};
-use serde::{Deserialize, Deserializer, Serialize, de::Error};
-use serde_json::value::RawValue;
-
-use crate::{encoding::base32::Base32Error, types::base32::Base32};
-
-/// <https://docs.taler.net/core/api-common.html#tsref-type-ErrorDetail>
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct ErrorDetail {
- pub code: u16,
- pub hint: Option<Box<str>>,
- pub detail: Option<Box<str>>,
- pub parameter: Option<Box<str>>,
- pub path: Option<Box<str>>,
- pub offset: Option<Box<str>>,
- pub index: Option<Box<str>>,
- pub object: Option<Box<str>>,
- pub currency: Option<Box<str>>,
- pub type_expected: Option<Box<str>>,
- pub type_actual: Option<Box<str>>,
- pub extra: Option<Box<RawValue>>,
-}
-
-pub fn safe_u64(nb: u64) -> SafeU64 {
- SafeU64::try_from(nb).expect("invalid safe u64")
-}
-
-/// <https://docs.taler.net/core/api-common.html#tsref-type-SafeUint64>
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
-pub struct SafeU64(u64);
-
-impl Deref for SafeU64 {
- type Target = u64;
-
- fn deref(&self) -> &Self::Target {
- &self.0
- }
-}
-
-#[derive(Debug, thiserror::Error)]
-pub enum SafeU64Error {
- #[error("{0} unsafe, {0} > (2^53 - 1)")]
- Unsafe(u64),
- #[error("{0} is negative")]
- Negative(i64),
-}
-
-impl TryFrom<u64> for SafeU64 {
- type Error = SafeU64Error;
-
- fn try_from(nb: u64) -> Result<Self, Self::Error> {
- if nb < (1 << 53) - 1 {
- Ok(SafeU64(nb))
- } else {
- Err(SafeU64Error::Unsafe(nb))
- }
- }
-}
-
-impl TryFrom<i64> for SafeU64 {
- type Error = SafeU64Error;
-
- fn try_from(nb: i64) -> Result<Self, Self::Error> {
- u64::try_from(nb)
- .map_err(|_| SafeU64Error::Negative(nb))
- .and_then(|it| it.try_into())
- }
-}
-
-impl TryFrom<i32> for SafeU64 {
- type Error = SafeU64Error;
-
- fn try_from(nb: i32) -> Result<Self, Self::Error> {
- u64::try_from(nb)
- .map_err(|_| SafeU64Error::Negative(nb as i64))
- .and_then(|it| it.try_into())
- }
-}
-
-impl<'de> Deserialize<'de> for SafeU64 {
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
- where
- D: Deserializer<'de>,
- {
- SafeU64::try_from(u64::deserialize(deserializer)?).map_err(D::Error::custom)
- }
-}
-
-impl Display for SafeU64 {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- self.0.fmt(f)
- }
-}
-
-/// 64-byte hash code
-pub type HashCode = Base32<64>;
-/// 32-bytes hash code
-pub type ShortHashCode = Base32<32>;
-pub type WadId = Base32<24>;
-pub type EddsaSignature = Base32<64>;
-
-/// EdDSA and ECDHE public keys always point on Curve25519
-/// and represented using the standard 256 bits Ed25519 compact format,
-/// converted to Crockford Base32.
-#[derive(Clone, PartialEq, Eq)]
-pub struct EddsaPublicKey(Base32<32>);
-
-impl Deref for EddsaPublicKey {
- type Target = Base32<32>;
-
- fn deref(&self) -> &Self::Target {
- &self.0
- }
-}
-
-impl Serialize for EddsaPublicKey {
- fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
- where
- S: serde::Serializer,
- {
- self.0.serialize(serializer)
- }
-}
-
-impl<'de> Deserialize<'de> for EddsaPublicKey {
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
- where
- D: Deserializer<'de>,
- {
- let raw = Cow::<str>::deserialize(deserializer)?;
- Self::from_str(&raw).map_err(D::Error::custom)
- }
-}
-
-impl Display for EddsaPublicKey {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- self.0.fmt(f)
- }
-}
-
-impl std::fmt::Debug for EddsaPublicKey {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- Display::fmt(&self.0, f)
- }
-}
-
-#[derive(Debug, thiserror::Error)]
-pub enum EddsaPublicKeyError {
- #[error(transparent)]
- Base32(#[from] Base32Error<32>),
- #[error(transparent)]
- Invalid(#[from] KeyRejected),
-}
-
-impl FromStr for EddsaPublicKey {
- type Err = EddsaPublicKeyError;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- let encoded = Base32::<32>::from_str(s)?;
- Self::try_from(encoded)
- }
-}
-
-impl TryFrom<&[u8]> for EddsaPublicKey {
- type Error = EddsaPublicKeyError;
-
- fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
- let encoded = Base32::try_from(value)?;
- Self::try_from(encoded)
- }
-}
-
-impl TryFrom<[u8; 32]> for EddsaPublicKey {
- type Error = EddsaPublicKeyError;
-
- fn try_from(value: [u8; 32]) -> Result<Self, Self::Error> {
- let encoded = Base32::from(value);
- Self::try_from(encoded)
- }
-}
-
-impl TryFrom<Base32<32>> for EddsaPublicKey {
- type Error = EddsaPublicKeyError;
-
- fn try_from(value: Base32<32>) -> Result<Self, Self::Error> {
- ParsedPublicKey::new(&signature::ED25519, value.as_ref())?;
- Ok(Self(value))
- }
-}
-
-impl EddsaPublicKey {
- pub fn rand() -> EddsaPublicKey {
- let signing_key = Ed25519KeyPair::generate().unwrap();
- let bytes: [u8; 32] = signing_key.public_key().as_ref().try_into().unwrap();
- Self(Base32::from(bytes))
- }
-}
-
-impl sqlx::Type<sqlx::Postgres> for EddsaPublicKey {
- fn type_info() -> sqlx::postgres::PgTypeInfo {
- <Base32<32>>::type_info()
- }
-}
-
-impl<'q> sqlx::Encode<'q, sqlx::Postgres> for EddsaPublicKey {
- fn encode_by_ref(
- &self,
- buf: &mut sqlx::postgres::PgArgumentBuffer,
- ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
- self.0.encode_by_ref(buf)
- }
-}
-
-impl<'r> sqlx::Decode<'r, sqlx::Postgres> for EddsaPublicKey {
- fn decode(value: sqlx::postgres::PgValueRef<'r>) -> Result<Self, sqlx::error::BoxDynError> {
- let raw = <Base32<32>>::decode(value)?;
- Ok(Self(raw))
- }
-}
diff --git a/common/taler-common/src/api_params.rs b/common/taler-common/src/api_params.rs
@@ -1,171 +0,0 @@
-/*
- This file is part of TALER
- Copyright (C) 2024, 2025, 2026 Taler Systems SA
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU Affero General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-*/
-
-use serde::Deserialize;
-use serde_with::{DisplayFromStr, serde_as};
-
-use crate::{api_wire::TransferState, types::payto::PaytoURI};
-
-#[derive(Debug, thiserror::Error)]
-#[error("Param '{param}' {reason}")]
-pub struct ParamsErr {
- pub param: &'static str,
- pub reason: String,
-}
-
-pub fn param_err(param: &'static str, reason: String) -> ParamsErr {
- ParamsErr { param, reason }
-}
-
-#[serde_as]
-#[derive(Debug, Clone, Deserialize)]
-/// <https://docs.taler.net/core/api-common.html#row-id-pagination>
-pub struct PageParams {
- #[serde_as(as = "Option<DisplayFromStr>")]
- #[serde(alias = "delta")]
- pub limit: Option<i64>,
- #[serde_as(as = "Option<DisplayFromStr>")]
- #[serde(alias = "start")]
- pub offset: Option<i64>,
-}
-
-impl PageParams {
- const MAX_PAGE_SIZE: i64 = 1024;
-
- pub fn check(self) -> Result<Page, ParamsErr> {
- Self::check_custom(self, Self::MAX_PAGE_SIZE)
- }
-
- pub fn check_custom(self, max_page_size: i64) -> Result<Page, ParamsErr> {
- let limit = self.limit.unwrap_or(-20);
- if limit == 0 {
- return Err(param_err("limit", format!("must be non-zero got {limit}")));
- } else if limit > max_page_size {
- return Err(param_err(
- "limit",
- format!("must be <= {max_page_size} for {limit}"),
- ));
- }
- if let Some(offset) = self.offset
- && offset < 0
- {
- return Err(param_err(
- "offset",
- format!("must be positive got {offset}"),
- ));
- }
-
- Ok(Page {
- limit,
- offset: self.offset,
- })
- }
-}
-
-#[derive(Debug)]
-pub struct Page {
- pub limit: i64,
- pub offset: Option<i64>,
-}
-
-impl Default for Page {
- fn default() -> Self {
- Self {
- limit: 20,
- offset: None,
- }
- }
-}
-
-impl Page {
- pub fn backward(&self) -> bool {
- self.limit < 0
- }
-}
-
-#[serde_as]
-#[derive(Debug, Clone, Deserialize)]
-/// <https://docs.taler.net/core/api-common.html#long-polling>
-pub struct PoolingParams {
- #[serde_as(as = "Option<DisplayFromStr>")]
- #[serde(alias = "long_poll_ms")]
- pub timeout_ms: Option<u64>,
-}
-
-#[derive(Debug, Default)]
-pub struct Pooling {
- pub timeout_ms: Option<u64>,
-}
-
-impl PoolingParams {
- pub const MAX_TIMEOUT_MS: u64 = 60 * 60 * 10; // 1H
-
- pub fn check(self) -> Result<Pooling, ParamsErr> {
- Self::check_custom(self, Self::MAX_TIMEOUT_MS)
- }
-
- pub fn check_custom(self, max_timeout_ms: u64) -> Result<Pooling, ParamsErr> {
- let timeout_ms = self.timeout_ms.map(|it| it.min(max_timeout_ms));
- Ok(Pooling { timeout_ms })
- }
-}
-
-#[derive(Debug, Clone, Deserialize)]
-pub struct HistoryParams {
- #[serde(flatten)]
- pub pagination: PageParams,
- #[serde(flatten)]
- pub pooling: PoolingParams,
-}
-
-impl HistoryParams {
- pub fn check(self) -> Result<History, ParamsErr> {
- Self::check_custom(
- self,
- PageParams::MAX_PAGE_SIZE,
- PoolingParams::MAX_TIMEOUT_MS,
- )
- }
-
- pub fn check_custom(
- self,
- max_page_size: i64,
- max_timeout_ms: u64,
- ) -> Result<History, ParamsErr> {
- Ok(History {
- page: self.pagination.check_custom(max_page_size)?,
- pooling: self.pooling.check_custom(max_timeout_ms)?,
- })
- }
-}
-
-#[derive(Debug, Default)]
-pub struct History {
- pub page: Page,
- pub pooling: Pooling,
-}
-
-#[derive(Debug, Clone, Deserialize)]
-pub struct TransferParams {
- #[serde(flatten)]
- pub pagination: PageParams,
- pub status: Option<TransferState>,
-}
-
-#[derive(Debug, Clone, Deserialize)]
-pub struct AccountParams {
- pub account: PaytoURI,
-}
diff --git a/common/taler-common/src/api_revenue.rs b/common/taler-common/src/api_revenue.rs
@@ -1,49 +0,0 @@
-/*
- This file is part of TALER
- Copyright (C) 2025, 2026 Taler Systems SA
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU Affero General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-*/
-
-//! Type for the Taler Wire Gateway HTTP API <https://docs.taler.net/core/api-bank-wire.html#taler-wire-gateway-http-api>
-
-use serde::{Deserialize, Serialize};
-
-use super::api_common::SafeU64;
-use crate::types::{amount::Amount, payto::PaytoURI, timestamp::TalerTimestamp};
-
-/// <https://docs.taler.net/core/api-bank-revenue.html#tsref-type-RevenueConfig>
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct RevenueConfig<'a> {
- pub name: &'a str,
- pub version: &'a str,
- pub currency: &'a str,
- pub implementation: Option<&'a str>,
-}
-
-/// <https://docs.taler.net/core/api-bank-revenue.html#tsref-type-RevenueIncomingHistory>
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
-pub struct RevenueIncomingHistory {
- pub incoming_transactions: Vec<RevenueIncomingBankTransaction>,
- pub credit_account: PaytoURI,
-}
-
-/// <https://docs.taler.net/core/api-bank-revenue.html#tsref-type-RevenueIncomingBankTransaction>
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
-pub struct RevenueIncomingBankTransaction {
- pub row_id: SafeU64,
- pub date: TalerTimestamp,
- pub amount: Amount,
- pub credit_fee: Option<Amount>,
- pub debit_account: PaytoURI,
- pub subject: String,
-}
diff --git a/common/taler-common/src/api_transfer.rs b/common/taler-common/src/api_transfer.rs
@@ -1,113 +0,0 @@
-/*
- This file is part of TALER
- Copyright (C) 2026 Taler Systems SA
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU Affero General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-*/
-
-//! Type for the Taler Wire Transfer Gateway HTTP API <https://docs.taler.net/core/api-bank-transfer.html#taler-prepared-transfer-http-api>
-
-use compact_str::CompactString;
-use serde::{Deserialize, Serialize};
-use url::Url;
-
-use super::api_common::EddsaPublicKey;
-use crate::{
- api_common::EddsaSignature,
- db::IncomingType,
- types::{amount::Amount, timestamp::TalerTimestamp},
-};
-
-/// <https://docs.taler.net/core/api-bank-transfer.html#tsref-type-SubjectFormat>
-#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
-#[allow(non_camel_case_types)]
-pub enum SubjectFormat {
- SIMPLE,
- URI,
- CH_QR_BILL,
-}
-
-/// <https://docs.taler.net/core/api-bank-transfer.html#tsref-type-PreparedTransferConfig>
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct PreparedTransferConfig<'a> {
- pub name: &'a str,
- pub version: &'a str,
- pub currency: &'a str,
- pub implementation: Option<&'a str>,
- pub supported_formats: Vec<SubjectFormat>,
-}
-
-#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
-#[allow(non_camel_case_types)]
-pub enum TransferType {
- reserve,
- kyc,
-}
-
-impl From<TransferType> for IncomingType {
- fn from(value: TransferType) -> Self {
- match value {
- TransferType::reserve => IncomingType::reserve,
- TransferType::kyc => IncomingType::kyc,
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
-pub enum PublicKeyAlg {
- EdDSA,
-}
-
-/// <https://docs.taler.net/core/api-bank-transfer.html#tsref-type-RegistrationRequest>
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct RegistrationRequest {
- pub credit_amount: Amount,
- pub r#type: TransferType,
- pub alg: PublicKeyAlg,
- pub account_pub: EddsaPublicKey,
- pub authorization_pub: EddsaPublicKey,
- pub authorization_sig: EddsaSignature,
- pub recurrent: bool,
-}
-
-/// <https://docs.taler.net/core/api-bank-transfer.html#tsref-type-TransferSubject>
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
-#[serde(tag = "type")]
-pub enum TransferSubject {
- #[serde(rename = "SIMPLE")]
- Simple {
- credit_amount: Amount,
- subject: String,
- },
- #[serde(rename = "URI")]
- Uri { credit_amount: Amount, uri: Url },
- #[serde(rename = "CH_QR_BILL")]
- QrBill {
- credit_amount: Amount,
- qr_reference_number: String,
- },
-}
-
-/// <https://docs.taler.net/core/api-bank-transfer.html#tsref-type-RegistrationResponse>
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
-pub struct RegistrationResponse {
- pub subjects: Vec<TransferSubject>,
- pub expiration: TalerTimestamp,
-}
-
-/// <https://docs.taler.net/core/api-bank-transfer.html#tsref-type-Unregistration>
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct Unregistration {
- pub timestamp: CompactString,
- pub authorization_pub: EddsaPublicKey,
- pub authorization_sig: EddsaSignature,
-}
diff --git a/common/taler-common/src/api_wire.rs b/common/taler-common/src/api_wire.rs
@@ -1,207 +0,0 @@
-/*
- This file is part of TALER
- Copyright (C) 2024, 2025, 2026 Taler Systems SA
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU Affero General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-*/
-
-//! Type for the Taler Wire Gateway HTTP API <https://docs.taler.net/core/api-bank-wire.html#taler-wire-gateway-http-api>
-
-use compact_str::CompactString;
-use serde::{Deserialize, Serialize};
-use url::Url;
-
-use super::api_common::{EddsaPublicKey, HashCode, SafeU64, ShortHashCode, WadId};
-use crate::{
- api_common::EddsaSignature,
- types::{amount::Amount, payto::PaytoURI, timestamp::TalerTimestamp},
-};
-
-/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-WireConfig>
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct WireConfig<'a> {
- pub name: &'a str,
- pub version: &'a str,
- pub currency: &'a str,
- pub implementation: Option<&'a str>,
- pub support_account_check: bool,
-}
-
-/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-TransferResponse>
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct TransferResponse {
- pub timestamp: TalerTimestamp,
- pub row_id: SafeU64,
-}
-
-/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-TransferRequest>
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
-pub struct TransferRequest {
- pub request_uid: HashCode,
- pub amount: Amount,
- pub exchange_base_url: Url,
- pub metadata: Option<CompactString>,
- pub wtid: ShortHashCode,
- 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: PaytoURI,
-}
-
-/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-TransferListStatus>
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
-pub struct TransferListStatus {
- pub row_id: SafeU64,
- pub status: TransferState,
- pub amount: Amount,
- pub credit_account: PaytoURI,
- pub timestamp: TalerTimestamp,
-}
-
-/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-TransfertSatus>
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
-pub struct TransferStatus {
- pub status: TransferState,
- pub status_msg: Option<String>,
- pub amount: Amount,
- pub origin_exchange_url: String,
- pub metadata: Option<CompactString>,
- pub wtid: ShortHashCode,
- pub credit_account: PaytoURI,
- pub timestamp: TalerTimestamp,
-}
-
-/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-OutgoingHistory>
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct OutgoingHistory {
- pub outgoing_transactions: Vec<OutgoingBankTransaction>,
- pub debit_account: PaytoURI,
-}
-
-/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-OutgoingBankTransaction>
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
-pub struct OutgoingBankTransaction {
- pub row_id: SafeU64,
- pub date: TalerTimestamp,
- pub amount: Amount,
- pub debit_fee: Option<Amount>,
- pub credit_account: PaytoURI,
- pub wtid: ShortHashCode,
- pub exchange_base_url: Url,
- pub metadata: Option<CompactString>,
-}
-
-/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-IncomingHistory>
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct IncomingHistory {
- pub credit_account: PaytoURI,
- pub incoming_transactions: Vec<IncomingBankTransaction>,
-}
-
-/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-IncomingBankTransaction>
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
-#[serde(tag = "type")]
-pub enum IncomingBankTransaction {
- #[serde(rename = "RESERVE")]
- Reserve {
- row_id: SafeU64,
- date: TalerTimestamp,
- amount: Amount,
- credit_fee: Option<Amount>,
- debit_account: PaytoURI,
- reserve_pub: EddsaPublicKey,
- authorization_pub: Option<EddsaPublicKey>,
- authorization_sig: Option<EddsaSignature>,
- },
- #[serde(rename = "WAD")]
- Wad {
- row_id: SafeU64,
- date: TalerTimestamp,
- amount: Amount,
- debit_account: PaytoURI,
- origin_exchange_url: Url,
- wad_id: WadId,
- },
- #[serde(rename = "KYCAUTH")]
- Kyc {
- row_id: SafeU64,
- date: TalerTimestamp,
- amount: Amount,
- credit_fee: Option<Amount>,
- debit_account: PaytoURI,
- account_pub: EddsaPublicKey,
- authorization_pub: Option<EddsaPublicKey>,
- authorization_sig: Option<EddsaSignature>,
- },
-}
-
-/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-AddIncomingRequest>
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct AddIncomingRequest {
- pub amount: Amount,
- pub reserve_pub: EddsaPublicKey,
- pub debit_account: PaytoURI,
-}
-
-/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-AddIncomingResponse>
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct AddIncomingResponse {
- pub row_id: SafeU64,
- pub timestamp: TalerTimestamp,
-}
-
-/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-AddKycauthRequest>
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct AddKycauthRequest {
- pub amount: Amount,
- pub account_pub: EddsaPublicKey,
- pub debit_account: PaytoURI,
-}
-
-/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-AddMappedRequest>
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct AddMappedRequest {
- pub amount: Amount,
- pub authorization_pub: EddsaPublicKey,
- pub debit_account: PaytoURI,
-}
-
-/// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-AccountInfo>
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct AccountInfo {}
-
-#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, sqlx::Type)]
-#[allow(non_camel_case_types)]
-#[sqlx(type_name = "transfer_status")]
-pub enum TransferState {
- pending,
- transient_failure,
- permanent_failure,
- late_failure,
- success,
-}
-
-impl AsRef<str> for TransferState {
- fn as_ref(&self) -> &str {
- match self {
- TransferState::pending => "pending",
- TransferState::transient_failure => "transient_failure",
- TransferState::permanent_failure => "permanent_failure",
- TransferState::late_failure => "late_failure",
- TransferState::success => "success",
- }
- }
-}
diff --git a/common/taler-common/src/lib.rs b/common/taler-common/src/lib.rs
@@ -22,11 +22,7 @@ use tracing_subscriber::util::SubscriberInitExt;
use crate::log::taler_logger;
-pub mod api_common;
-pub mod api_params;
-pub mod api_revenue;
-pub mod api_transfer;
-pub mod api_wire;
+pub mod api;
pub mod bench;
pub mod cli;
pub mod config;
diff --git a/common/taler-macros/Cargo.toml b/common/taler-macros/Cargo.toml
@@ -15,3 +15,4 @@ test = false
[dependencies]
quote = "1"
syn = "2"
+proc-macro2 = "1"
+\ No newline at end of file
diff --git a/common/taler-macros/src/lib.rs b/common/taler-macros/src/lib.rs
@@ -16,7 +16,10 @@
use proc_macro::TokenStream;
use quote::quote;
-use syn::{Data, DeriveInput, Error, Expr, Lit, Meta, parse_macro_input};
+use syn::{
+ Data, DeriveInput, Error, Expr, Field, Fields, ItemStruct, Lit, LitStr, Meta, parse::Parser,
+ parse_macro_input,
+};
#[proc_macro_derive(EnumMeta, attributes(enum_meta, code))]
pub fn derive_domain_code(input: TokenStream) -> TokenStream {
@@ -224,3 +227,106 @@ pub fn derive_domain_code(input: TokenStream) -> TokenStream {
TokenStream::from(expanded)
}
+
+#[proc_macro_attribute]
+pub fn api_config(attr: TokenStream, item: TokenStream) -> TokenStream {
+ // 1. Cleanly parse the string attribute argument
+ let api_name_lit = parse_macro_input!(attr as LitStr);
+ let api_name_value = api_name_lit.value();
+
+ // 2. Parse the target struct
+ let mut input_struct = parse_macro_input!(item as ItemStruct);
+ let struct_name = &input_struct.ident;
+
+ // 3. Generate deterministic function names for serialization AND deserialization
+ let struct_lower = struct_name.to_string().to_lowercase();
+ let serialize_fn_ident = syn::Ident::new(
+ &format!("_serialize_api_name_for_{}", struct_lower),
+ proc_macro2::Span::call_site(),
+ );
+ let deserialize_fn_ident = syn::Ident::new(
+ &format!("_deserialize_api_name_for_{}", struct_lower),
+ proc_macro2::Span::call_site(),
+ );
+
+ // 4. Convert identifiers to string paths for Serde attributes
+ let serialize_fn_str = serialize_fn_ident.to_string();
+ let deserialize_fn_str = deserialize_fn_ident.to_string();
+
+ let crate_path = if std::env::var("CARGO_CRATE_NAME").unwrap_or_default() == "taler_common" {
+ quote! { crate }
+ } else {
+ quote! { ::taler_common }
+ };
+
+ // 5. Inject the `name` field with BOTH serialization and deserialization hooks
+ if let Fields::Named(ref mut fields) = input_struct.fields {
+ fields.named.insert(
+ 0,
+ Field::parse_named
+ .parse2(quote! {
+ #[serde(
+ serialize_with = #serialize_fn_str,
+ deserialize_with = #deserialize_fn_str
+ )]
+ pub name: ()
+ })
+ .unwrap(),
+ );
+ fields.named.insert(
+ 1,
+ Field::parse_named
+ .parse2(quote! {
+ pub version: #crate_path::api::LibtoolVersion
+ })
+ .unwrap(),
+ );
+ fields.named.insert(
+ 2,
+ Field::parse_named
+ .parse2(quote! {
+ pub implementation: Option<&'a str>
+ })
+ .unwrap(),
+ );
+ } else {
+ return syn::Error::new_spanned(
+ input_struct,
+ "#[api_config] only works on structs with named fields",
+ )
+ .to_compile_error()
+ .into();
+ }
+
+ let expanded = quote! {
+ #input_struct
+
+ #[doc(hidden)]
+ #[allow(non_snake_case)]
+ pub fn #serialize_fn_ident<S>(_: &(), s: S) -> ::std::result::Result<S::Ok, S::Error>
+ where
+ S: ::serde::Serializer
+ {
+ s.serialize_str(#api_name_value)
+ }
+
+ #[doc(hidden)]
+ #[allow(non_snake_case)]
+ pub fn #deserialize_fn_ident<'de, D>(deserializer: D) -> ::std::result::Result<(), D::Error>
+ where
+ D: ::serde::Deserializer<'de>,
+ {
+ let s: ::std::string::String = ::serde::Deserialize::deserialize(deserializer)?;
+ if s == #api_name_value {
+ Ok(())
+ } else {
+ Err(::serde::de::Error::custom(::std::format!(
+ "invalid API name: expected '{}', found '{}'",
+ #api_name_value, s
+ )))
+ }
+ }
+ };
+
+ TokenStream::from(expanded)
+}
diff --git a/common/taler-test-utils/src/lib.rs b/common/taler-test-utils/src/lib.rs
@@ -17,6 +17,7 @@
pub use axum::Router;
use taler_common::log::taler_logger;
use tracing_subscriber::util::SubscriberInitExt;
+
pub mod db;
pub mod json;
pub mod routine;
diff --git a/common/taler-test-utils/src/routine.rs b/common/taler-test-utils/src/routine.rs
@@ -29,13 +29,15 @@ use taler_api::{
subject::fmt_in_subject,
};
use taler_common::{
- api_common::{EddsaPublicKey, HashCode, ShortHashCode},
- api_params::PageParams,
- api_revenue::RevenueIncomingHistory,
- api_transfer::{RegistrationResponse, TransferSubject, TransferType},
- api_wire::{
- IncomingBankTransaction, IncomingHistory, OutgoingHistory, TransferList, TransferRequest,
- TransferResponse, TransferState, TransferStatus,
+ api::{
+ EddsaPublicKey, HashCode, ShortHashCode,
+ params::PageParams,
+ prepared::{RegistrationResponse, TransferSubject, TransferType},
+ revenue::RevenueIncomingHistory,
+ wire::{
+ IncomingBankTransaction, IncomingHistory, OutgoingHistory, TransferList,
+ TransferRequest, TransferResponse, TransferState, TransferStatus,
+ },
},
db::IncomingType,
error_code::ErrorCode,
@@ -59,7 +61,7 @@ impl Page for IncomingHistory {
.map(|it| match it {
IncomingBankTransaction::Reserve { row_id, .. }
| IncomingBankTransaction::Wad { row_id, .. }
- | IncomingBankTransaction::Kyc { row_id, .. } => **row_id as i64,
+ | IncomingBankTransaction::Kyc { row_id, .. } => *row_id as i64,
})
.collect()
}
@@ -69,7 +71,7 @@ impl Page for OutgoingHistory {
fn ids(&self) -> Vec<i64> {
self.outgoing_transactions
.iter()
- .map(|it| *it.row_id as i64)
+ .map(|it| it.row_id as i64)
.collect()
}
}
@@ -78,14 +80,14 @@ impl Page for RevenueIncomingHistory {
fn ids(&self) -> Vec<i64> {
self.incoming_transactions
.iter()
- .map(|it| *it.row_id as i64)
+ .map(|it| it.row_id as i64)
.collect()
}
}
impl Page for TransferList {
fn ids(&self) -> Vec<i64> {
- self.transfers.iter().map(|it| *it.row_id as i64).collect()
+ self.transfers.iter().map(|it| it.row_id as i64).collect()
}
}
@@ -473,13 +475,15 @@ pub async fn transfer_routine(
.assert_error(ErrorCode::GENERIC_JSON_INVALID);
// Missing receiver-name
- server
+ let res = server
.post("/taler-wire-gateway/transfer")
.json(&json!(transfer_req + {
- "credit_account": credit_account.as_ref().as_str().split_once('?').unwrap().0
+ "credit_account": credit_account.as_ref().as_str().split('?').next().unwrap()
}))
- .await
- .assert_error(ErrorCode::GENERIC_PAYTO_URI_MALFORMED);
+ .await;
+ if !res.status.is_success() {
+ res.assert_error(ErrorCode::GENERIC_PAYTO_URI_MALFORMED);
+ }
// TODO check bad payto
// TODO Bad base URL
@@ -682,50 +686,108 @@ pub struct Tasks<F: AsyncFnMut(usize)> {
#[macro_export]
macro_rules! tasks {
- ( $( $(if $cond:expr =>)? $body:block ),* $(,)? ) => {{
- // Evaluate every condition exactly once, up front
- let conditions = [ $( true $( && ($cond) )? ),* ];
+ // Create new
+ ( $( $(if $cond:expr =>)? $body:block ),* $(,)? ) => {
+ $crate::tasks!(@build 0usize;
+ $( $(if $cond =>)? $body ),*
+ )
+ };
- // Compact length = number of true conditions
- let active_len = conditions.iter().filter(|&&c| c).count();
+ // Append to existing
+ ( $existing:expr ; $( $(if $cond:expr =>)? $body:block ),* $(,)? ) => {{
+ let mut existing = $existing;
+ let mut extra = $crate::tasks![
+ $( $(if $cond =>)? $body ),*
+ ];
$crate::routine::Tasks {
- len: active_len,
+ len: existing.len + extra.len,
lambda: async move |i: usize| {
- if active_len == 0 {
- return
+ if existing.len == 0 && extra.len == 0 {
+ return;
+ }
+
+ let i = i % (existing.len + extra.len);
+
+ if i < existing.len {
+ (existing.lambda)(i).await;
+ } else {
+ (extra.lambda)(i - existing.len).await;
}
- let i = i % active_len;
- let mut cond_idx = 0usize;
- let mut current_idx = 0usize;
-
- $(
- // If this specific block's condition passed...
- if conditions[cond_idx] {
- // ...and it matches the requested execution index
- if i == current_idx {
- (async $body).await;
- return;
- }
- current_idx += 1;
- }
- cond_idx += 1;
- )*
-
- let _ = (&mut current_idx, &mut cond_idx); // suppress lints
- unreachable!("Index {i} out of bounds for {active_len} active tasks");
}
}
}};
+
+ // Internal builder
+ (@build $idx:expr; $( $(if $cond:expr =>)? $body:block ),* ) => {{
+ let conditions = [ $( true $( && $cond )? ),* ];
+ let len = conditions.iter().filter(|&&x| x).count();
+
+ $crate::routine::Tasks {
+ len,
+ lambda: async move |i: usize| {
+ if len == 0 {
+ return;
+ }
+
+ let i = i % len;
+ let mut current = 0usize;
+
+ $crate::tasks!(
+ @dispatch i, current, conditions, 0usize;
+ $( $(if $cond =>)? $body ),*
+ );
+
+ let _ = (i, &mut current); // suppress lints
+
+ unreachable!()
+ }
+ }
+ }};
+
+ // Recursive dispatcher
+ (@dispatch $i:expr, $current:ident, $conditions:ident, $idx:expr;) => {};
+
+ (@dispatch
+ $i:expr,
+ $current:ident,
+ $conditions:ident,
+ $idx:expr;
+ $(if $cond:expr =>)? $body:block
+ $(, $($rest:tt)*)?
+ ) => {{
+ if $conditions[$idx] {
+ if $i == $current {
+ (async $body).await;
+ return;
+ }
+ $current += 1;
+ }
+
+ $crate::tasks!(
+ @dispatch
+ $i,
+ $current,
+ $conditions,
+ $idx + 1usize;
+ $($($rest)*)?
+ );
+ }};
}
/// Test standard behavior of the revenue endpoints
-pub async fn revenue_routine(server: &Router, debit_acount: &PaytoURI, kyc: bool) {
+pub async fn revenue_routine(
+ server: &Router,
+ debit_acount: &PaytoURI,
+ kyc: bool,
+ register: Tasks<impl AsyncFnMut(usize)>,
+ ignore: Tasks<impl AsyncFnMut(usize)>,
+) {
let currency = &get_currency(server).await;
routine_history::<RevenueIncomingHistory>(
server,
"/taler-revenue/history",
- tasks!(
+ tasks!(register;
{
server
.post("/taler-wire-gateway/admin/add-incoming")
@@ -749,155 +811,173 @@ pub async fn revenue_routine(server: &Router, debit_acount: &PaytoURI, kyc: bool
.assert_ok_json::<TransferResponse>();
}
),
- tasks!(),
+ ignore,
+ )
+ .await;
+}
+
+/// Test standard behavior of the outgoing history endpoint
+pub async fn out_history_routine(
+ server: &Router,
+ register: Tasks<impl AsyncFnMut(usize)>,
+ ignore: Tasks<impl AsyncFnMut(usize)>,
+) {
+ routine_history::<OutgoingHistory>(
+ server,
+ "/taler-wire-gateway/history/outgoing",
+ register,
+ ignore,
)
.await;
}
/// Test standard behavior of the incoming history endpoint
-pub async fn in_history_routine(server: &Router, debit_acount: &PaytoURI, kyc: bool) {
+pub async fn in_history_routine(
+ server: &Router,
+ debit_acount: &PaytoURI,
+ kyc: bool,
+ register: Tasks<impl AsyncFnMut(usize)>,
+ ignored: Tasks<impl AsyncFnMut(usize)>,
+) {
let currency = &get_currency(server).await;
- // History
- // TODO check non taler some are ignored
let mut key = Ed25519KeyPair::generate().unwrap();
- let tasks = tasks!(
- {
- server
- .post("/taler-wire-gateway/admin/add-incoming")
- .json(json!({
- "amount": format!("{currency}:1"),
- "reserve_pub": EddsaPublicKey::rand(),
- "debit_account": debit_acount,
- }))
- .await
- .assert_ok_json::<TransferResponse>();
- },
- {
- key = Ed25519KeyPair::generate().unwrap();
- let auth_pub = EddsaPublicKey::try_from(key.public_key().as_ref()).unwrap();
- let reserve_pub = EddsaPublicKey::rand();
- let amount = format!("{currency}:2");
- server
- .post("/taler-prepared-transfer/registration")
- .json(json!({
- "credit_amount": amount,
- "type": "reserve",
- "alg": "EdDSA",
- "account_pub": reserve_pub,
- "authorization_pub": auth_pub,
- "authorization_sig": eddsa_sign(&key, reserve_pub.as_ref()),
- "recurrent": true
- }))
- .await
- .assert_ok_json::<RegistrationResponse>();
- server
- .post("/taler-wire-gateway/admin/add-mapped")
- .json(json!({
- "amount": amount,
- "authorization_pub": auth_pub,
- "debit_account": debit_acount,
- }))
- .await
- .assert_ok_json::<TransferResponse>();
- server
- .post("/taler-wire-gateway/admin/add-mapped")
- .json(json!({
- "amount": amount,
- "authorization_pub": auth_pub,
- "debit_account": debit_acount,
- }))
- .await
- .assert_ok_json::<TransferResponse>();
- },
- {
- let auth_pub = EddsaPublicKey::try_from(key.public_key().as_ref()).unwrap();
- let reserve_pub = EddsaPublicKey::rand();
- server
- .post("/taler-prepared-transfer/registration")
- .json(json!({
- "credit_amount": format!("{currency}:3"),
- "type": "reserve",
- "alg": "EdDSA",
- "account_pub": reserve_pub,
- "authorization_pub": auth_pub,
- "authorization_sig": eddsa_sign(&key, reserve_pub.as_ref()),
- "recurrent": true
- }))
- .await
- .assert_ok_json::<RegistrationResponse>();
- },
- if kyc => {
- server
- .post("/taler-wire-gateway/admin/add-kycauth")
- .json(json!({
- "amount": format!("{currency}:4"),
- "account_pub": EddsaPublicKey::rand(),
- "debit_account": debit_acount,
- }))
- .await
- .assert_ok_json::<TransferResponse>();
- },
- if kyc => {
- key = Ed25519KeyPair::generate().unwrap();
- let auth_pub = EddsaPublicKey::try_from(key.public_key().as_ref()).unwrap();
- let account_pub = EddsaPublicKey::rand();
- let amount = format!("{currency}:5");
- server
- .post("/taler-prepared-transfer/registration")
- .json(json!({
- "credit_amount": amount,
- "type": "kyc",
- "alg": "EdDSA",
- "account_pub": account_pub,
- "authorization_pub": auth_pub,
- "authorization_sig": eddsa_sign(&key, account_pub.as_ref()),
- "recurrent": true
- }))
- .await
- .assert_ok_json::<RegistrationResponse>();
- server
- .post("/taler-wire-gateway/admin/add-mapped")
- .json(json!({
- "amount": amount,
- "authorization_pub": auth_pub,
- "debit_account": debit_acount,
- }))
- .await
- .assert_ok_json::<TransferResponse>();
- server
- .post("/taler-wire-gateway/admin/add-mapped")
- .json(json!({
- "amount": amount,
- "authorization_pub": auth_pub,
- "debit_account": debit_acount,
- }))
- .await
- .assert_ok_json::<TransferResponse>();
- },
- if kyc => {
- let auth_pub = EddsaPublicKey::try_from(key.public_key().as_ref()).unwrap();
- let account_pub = EddsaPublicKey::rand();
- server
- .post("/taler-prepared-transfer/registration")
- .json(json!({
- "credit_amount": format!("{currency}:6"),
- "type": "kyc",
- "alg": "EdDSA",
- "account_pub": account_pub,
- "authorization_pub": auth_pub,
- "authorization_sig": eddsa_sign(&key, account_pub.as_ref()),
- "recurrent": true
- }))
- .await
- .assert_ok_json::<RegistrationResponse>();
- }
- );
routine_history::<IncomingHistory>(
server,
"/taler-wire-gateway/history/incoming",
- tasks,
- tasks!(),
+ tasks!(register;
+ {
+ server
+ .post("/taler-wire-gateway/admin/add-incoming")
+ .json(json!({
+ "amount": format!("{currency}:1"),
+ "reserve_pub": EddsaPublicKey::rand(),
+ "debit_account": debit_acount,
+ }))
+ .await
+ .assert_ok_json::<TransferResponse>();
+ },
+ {
+ key = Ed25519KeyPair::generate().unwrap();
+ let auth_pub = EddsaPublicKey::try_from(key.public_key().as_ref()).unwrap();
+ let reserve_pub = EddsaPublicKey::rand();
+ let amount = format!("{currency}:2");
+ server
+ .post("/taler-prepared-transfer/registration")
+ .json(json!({
+ "credit_amount": amount,
+ "type": "reserve",
+ "alg": "EdDSA",
+ "account_pub": reserve_pub,
+ "authorization_pub": auth_pub,
+ "authorization_sig": eddsa_sign(&key, reserve_pub.as_ref()),
+ "recurrent": true
+ }))
+ .await
+ .assert_ok_json::<RegistrationResponse>();
+ server
+ .post("/taler-wire-gateway/admin/add-mapped")
+ .json(json!({
+ "amount": amount,
+ "authorization_pub": auth_pub,
+ "debit_account": debit_acount,
+ }))
+ .await
+ .assert_ok_json::<TransferResponse>();
+ server
+ .post("/taler-wire-gateway/admin/add-mapped")
+ .json(json!({
+ "amount": amount,
+ "authorization_pub": auth_pub,
+ "debit_account": debit_acount,
+ }))
+ .await
+ .assert_ok_json::<TransferResponse>();
+ },
+ {
+ let auth_pub = EddsaPublicKey::try_from(key.public_key().as_ref()).unwrap();
+ let reserve_pub = EddsaPublicKey::rand();
+ server
+ .post("/taler-prepared-transfer/registration")
+ .json(json!({
+ "credit_amount": format!("{currency}:3"),
+ "type": "reserve",
+ "alg": "EdDSA",
+ "account_pub": reserve_pub,
+ "authorization_pub": auth_pub,
+ "authorization_sig": eddsa_sign(&key, reserve_pub.as_ref()),
+ "recurrent": true
+ }))
+ .await
+ .assert_ok_json::<RegistrationResponse>();
+ },
+ if kyc => {
+ server
+ .post("/taler-wire-gateway/admin/add-kycauth")
+ .json(json!({
+ "amount": format!("{currency}:4"),
+ "account_pub": EddsaPublicKey::rand(),
+ "debit_account": debit_acount,
+ }))
+ .await
+ .assert_ok_json::<TransferResponse>();
+ },
+ if kyc => {
+ key = Ed25519KeyPair::generate().unwrap();
+ let auth_pub = EddsaPublicKey::try_from(key.public_key().as_ref()).unwrap();
+ let account_pub = EddsaPublicKey::rand();
+ let amount = format!("{currency}:5");
+ server
+ .post("/taler-prepared-transfer/registration")
+ .json(json!({
+ "credit_amount": amount,
+ "type": "kyc",
+ "alg": "EdDSA",
+ "account_pub": account_pub,
+ "authorization_pub": auth_pub,
+ "authorization_sig": eddsa_sign(&key, account_pub.as_ref()),
+ "recurrent": true
+ }))
+ .await
+ .assert_ok_json::<RegistrationResponse>();
+ server
+ .post("/taler-wire-gateway/admin/add-mapped")
+ .json(json!({
+ "amount": amount,
+ "authorization_pub": auth_pub,
+ "debit_account": debit_acount,
+ }))
+ .await
+ .assert_ok_json::<TransferResponse>();
+ server
+ .post("/taler-wire-gateway/admin/add-mapped")
+ .json(json!({
+ "amount": amount,
+ "authorization_pub": auth_pub,
+ "debit_account": debit_acount,
+ }))
+ .await
+ .assert_ok_json::<TransferResponse>();
+ },
+ if kyc => {
+ let auth_pub = EddsaPublicKey::try_from(key.public_key().as_ref()).unwrap();
+ let account_pub = EddsaPublicKey::rand();
+ server
+ .post("/taler-prepared-transfer/registration")
+ .json(json!({
+ "credit_amount": format!("{currency}:6"),
+ "type": "kyc",
+ "alg": "EdDSA",
+ "account_pub": account_pub,
+ "authorization_pub": auth_pub,
+ "authorization_sig": eddsa_sign(&key, account_pub.as_ref()),
+ "recurrent": true
+ }))
+ .await
+ .assert_ok_json::<RegistrationResponse>();
+ }
+ ),
+ ignored,
)
.await;
}
@@ -992,7 +1072,7 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>(
for sub in res.subjects {
if let TransferSubject::Simple { subject, .. } = sub {
- assert_eq!(subject, fmt_in_subject(fmt, &auth_pub1));
+ assert_eq!(subject, fmt_in_subject(fmt, &auth_pub1).to_string());
};
}
};
@@ -1334,7 +1414,6 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>(
.assert_no_content();
// Check bounce pending on deletion
-
check_in(&[
Reserve(acc_pub1.clone()),
Reserve(acc_pub2.clone()),
diff --git a/common/taler-test-utils/src/server.rs b/common/taler-test-utils/src/server.rs
@@ -19,21 +19,25 @@ use std::{fmt::Debug, io::Write, pin::Pin};
use axum::{
Router,
body::{Body, Bytes},
- extract::Query,
+ extract::{Query, Request, State},
http::{
HeaderMap, HeaderValue, Method, StatusCode, Uri,
header::{self, AUTHORIZATION, AsHeaderName, IntoHeaderName},
+ uri::PathAndQuery,
},
+ middleware::{self},
};
use flate2::{Compression, write::ZlibEncoder};
use http_body_util::BodyExt as _;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
-use taler_common::{api_common::ErrorDetail, encoding::base64, error_code::ErrorCode};
+use taler_common::{api::ErrorDetail, encoding::base64, error_code::ErrorCode};
use tower::ServiceExt as _;
use tracing::warn;
use url::Url;
pub trait TestServer {
+ fn prefix(&self, prefix: &'static str) -> Self;
+
fn request(&self, method: Method, path: impl AsRef<str>) -> TestRequest;
fn get(&self, path: impl AsRef<str>) -> TestRequest {
@@ -50,6 +54,31 @@ pub trait TestServer {
}
impl TestServer for Router {
+ fn prefix(&self, prefix: &'static str) -> Self {
+ Router::new()
+ .fallback_service(self.clone().into_service())
+ .layer(middleware::map_request_with_state(
+ prefix,
+ async |State(prefix): State<&'static str>, mut req: Request| {
+ let uri = req.uri().clone();
+ let mut parts = uri.into_parts();
+
+ let path_and_query = parts.path_and_query.unwrap();
+ let current_path = path_and_query.path();
+
+ let new_path_and_query = match path_and_query.query() {
+ Some(query) => format!("{prefix}{}?{}", current_path, query),
+ None => format!("{prefix}{}", current_path),
+ };
+
+ let new_pq = PathAndQuery::from_maybe_shared(new_path_and_query).unwrap();
+ parts.path_and_query = Some(new_pq);
+ *req.uri_mut() = Uri::from_parts(parts).unwrap();
+ req
+ },
+ ))
+ }
+
fn request(&self, method: Method, path: impl AsRef<str>) -> TestRequest {
let url = format!("https://example{}", path.as_ref());
TestRequest {
diff --git a/taler-cyclos/src/api.rs b/taler-cyclos/src/api.rs
@@ -17,21 +17,23 @@
use compact_str::CompactString;
use jiff::Timestamp;
use taler_api::{
- api::{TalerApi, revenue::Revenue, transfer::PreparedTransfer, wire::WireGateway},
- error::{ApiResult, failure, failure_code},
+ api::{TalerApi, prepared::PreparedTransfer, revenue::Revenue, wire::WireGateway},
+ error::{ApiResult, failure_code},
subject::{IncomingSubject, fmt_in_subject},
};
use taler_common::{
- api_common::safe_u64,
- api_params::{History, Page},
- api_revenue::RevenueIncomingHistory,
- api_transfer::{
- RegistrationRequest, RegistrationResponse, SubjectFormat, TransferSubject, Unregistration,
- },
- api_wire::{
- AddIncomingRequest, AddIncomingResponse, AddKycauthRequest, AddMappedRequest,
- IncomingHistory, OutgoingHistory, TransferList, TransferRequest, TransferResponse,
- TransferState, TransferStatus,
+ api::{
+ params::{History, Page},
+ prepared::{
+ RegistrationRequest, RegistrationResponse, SubjectFormat, TransferSubject,
+ Unregistration,
+ },
+ revenue::RevenueIncomingHistory,
+ wire::{
+ AddIncomingRequest, AddIncomingResponse, AddKycauthRequest, AddMappedRequest,
+ IncomingHistory, OutgoingHistory, TransferList, TransferRequest, TransferResponse,
+ TransferState, TransferStatus,
+ },
},
db::IncomingType,
error_code::ErrorCode,
@@ -88,8 +90,8 @@ impl CyclosApi {
}
impl TalerApi for CyclosApi {
- fn currency(&self) -> &str {
- self.currency.as_ref()
+ fn currency(&self) -> Currency {
+ self.currency
}
fn implementation(&self) -> &'static str {
@@ -117,7 +119,7 @@ impl WireGateway for CyclosApi {
match result {
db::TransferResult::Success { id, initiated_at } => Ok(TransferResponse {
timestamp: initiated_at.into(),
- row_id: safe_u64(id),
+ row_id: id,
}),
db::TransferResult::RequestUidReuse => {
Err(failure_code(ErrorCode::BANK_TRANSFER_REQUEST_UID_REUSED))
@@ -193,7 +195,7 @@ impl WireGateway for CyclosApi {
AddIncomingResult::Success {
row_id, valued_at, ..
} => Ok(AddIncomingResponse {
- row_id: safe_u64(row_id),
+ row_id,
timestamp: valued_at.into(),
}),
AddIncomingResult::ReservePubReuse => {
@@ -223,7 +225,7 @@ impl WireGateway for CyclosApi {
AddIncomingResult::Success {
row_id, valued_at, ..
} => Ok(AddIncomingResponse {
- row_id: safe_u64(row_id),
+ row_id,
timestamp: valued_at.into(),
}),
AddIncomingResult::ReservePubReuse => {
@@ -253,7 +255,7 @@ impl WireGateway for CyclosApi {
AddIncomingResult::Success {
row_id, valued_at, ..
} => Ok(AddIncomingResponse {
- row_id: safe_u64(row_id),
+ row_id,
timestamp: valued_at.into(),
}),
AddIncomingResult::ReservePubReuse => {
@@ -300,9 +302,9 @@ impl PreparedTransfer for CyclosApi {
let simple = TransferSubject::Simple {
credit_amount: req.credit_amount,
subject: if req.authorization_pub == req.account_pub && !req.recurrent {
- fmt_in_subject(req.r#type.into(), &req.account_pub)
+ fmt_in_subject(req.r#type.into(), &req.account_pub).to_string()
} else {
- fmt_in_subject(IncomingType::map, &req.authorization_pub)
+ fmt_in_subject(IncomingType::map, &req.authorization_pub).to_string()
},
};
ApiResult::Ok(RegistrationResponse {
@@ -316,15 +318,8 @@ impl PreparedTransfer for CyclosApi {
}
}
- async fn unregistration(&self, req: Unregistration) -> ApiResult<()> {
- if !db::transfer_unregister(&self.pool, &req).await? {
- Err(failure(
- ErrorCode::BANK_TRANSACTION_NOT_FOUND,
- format!("Prepared transfer '{}' not found", req.authorization_pub),
- ))
- } else {
- Ok(())
- }
+ async fn unregistration(&self, req: Unregistration) -> ApiResult<bool> {
+ Ok(db::transfer_unregister(&self.pool, &req).await?)
}
}
@@ -339,12 +334,18 @@ mod test {
use jiff::Timestamp;
use sqlx::{PgPool, Row as _, postgres::PgRow};
use taler_api::{
- api::TalerRouter as _, auth::AuthMethod, db::TypeHelper as _, subject::OutgoingSubject,
+ api::TalerRouter as _,
+ auth::AuthMethod,
+ db::TypeHelper as _,
+ subject::{IncomingSubject, OutgoingSubject},
};
use taler_common::{
- api_revenue::RevenueConfig,
- api_transfer::PreparedTransferConfig,
- api_wire::{OutgoingHistory, TransferState, WireConfig},
+ api::{
+ EddsaPublicKey,
+ prepared::PreparedTransferConfig,
+ revenue::RevenueConfig,
+ wire::{TransferState, WireConfig},
+ },
db::IncomingType,
types::{
amount::{Currency, decimal},
@@ -355,8 +356,8 @@ mod test {
Router,
db::db_test_setup,
routine::{
- Status, admin_add_incoming_routine, in_history_routine, registration_routine,
- revenue_routine, routine_pagination, transfer_routine,
+ Status, admin_add_incoming_routine, in_history_routine, out_history_routine,
+ registration_routine, revenue_routine, transfer_routine,
},
server::TestServer as _,
tasks,
@@ -365,7 +366,7 @@ mod test {
use crate::{
api::CyclosApi,
constants::CONFIG_SOURCE,
- db::{self, TxOutKind},
+ db::{self, TxIn, TxOutKind},
};
static ACCOUNT: LazyLock<PaytoURI> =
@@ -411,33 +412,82 @@ mod test {
transfer_routine(&server, TransferState::pending, &ACCOUNT).await;
}
+ static CODE: AtomicI64 = AtomicI64::new(0);
+
+ async fn r#in(db: &PgPool, subject: Option<IncomingSubject>) {
+ let now = Timestamp::now();
+ db::register_tx_in(
+ &mut db.acquire().await.unwrap(),
+ &TxIn {
+ transfer_id: CODE.fetch_add(1, Ordering::Relaxed),
+ tx_id: None,
+ amount: decimal("10"),
+ subject: "subject".to_owned(),
+ debtor_id: 31000163100000000,
+ debtor_name: "Name".into(),
+ valued_at: Timestamp::now(),
+ },
+ &subject,
+ &now,
+ )
+ .await
+ .unwrap();
+ }
+
+ async fn in_malformed(db: &PgPool) {
+ r#in(db, None).await
+ }
+
+ async fn in_talerable(db: &PgPool) {
+ r#in(db, Some(IncomingSubject::Reserve(EddsaPublicKey::rand()))).await
+ }
+
+ async fn out(db: &PgPool, kind: &TxOutKind) {
+ let i = CODE.fetch_add(1, Ordering::Relaxed);
+ let now = Timestamp::now();
+ db::register_tx_out(
+ &mut db.acquire().await.unwrap(),
+ &db::TxOut {
+ transfer_id: i,
+ tx_id: if i % 2 == 0 { Some(i % 2) } else { None },
+ amount: decimal("10"),
+ subject: "subject".to_owned(),
+ creditor_id: 31000163100000000,
+ creditor_name: "Name".into(),
+ valued_at: now,
+ },
+ kind,
+ &now,
+ )
+ .await
+ .unwrap();
+ }
+
+ async fn out_talerable(db: &PgPool) {
+ out(db, &TxOutKind::Talerable(OutgoingSubject::rand())).await
+ }
+
+ async fn out_bounce(db: &PgPool) {
+ out(db, &TxOutKind::Bounce(CODE.load(Ordering::Relaxed))).await
+ }
+
+ async fn out_malformed(db: &PgPool) {
+ out(db, &TxOutKind::Simple).await
+ }
+
#[tokio::test]
async fn outgoing_history() {
- let (server, pool) = setup().await;
- static CODE: AtomicI64 = AtomicI64::new(0);
+ let (server, db) = &setup().await;
- routine_pagination::<OutgoingHistory>(
+ out_history_routine(
&server,
- "/taler-wire-gateway/history/outgoing",
- tasks!({
- let i = CODE.fetch_add(1, Ordering::Relaxed);
- db::register_tx_out(
- &mut pool.acquire().await.unwrap(),
- &db::TxOut {
- transfer_id: i,
- tx_id: if i % 2 == 0 { Some(i % 2) } else { None },
- amount: decimal("10"),
- subject: "subject".to_owned(),
- creditor_id: 31000163100000000,
- creditor_name: "Name".into(),
- valued_at: Timestamp::now(),
- },
- &TxOutKind::Talerable(OutgoingSubject::rand()),
- &Timestamp::now(),
- )
- .await
- .unwrap();
- }),
+ tasks!({ out_talerable(db).await }),
+ tasks!(
+ { out_bounce(db).await },
+ { out_malformed(db).await },
+ { in_malformed(db).await },
+ { in_talerable(db).await }
+ ),
)
.await;
}
@@ -450,14 +500,35 @@ mod test {
#[tokio::test]
async fn in_history() {
- let (server, _) = setup().await;
- in_history_routine(&server, &ACCOUNT, true).await;
+ let (server, db) = &setup().await;
+ in_history_routine(
+ &server,
+ &ACCOUNT,
+ true,
+ tasks!({ in_talerable(db).await }),
+ tasks!(
+ { out_malformed(db).await },
+ { out_talerable(db).await },
+ { out_bounce(db).await },
+ { in_malformed(db).await }
+ ),
+ )
+ .await;
}
#[tokio::test]
async fn revenue() {
- let (server, _) = setup().await;
- revenue_routine(&server, &ACCOUNT, true).await;
+ let (server, db) = &setup().await;
+ revenue_routine(
+ &server,
+ &ACCOUNT,
+ true,
+ tasks!({ in_malformed(db).await }, { in_talerable(db).await },),
+ tasks!({ out_malformed(db).await }, { out_talerable(db).await }, {
+ out_bounce(db).await
+ }),
+ )
+ .await;
}
async fn check_in(pool: &PgPool) -> Vec<Status> {
diff --git a/taler-cyclos/src/bin/cyclos-harness.rs b/taler-cyclos/src/bin/cyclos-harness.rs
@@ -26,9 +26,11 @@ use taler_api::notification::dummy_listen;
use taler_build::long_version;
use taler_common::{
CommonArgs,
- api_common::{EddsaPublicKey, HashCode, ShortHashCode},
- api_params::{History, Page, Pooling},
- api_wire::{IncomingBankTransaction, TransferState},
+ api::{
+ EddsaPublicKey, HashCode, ShortHashCode,
+ params::{History, Page, Pooling},
+ wire::{IncomingBankTransaction, TransferState},
+ },
config::Config,
taler_main,
types::{
diff --git a/taler-cyclos/src/db.rs b/taler-cyclos/src/db.rs
@@ -26,13 +26,15 @@ use taler_api::{
subject::{IncomingSubject, OutgoingSubject, fmt_out_subject},
};
use taler_common::{
- api_common::{HashCode, ShortHashCode},
- api_params::{History, Page},
- api_revenue::RevenueIncomingBankTransaction,
- api_transfer::{RegistrationRequest, Unregistration},
- api_wire::{
- IncomingBankTransaction, OutgoingBankTransaction, TransferListStatus, TransferState,
- TransferStatus,
+ api::{
+ HashCode, ShortHashCode,
+ params::{History, Page},
+ prepared::{RegistrationRequest, Unregistration},
+ revenue::RevenueIncomingBankTransaction,
+ wire::{
+ IncomingBankTransaction, OutgoingBankTransaction, TransferListStatus, TransferState,
+ TransferStatus,
+ },
},
config::Config,
db::IncomingType,
@@ -494,7 +496,7 @@ pub async fn transfer_page(
},
|r: PgRow| {
Ok(TransferListStatus {
- row_id: r.try_get_safeu64(0)?,
+ row_id: r.try_get_u64(0)?,
status: r.try_get(1)?,
amount: r.try_get_amount(2, currency)?,
credit_account: r.try_get_cyclos_fullpaytouri(3, 4, root)?,
@@ -537,7 +539,7 @@ pub async fn outgoing_history(
},
|r: PgRow| {
Ok(OutgoingBankTransaction {
- row_id: r.try_get_safeu64(0)?,
+ row_id: r.try_get_u64(0)?,
amount: r.try_get_amount(1, currency)?,
debit_fee: None,
credit_account: r.try_get_cyclos_fullpaytouri(2, 3, root)?,
@@ -585,7 +587,7 @@ pub async fn incoming_history(
|r: PgRow| {
Ok(match r.try_get(0)? {
IncomingType::reserve => IncomingBankTransaction::Reserve {
- row_id: r.try_get_safeu64(1)?,
+ row_id: r.try_get_u64(1)?,
amount: r.try_get_amount(2, currency)?,
credit_fee: None,
debit_account: r.try_get_cyclos_fullpaytouri(3, 4, root)?,
@@ -595,7 +597,7 @@ pub async fn incoming_history(
authorization_sig: r.try_get(8)?,
},
IncomingType::kyc => IncomingBankTransaction::Kyc {
- row_id: r.try_get_safeu64(1)?,
+ row_id: r.try_get_u64(1)?,
amount: r.try_get_amount(2, currency)?,
credit_fee: None,
debit_account: r.try_get_cyclos_fullpaytouri(3, 4, root)?,
@@ -640,7 +642,7 @@ pub async fn revenue_history(
},
|r: PgRow| {
Ok(RevenueIncomingBankTransaction {
- row_id: r.try_get_safeu64(0)?,
+ row_id: r.try_get_u64(0)?,
date: r.try_get_timestamp(1)?.into(),
amount: r.try_get_amount(2, currency)?,
credit_fee: None,
@@ -921,9 +923,11 @@ mod test {
subject::{IncomingSubject, OutgoingSubject},
};
use taler_common::{
- api_common::{EddsaPublicKey, HashCode, ShortHashCode},
- api_params::{History, Page},
- api_wire::TransferState,
+ api::{
+ EddsaPublicKey, HashCode, ShortHashCode,
+ params::{History, Page},
+ wire::TransferState,
+ },
types::{
amount::{Currency, decimal},
url,
diff --git a/taler-magnet-bank/src/api.rs b/taler-magnet-bank/src/api.rs
@@ -16,25 +16,27 @@
use jiff::Timestamp;
use taler_api::{
- api::{TalerApi, revenue::Revenue, transfer::PreparedTransfer, wire::WireGateway},
- error::{ApiResult, failure, failure_code},
+ api::{TalerApi, prepared::PreparedTransfer, revenue::Revenue, wire::WireGateway},
+ error::{ApiResult, failure_code},
subject::{IncomingSubject, fmt_in_subject},
};
use taler_common::{
- api_common::safe_u64,
- api_params::{History, Page},
- api_revenue::RevenueIncomingHistory,
- api_transfer::{
- RegistrationRequest, RegistrationResponse, SubjectFormat, TransferSubject, Unregistration,
- },
- api_wire::{
- AddIncomingRequest, AddIncomingResponse, AddKycauthRequest, AddMappedRequest,
- IncomingHistory, OutgoingHistory, TransferList, TransferRequest, TransferResponse,
- TransferState, TransferStatus,
+ api::{
+ params::{History, Page},
+ prepared::{
+ RegistrationRequest, RegistrationResponse, SubjectFormat, TransferSubject,
+ Unregistration,
+ },
+ revenue::RevenueIncomingHistory,
+ wire::{
+ AddIncomingRequest, AddIncomingResponse, AddKycauthRequest, AddMappedRequest,
+ IncomingHistory, OutgoingHistory, TransferList, TransferRequest, TransferResponse,
+ TransferState, TransferStatus,
+ },
},
db::IncomingType,
error_code::ErrorCode,
- types::{payto::PaytoURI, timestamp::TalerTimestamp, utils::date_to_utc_ts},
+ types::{amount::Currency, payto::PaytoURI, timestamp::TalerTimestamp, utils::date_to_utc_ts},
};
use tokio::sync::watch::Sender;
@@ -79,8 +81,8 @@ impl MagnetApi {
}
impl TalerApi for MagnetApi {
- fn currency(&self) -> &str {
- CURR.as_ref()
+ fn currency(&self) -> Currency {
+ CURR
}
fn implementation(&self) -> &'static str {
@@ -107,7 +109,7 @@ impl WireGateway for MagnetApi {
match result {
db::TransferResult::Success { id, initiated_at } => Ok(TransferResponse {
timestamp: initiated_at.into(),
- row_id: safe_u64(id),
+ row_id: id,
}),
db::TransferResult::RequestUidReuse => {
Err(failure_code(ErrorCode::BANK_TRANSFER_REQUEST_UID_REUSED))
@@ -173,7 +175,7 @@ impl WireGateway for MagnetApi {
AddIncomingResult::Success {
row_id, valued_at, ..
} => Ok(AddIncomingResponse {
- row_id: safe_u64(row_id),
+ row_id,
timestamp: date_to_utc_ts(&valued_at).into(),
}),
AddIncomingResult::ReservePubReuse => {
@@ -202,7 +204,7 @@ impl WireGateway for MagnetApi {
AddIncomingResult::Success {
row_id, valued_at, ..
} => Ok(AddIncomingResponse {
- row_id: safe_u64(row_id),
+ row_id,
timestamp: date_to_utc_ts(&valued_at).into(),
}),
AddIncomingResult::ReservePubReuse => unreachable!("kyc"),
@@ -229,7 +231,7 @@ impl WireGateway for MagnetApi {
AddIncomingResult::Success {
row_id, valued_at, ..
} => Ok(AddIncomingResponse {
- row_id: safe_u64(row_id),
+ row_id,
timestamp: date_to_utc_ts(&valued_at).into(),
}),
AddIncomingResult::ReservePubReuse => {
@@ -272,9 +274,9 @@ impl PreparedTransfer for MagnetApi {
let simple = TransferSubject::Simple {
credit_amount: req.credit_amount,
subject: if req.authorization_pub == req.account_pub && !req.recurrent {
- fmt_in_subject(req.r#type.into(), &req.account_pub)
+ fmt_in_subject(req.r#type.into(), &req.account_pub).to_string()
} else {
- fmt_in_subject(IncomingType::map, &req.authorization_pub)
+ fmt_in_subject(IncomingType::map, &req.authorization_pub).to_string()
},
};
ApiResult::Ok(RegistrationResponse {
@@ -288,15 +290,8 @@ impl PreparedTransfer for MagnetApi {
}
}
- async fn unregistration(&self, req: Unregistration) -> ApiResult<()> {
- if !db::transfer_unregister(&self.pool, &req).await? {
- Err(failure(
- ErrorCode::BANK_TRANSACTION_NOT_FOUND,
- format!("Prepared transfer '{}' not found", req.authorization_pub),
- ))
- } else {
- Ok(())
- }
+ async fn unregistration(&self, req: Unregistration) -> ApiResult<bool> {
+ Ok(db::transfer_unregister(&self.pool, &req).await?)
}
}
@@ -311,12 +306,18 @@ mod test {
use jiff::{Timestamp, Zoned};
use sqlx::{PgPool, Row as _, postgres::PgRow};
use taler_api::{
- api::TalerRouter as _, auth::AuthMethod, db::TypeHelper as _, subject::OutgoingSubject,
+ api::TalerRouter as _,
+ auth::AuthMethod,
+ db::TypeHelper as _,
+ subject::{IncomingSubject, OutgoingSubject},
};
use taler_common::{
- api_revenue::RevenueConfig,
- api_transfer::PreparedTransferConfig,
- api_wire::{OutgoingHistory, TransferState, WireConfig},
+ api::{
+ EddsaPublicKey,
+ prepared::PreparedTransferConfig,
+ revenue::RevenueConfig,
+ wire::{TransferState, WireConfig},
+ },
db::IncomingType,
types::{
amount::amount,
@@ -327,8 +328,8 @@ mod test {
Router,
db::db_test_setup,
routine::{
- Status, admin_add_incoming_routine, in_history_routine, registration_routine,
- revenue_routine, routine_pagination, transfer_routine,
+ Status, admin_add_incoming_routine, in_history_routine, out_history_routine,
+ registration_routine, revenue_routine, transfer_routine,
},
server::TestServer,
tasks,
@@ -338,7 +339,7 @@ mod test {
FullHuPayto,
api::MagnetApi,
constants::CONFIG_SOURCE,
- db::{self, TxOutKind},
+ db::{self, TxIn, TxOutKind},
magnet_api::types::TxStatus,
magnet_payto,
};
@@ -388,32 +389,79 @@ mod test {
.await;
}
+ static CODE: AtomicU64 = AtomicU64::new(0);
+
+ async fn r#in(db: &PgPool, subject: Option<IncomingSubject>) {
+ db::register_tx_in(
+ &mut db.acquire().await.unwrap(),
+ &TxIn {
+ code: CODE.fetch_add(1, Ordering::Relaxed),
+ amount: amount("EUR:10"),
+ subject: "subject".into(),
+ debtor: magnet_payto(
+ "payto://iban/HU30162000031000163100000000?receiver-name=name",
+ ),
+ value_date: Zoned::now().date(),
+ status: TxStatus::Completed,
+ },
+ &subject,
+ &Timestamp::now(),
+ )
+ .await
+ .unwrap();
+ }
+
+ async fn in_malformed(db: &PgPool) {
+ r#in(db, None).await
+ }
+
+ async fn in_talerable(db: &PgPool) {
+ r#in(db, Some(IncomingSubject::Reserve(EddsaPublicKey::rand()))).await
+ }
+
+ async fn out(db: &PgPool, kind: &TxOutKind) {
+ db::register_tx_out(
+ &mut db.acquire().await.unwrap(),
+ &db::TxOut {
+ code: CODE.fetch_add(1, Ordering::Relaxed),
+ amount: amount("EUR:10"),
+ subject: "subject".into(),
+ creditor: PAYTO.clone(),
+ value_date: Zoned::now().date(),
+ status: TxStatus::Completed,
+ },
+ kind,
+ &Timestamp::now(),
+ )
+ .await
+ .unwrap();
+ }
+
+ async fn out_talerable(db: &PgPool) {
+ out(db, &TxOutKind::Talerable(OutgoingSubject::rand())).await
+ }
+
+ async fn out_bounce(db: &PgPool) {
+ out(db, &TxOutKind::Bounce(CODE.load(Ordering::Relaxed) as u32)).await
+ }
+
+ async fn out_malformed(db: &PgPool) {
+ out(db, &TxOutKind::Simple).await
+ }
+
#[tokio::test]
async fn outgoing_history() {
- let (server, pool) = setup().await;
- static CODE: AtomicU64 = AtomicU64::new(0);
- routine_pagination::<OutgoingHistory>(
+ let (server, db) = &setup().await;
+
+ out_history_routine(
&server,
- "/taler-wire-gateway/history/outgoing",
- tasks!({
- let mut conn = pool.acquire().await.unwrap();
- let now = Zoned::now().date();
- db::register_tx_out(
- &mut conn,
- &db::TxOut {
- code: CODE.fetch_add(1, Ordering::Relaxed),
- amount: amount("EUR:10"),
- subject: "subject".into(),
- creditor: PAYTO.clone(),
- value_date: now,
- status: TxStatus::Completed,
- },
- &TxOutKind::Talerable(OutgoingSubject::rand()),
- &Timestamp::now(),
- )
- .await
- .unwrap();
- }),
+ tasks!({ out_talerable(db).await }),
+ tasks!(
+ { out_bounce(db).await },
+ { out_malformed(db).await },
+ { in_malformed(db).await },
+ { in_talerable(db).await }
+ ),
)
.await;
}
@@ -426,14 +474,35 @@ mod test {
#[tokio::test]
async fn in_history() {
- let (server, _) = setup().await;
- in_history_routine(&server, &ACCOUNT, true).await;
+ let (server, db) = &setup().await;
+ in_history_routine(
+ &server,
+ &ACCOUNT,
+ true,
+ tasks!({ in_talerable(db).await }),
+ tasks!(
+ { out_malformed(db).await },
+ { out_talerable(db).await },
+ { out_bounce(db).await },
+ { in_malformed(db).await }
+ ),
+ )
+ .await;
}
#[tokio::test]
async fn revenue() {
- let (server, _) = setup().await;
- revenue_routine(&server, &ACCOUNT, true).await;
+ let (server, db) = &setup().await;
+ revenue_routine(
+ &server,
+ &ACCOUNT,
+ true,
+ tasks!({ in_malformed(db).await }, { in_talerable(db).await },),
+ tasks!({ out_malformed(db).await }, { out_talerable(db).await }, {
+ out_bounce(db).await
+ }),
+ )
+ .await;
}
async fn check_in(pool: &PgPool) -> Vec<Status> {
diff --git a/taler-magnet-bank/src/bin/magnet-bank-harness.rs b/taler-magnet-bank/src/bin/magnet-bank-harness.rs
@@ -26,9 +26,11 @@ use taler_api::notification::dummy_listen;
use taler_build::long_version;
use taler_common::{
CommonArgs,
- api_common::{EddsaPublicKey, HashCode, ShortHashCode},
- api_params::{History, Page, Pooling},
- api_wire::{IncomingBankTransaction, TransferState},
+ api::{
+ EddsaPublicKey, HashCode, ShortHashCode,
+ params::{History, Page, Pooling},
+ wire::{IncomingBankTransaction, TransferState},
+ },
config::Config,
db::{dbinit, pool},
taler_main,
diff --git a/taler-magnet-bank/src/db.rs b/taler-magnet-bank/src/db.rs
@@ -26,13 +26,15 @@ use taler_api::{
subject::{IncomingSubject, OutgoingSubject, fmt_out_subject},
};
use taler_common::{
- api_common::{HashCode, ShortHashCode},
- api_params::{History, Page},
- api_revenue::RevenueIncomingBankTransaction,
- api_transfer::{RegistrationRequest, Unregistration},
- api_wire::{
- IncomingBankTransaction, OutgoingBankTransaction, TransferListStatus, TransferState,
- TransferStatus,
+ api::{
+ HashCode, ShortHashCode,
+ params::{History, Page},
+ prepared::{RegistrationRequest, Unregistration},
+ revenue::RevenueIncomingBankTransaction,
+ wire::{
+ IncomingBankTransaction, OutgoingBankTransaction, TransferListStatus, TransferState,
+ TransferStatus,
+ },
},
config::Config,
db::IncomingType,
@@ -312,7 +314,7 @@ pub async fn register_tx_out(
serialized!({
let query = sqlx::query(
"
- SELECT out_result, out_tx_row_id
+ SELECT out_result, out_tx_row_id
FROM register_tx_out($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
",
)
@@ -366,7 +368,7 @@ pub async fn register_tx_out_failure(
serialized!(
sqlx::query(
"
- SELECT out_new, out_initiated_id
+ SELECT out_new, out_initiated_id
FROM register_tx_out_failure($1, $2, $3)
",
)
@@ -498,7 +500,7 @@ pub async fn transfer_page(
credit_account,
credit_name,
initiated_at
- FROM transfer
+ FROM transfer
JOIN initiated USING (initiated_id)
WHERE
",
@@ -510,7 +512,7 @@ pub async fn transfer_page(
},
|r: PgRow| {
Ok(TransferListStatus {
- row_id: r.try_get_safeu64(0)?,
+ row_id: r.try_get_u64(0)?,
status: r.try_get(1)?,
amount: r.try_get_amount(2, &CURR)?,
credit_account: r.try_get_iban(3)?.as_full_uri(r.try_get(4)?),
@@ -551,7 +553,7 @@ pub async fn outgoing_history(
},
|r: PgRow| {
Ok(OutgoingBankTransaction {
- row_id: r.try_get_safeu64(0)?,
+ row_id: r.try_get_u64(0)?,
amount: r.try_get_amount(1, &CURR)?,
debit_fee: None,
credit_account: r.try_get_iban(2)?.as_full_uri(r.try_get(3)?),
@@ -597,7 +599,7 @@ pub async fn incoming_history(
|r: PgRow| {
Ok(match r.try_get(0)? {
IncomingType::reserve => IncomingBankTransaction::Reserve {
- row_id: r.try_get_safeu64(1)?,
+ row_id: r.try_get_u64(1)?,
amount: r.try_get_amount(2, &CURR)?,
credit_fee: None,
debit_account: r.try_get_iban(3)?.as_full_uri(r.try_get(4)?),
@@ -607,7 +609,7 @@ pub async fn incoming_history(
authorization_sig: r.try_get(8)?,
},
IncomingType::kyc => IncomingBankTransaction::Kyc {
- row_id: r.try_get_safeu64(1)?,
+ row_id: r.try_get_u64(1)?,
amount: r.try_get_amount(2, &CURR)?,
credit_fee: None,
debit_account: r.try_get_iban(3)?.as_full_uri(r.try_get(4)?),
@@ -650,7 +652,7 @@ pub async fn revenue_history(
},
|r: PgRow| {
Ok(RevenueIncomingBankTransaction {
- row_id: r.try_get_safeu64(0)?,
+ row_id: r.try_get_u64(0)?,
date: r.try_get_timestamp(1)?.into(),
amount: r.try_get_amount(2, &CURR)?,
credit_fee: None,
@@ -676,8 +678,8 @@ pub async fn transfer_by_id(db: &PgPool, id: u64) -> sqlx::Result<Option<Transfe
credit_account,
credit_name,
initiated_at
- FROM transfer
- JOIN initiated USING (initiated_id)
+ FROM transfer
+ JOIN initiated USING (initiated_id)
WHERE initiated_id = $1
",
)
@@ -707,7 +709,7 @@ pub async fn pending_batch(
sqlx::query(
"
SELECT initiated_id, amount, subject, credit_account, credit_name
- FROM initiated
+ FROM initiated
WHERE magnet_code IS NULL
AND status='pending'
AND (last_submitted IS NULL OR last_submitted < $1)
@@ -736,7 +738,7 @@ pub async fn initiated_by_code(
sqlx::query(
"
SELECT initiated_id, amount, subject, credit_account, credit_name
- FROM initiated
+ FROM initiated
WHERE magnet_code IS $1
",
)
@@ -887,8 +889,10 @@ mod test {
subject::{IncomingSubject, OutgoingSubject},
};
use taler_common::{
- api_common::{EddsaPublicKey, HashCode, ShortHashCode},
- api_params::{History, Page},
+ api::{
+ EddsaPublicKey, HashCode, ShortHashCode,
+ params::{History, Page},
+ },
types::{
amount::{amount, decimal},
url,