commit 6c313f6bf54c394179988f761e87cbbbf27290b9
parent a21fa61c42d0c20807af46ef435f529f87889edd
Author: Antoine A <>
Date: Wed, 22 Jan 2025 15:43:43 +0100
magnet: magnet bank payto and better errors
Diffstat:
15 files changed, 404 insertions(+), 241 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -142,9 +142,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "axum"
-version = "0.8.1"
+version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
+checksum = "efea76243612a2436fb4074ba0cf3ba9ea29efdeb72645d8fc63f116462be1de"
dependencies = [
"axum-core",
"bytes",
@@ -176,12 +176,12 @@ dependencies = [
[[package]]
name = "axum-core"
-version = "0.5.0"
+version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733"
+checksum = "eab1b0df7cded837c40dacaa2e1c33aa17c84fc3356ae67b5645f1e83190753e"
dependencies = [
"bytes",
- "futures-util",
+ "futures-core",
"http 1.2.0",
"http-body",
"http-body-util",
@@ -540,9 +540,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
-version = "0.2.2"
+version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
+checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
[[package]]
name = "crypto-bigint"
@@ -1360,13 +1360,13 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]]
name = "is-terminal"
-version = "0.4.13"
+version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b"
+checksum = "3f187290c0ed3dfe3f7c85bedddd320949b68fc86ca0ceb71adfb05b3dc3af2a"
dependencies = [
"hermit-abi",
"libc",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -1392,9 +1392,9 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "jiff"
-version = "0.1.24"
+version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2bb0c2e28117985a4d90e3bc70092bc8f226f434c7ec7e23dd9ff99c5c5721a"
+checksum = "fb73dbeee753ae9411475ddd8861765fa7f25fe1eebf180c24e1bbabef3fbdcd"
dependencies = [
"log",
"portable-atomic",
@@ -2076,9 +2076,9 @@ dependencies = [
[[package]]
name = "rustix"
-version = "0.38.43"
+version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6"
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
"bitflags",
"errno",
@@ -2552,6 +2552,7 @@ dependencies = [
"listenfd",
"serde",
"serde_json",
+ "serde_path_to_error",
"sqlx",
"taler-common",
"test-utils",
diff --git a/common/taler-api/Cargo.toml b/common/taler-api/Cargo.toml
@@ -20,6 +20,7 @@ serde.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
serde_json.workspace = true
+serde_path_to_error.workspace = true
axum.workspace = true
url.workspace = true
thiserror.workspace = true
diff --git a/common/taler-api/src/error.rs b/common/taler-api/src/error.rs
@@ -1,6 +1,6 @@
/*
This file is part of TALER
- Copyright (C) 2024 Taler Systems SA
+ Copyright (C) 2024-2025 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
@@ -19,7 +19,9 @@ use axum::{
response::{IntoResponse, Response},
Json,
};
-use taler_common::{api_common::ErrorDetail, api_params::ParamsError, error_code::ErrorCode};
+use taler_common::{
+ api_common::ErrorDetail, api_params::ParamsErr, error_code::ErrorCode, types::payto::PaytoErr,
+};
pub type ApiResult<T> = Result<T, ApiError>;
@@ -28,6 +30,7 @@ pub struct ApiError {
hint: Option<String>,
log: Option<String>,
status: Option<StatusCode>,
+ path: Option<String>,
}
impl From<sqlx::Error> for ApiError {
@@ -50,17 +53,44 @@ impl From<sqlx::Error> for ApiError {
hint: None,
status: Some(status),
log: Some(format!("db: {value}")),
+ path: None,
+ }
+ }
+}
+
+impl From<PaytoErr> for ApiError {
+ fn from(value: PaytoErr) -> Self {
+ Self {
+ code: ErrorCode::GENERIC_PAYTO_URI_MALFORMED,
+ hint: Some(value.to_string()),
+ log: None,
+ status: None,
+ path: None,
}
}
}
-impl From<ParamsError> for ApiError {
- fn from(value: ParamsError) -> Self {
+impl From<ParamsErr> for ApiError {
+ fn from(value: ParamsErr) -> Self {
Self {
code: ErrorCode::GENERIC_PARAMETER_MALFORMED,
- hint: Some(value.0),
+ hint: Some(value.to_string()),
+ log: None,
+ status: None,
+ path: Some(value.param.to_owned()),
+ }
+ }
+}
+
+impl From<serde_path_to_error::Error<serde_json::Error>> for ApiError {
+ fn from(value: serde_path_to_error::Error<serde_json::Error>) -> Self {
+ let e = value.inner();
+ Self {
+ code: ErrorCode::GENERIC_JSON_INVALID,
+ hint: Some(e.to_string()),
log: None,
status: None,
+ path: Some(value.path().to_string()),
}
}
}
@@ -82,7 +112,7 @@ impl IntoResponse for ApiError {
hint: self.hint,
detail: None,
parameter: None,
- path: None,
+ path: self.path,
offset: None,
index: None,
object: None,
@@ -102,6 +132,7 @@ pub fn failure_code(code: ErrorCode) -> ApiError {
hint: None,
log: None,
status: None,
+ path: None,
}
}
@@ -111,6 +142,7 @@ pub fn failure(code: ErrorCode, hint: impl Into<String>) -> ApiError {
hint: Some(hint.into()),
log: None,
status: None,
+ path: None,
}
}
@@ -120,5 +152,6 @@ pub fn failure_status(code: ErrorCode, hint: impl Into<String>, status: StatusCo
hint: Some(hint.into()),
log: None,
status: Some(status),
+ path: None,
}
}
diff --git a/common/taler-api/src/json.rs b/common/taler-api/src/json.rs
@@ -0,0 +1,128 @@
+use axum::{
+ body::Bytes,
+ extract::{FromRequest, Request},
+ http::header,
+};
+use http_body_util::BodyExt as _;
+use serde::de::DeserializeOwned;
+use taler_common::error_code::ErrorCode;
+
+use crate::{
+ constants::MAX_BODY_LENGTH,
+ error::{failure, ApiError},
+};
+
+#[derive(Debug, Clone, Copy, Default)]
+#[must_use]
+pub struct Req<T>(pub T);
+
+impl<T, S> FromRequest<S> for Req<T>
+where
+ T: DeserializeOwned,
+ S: Send + Sync,
+{
+ type Rejection = ApiError;
+
+ async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
+ // TODO UNSUPPORTED_MEDIA_TYPE & UNPROCESSABLE_ENTITY
+ // Check content type
+ match req
+ .headers()
+ .get(header::CONTENT_TYPE)
+ .map(|it| it == "application/json")
+ {
+ Some(true) => {}
+ Some(false) => {
+ return Err(failure(
+ ErrorCode::GENERIC_JSON_INVALID,
+ "Bad Content-Type header",
+ ))
+ }
+ None => {
+ return Err(failure(
+ ErrorCode::GENERIC_JSON_INVALID,
+ "Missing Content-Type header",
+ ))
+ }
+ }
+
+ // Check content length if present and wellformed
+ if let Some(lenght) = req
+ .headers()
+ .get(header::CONTENT_LENGTH)
+ .and_then(|it| it.to_str().ok())
+ .and_then(|it| it.parse::<usize>().ok())
+ {
+ if lenght > MAX_BODY_LENGTH {
+ return Err(failure(
+ ErrorCode::GENERIC_JSON_INVALID,
+ format!("Body is suspiciously big > {MAX_BODY_LENGTH}B"),
+ ));
+ }
+ }
+
+ // Check compression
+ let compressed = if let Some(encoding) = req.headers().get(header::CONTENT_ENCODING) {
+ if encoding == "deflate" {
+ true
+ } else {
+ return Err(failure(
+ ErrorCode::GENERIC_COMPRESSION_INVALID,
+ format!(
+ "Unsupported encoding '{}'",
+ String::from_utf8_lossy(encoding.as_bytes())
+ ),
+ ));
+ }
+ } else {
+ false
+ };
+
+ // Buffer body
+ let (_, body) = req.into_parts();
+ let body = http_body_util::Limited::new(body, MAX_BODY_LENGTH);
+ let bytes = match body.collect().await {
+ Ok(chunks) => chunks.to_bytes(),
+ Err(it) => match it.downcast::<http_body_util::LengthLimitError>() {
+ Ok(_) => {
+ return Err(failure(
+ ErrorCode::GENERIC_JSON_INVALID,
+ format!("Body is suspiciously big > {MAX_BODY_LENGTH}B"),
+ ))
+ }
+ Err(err) => {
+ return Err(failure(
+ ErrorCode::GENERIC_UNEXPECTED_REQUEST_ERROR,
+ format!("Failed to read body: {}", err),
+ ))
+ }
+ },
+ };
+
+ let bytes = if compressed {
+ let mut buf = vec![0; MAX_BODY_LENGTH];
+ match libdeflater::Decompressor::new().zlib_decompress(&bytes, &mut buf) {
+ Ok(it) => Bytes::copy_from_slice(&buf[..it]),
+ Err(it) => match it {
+ libdeflater::DecompressionError::BadData => {
+ return Err(failure(
+ ErrorCode::GENERIC_COMPRESSION_INVALID,
+ "Failed to decompress body: invalid gzip",
+ ))
+ }
+ libdeflater::DecompressionError::InsufficientSpace => {
+ return Err(failure(
+ ErrorCode::GENERIC_JSON_INVALID,
+ format!("Body is suspiciously big > {MAX_BODY_LENGTH}B"),
+ ))
+ }
+ },
+ }
+ } else {
+ bytes
+ };
+ let mut de = serde_json::de::Deserializer::from_slice(&bytes);
+ let parsed = serde_path_to_error::deserialize(&mut de)?;
+ Ok(Req(parsed))
+ }
+}
diff --git a/common/taler-api/src/lib.rs b/common/taler-api/src/lib.rs
@@ -28,19 +28,17 @@ use std::{
use auth::{auth_middleware, AuthMethod};
use axum::{
- body::Bytes,
- extract::{FromRequest, Path, Query, Request, State},
- http::{header, StatusCode},
+ extract::{Path, Query, Request, State},
+ http::StatusCode,
middleware::{self, Next},
response::{IntoResponse, Response},
routing::{get, post},
Json, Router,
};
-use constants::{MAX_BODY_LENGTH, MAX_PAGE_SIZE, MAX_TIMEOUT_MS, WIRE_GATEWAY_API_VERSION};
-use error::{failure, failure_code, ApiError, ApiResult};
-use http_body_util::BodyExt;
+use constants::{MAX_PAGE_SIZE, MAX_TIMEOUT_MS, WIRE_GATEWAY_API_VERSION};
+use error::{failure, failure_code, ApiResult};
+use json::Req;
use listenfd::ListenFd;
-use serde::de::DeserializeOwned;
use taler_common::{
api_params::{History, HistoryParams, Page, TransferParams},
api_wire::{
@@ -61,125 +59,10 @@ pub mod auth;
mod constants;
pub mod db;
pub mod error;
+pub mod json;
pub mod notification;
pub mod subject;
-#[derive(Debug, Clone, Copy, Default)]
-#[must_use]
-pub struct Req<T>(pub T);
-
-impl<T, S> FromRequest<S> for Req<T>
-where
- T: DeserializeOwned,
- S: Send + Sync,
-{
- type Rejection = ApiError;
-
- async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
- // TODO UNSUPPORTED_MEDIA_TYPE & UNPROCESSABLE_ENTITY
- // Check content type
- match req
- .headers()
- .get(header::CONTENT_TYPE)
- .map(|it| it == "application/json")
- {
- Some(true) => {}
- Some(false) => {
- return Err(failure(
- ErrorCode::GENERIC_JSON_INVALID,
- "Bad Content-Type header",
- ))
- }
- None => {
- return Err(failure(
- ErrorCode::GENERIC_JSON_INVALID,
- "Missing Content-Type header",
- ))
- }
- }
-
- // Check content length if present and wellformed
- if let Some(lenght) = req
- .headers()
- .get(header::CONTENT_LENGTH)
- .and_then(|it| it.to_str().ok())
- .and_then(|it| it.parse::<usize>().ok())
- {
- if lenght > MAX_BODY_LENGTH {
- return Err(failure(
- ErrorCode::GENERIC_JSON_INVALID,
- format!("Body is suspiciously big > {MAX_BODY_LENGTH}B"),
- ));
- }
- }
-
- // Check compression
- let compressed = if let Some(encoding) = req.headers().get(header::CONTENT_ENCODING) {
- if encoding == "deflate" {
- true
- } else {
- return Err(failure(
- ErrorCode::GENERIC_COMPRESSION_INVALID,
- format!(
- "Unsupported encoding '{}'",
- String::from_utf8_lossy(encoding.as_bytes())
- ),
- ));
- }
- } else {
- false
- };
-
- // Buffer body
- let (_, body) = req.into_parts();
- let body = http_body_util::Limited::new(body, MAX_BODY_LENGTH);
- let bytes = match body.collect().await {
- Ok(chunks) => chunks.to_bytes(),
- Err(it) => match it.downcast::<http_body_util::LengthLimitError>() {
- Ok(_) => {
- return Err(failure(
- ErrorCode::GENERIC_JSON_INVALID,
- format!("Body is suspiciously big > {MAX_BODY_LENGTH}B"),
- ))
- }
- Err(err) => {
- return Err(failure(
- ErrorCode::GENERIC_UNEXPECTED_REQUEST_ERROR,
- format!("Failed to read body: {}", err),
- ))
- }
- },
- };
-
- let bytes = if compressed {
- let mut buf = vec![0; MAX_BODY_LENGTH];
- match libdeflater::Decompressor::new().zlib_decompress(&bytes, &mut buf) {
- Ok(it) => Bytes::copy_from_slice(&buf[..it]),
- Err(it) => match it {
- libdeflater::DecompressionError::BadData => {
- return Err(failure(
- ErrorCode::GENERIC_COMPRESSION_INVALID,
- "Failed to decompress body: invalid gzip",
- ))
- }
- libdeflater::DecompressionError::InsufficientSpace => {
- return Err(failure(
- ErrorCode::GENERIC_JSON_INVALID,
- format!("Body is suspiciously big > {MAX_BODY_LENGTH}B"),
- ))
- }
- },
- }
- } else {
- bytes
- };
- match serde_json::de::from_slice(&bytes) {
- Ok(it) => Ok(Req(it)),
- Err(err) => Err(failure(ErrorCode::GENERIC_JSON_INVALID, format!("{err}"))),
- }
- }
-}
-
pub trait WireGatewayImpl: Send + Sync {
fn name(&self) -> &str;
fn currency(&self) -> &str;
diff --git a/common/taler-common/src/api_params.rs b/common/taler-common/src/api_params.rs
@@ -20,8 +20,15 @@ use serde_with::{serde_as, DisplayFromStr};
use crate::api_wire::TransferState;
#[derive(Debug, thiserror::Error)]
-#[error("{0}")]
-pub struct ParamsError(pub String);
+#[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)]
@@ -36,22 +43,22 @@ pub struct PageParams {
}
impl PageParams {
- pub fn check(self, max_page_size: i64) -> Result<Page, ParamsError> {
+ pub fn check(self, max_page_size: i64) -> Result<Page, ParamsErr> {
let limit = self.limit.unwrap_or(-20);
if limit == 0 {
- return Err(ParamsError(format!(
- "Param 'limit' must be non-zero got {limit}"
- )));
+ return Err(param_err("limit", format!("must be non-zero got {limit}")));
} else if limit > max_page_size {
- return Err(ParamsError(format!(
- "Param 'limit' must be <= {max_page_size} for {limit}"
- )));
+ return Err(param_err(
+ "limit",
+ format!("must be <= {max_page_size} for {limit}"),
+ ));
}
if let Some(offset) = self.offset {
if offset < 0 {
- return Err(ParamsError(format!(
- "Param 'offset' must be positive got {offset}"
- )));
+ return Err(param_err(
+ "offset",
+ format!("must be positive got {offset}"),
+ ));
}
}
@@ -93,7 +100,7 @@ pub struct HistoryParams {
}
impl HistoryParams {
- pub fn check(self, max_page_size: i64, max_timeout_ms: u64) -> Result<History, ParamsError> {
+ pub fn check(self, max_page_size: i64, max_timeout_ms: u64) -> Result<History, ParamsErr> {
let timeout_ms = self.timeout_ms.map(|it| it.min(max_timeout_ms));
Ok(History {
page: self.pagination.check(max_page_size)?,
diff --git a/common/taler-common/src/types/payto.rs b/common/taler-common/src/types/payto.rs
@@ -14,8 +14,11 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-use serde::{de::DeserializeOwned, Deserialize};
-use std::{fmt::Debug, str::FromStr};
+use serde::{de::DeserializeOwned, Deserialize, Serialize};
+use std::{
+ fmt::{Debug, Display},
+ str::FromStr,
+};
use url::Url;
/// Parse a payto URI, panic if malformed
@@ -39,6 +42,11 @@ impl Payto {
let de = serde_urlencoded::Deserializer::new(url::form_urlencoded::parse(query));
serde_path_to_error::deserialize(de).map_err(PaytoErr::Query)
}
+
+ pub fn from_parts(domain_path: impl Display, query: impl Serialize) -> Self {
+ let query = serde_urlencoded::to_string(query).unwrap();
+ payto(format!("payto://{domain_path}?{query}"))
+ }
}
impl AsRef<Url> for Payto {
diff --git a/common/test-utils/src/routine.rs b/common/test-utils/src/routine.rs
@@ -469,13 +469,15 @@ async fn add_incoming_routine(
}))
.await
.assert_error(ErrorCode::GENERIC_JSON_INVALID);
- /*
+
// Bad payto kind
- client.postA("/taler-wire-gateway/admin/$path") {
- json(valid_req) {
- "debit_account" to "payto://x-taler-bank/bank.hostname.test/bar"
- }
- }.assertBadRequest()*/
+ server
+ .post(path)
+ .json(&json!(valid_req + {
+ "debit_account": "http://email@test.com"
+ }))
+ .await
+ .assert_error(ErrorCode::GENERIC_JSON_INVALID);
}
/// Test standard behavior of the admin add incoming endpoints
diff --git a/wire-gateway/magnet-bank/db/schema.sql b/wire-gateway/magnet-bank/db/schema.sql
@@ -21,7 +21,8 @@ CREATE TABLE tx_in(
magnet_code INT8 UNIQUE,
amount taler_amount NOT NULL,
subject TEXT NOT NULL,
- debit_payto TEXT NOT NULL,
+ debit_account TEXT NOT NULL,
+ debit_name TEXT NOT NULL,
created INT8 NOT NULL
);
COMMENT ON TABLE tx_in IS 'Incoming transactions';
@@ -31,7 +32,8 @@ CREATE TABLE tx_out(
magnet_code INT8 UNIQUE,
amount taler_amount NOT NULL,
subject TEXT NOT NULL,
- credit_payto TEXT NOT NULL,
+ credit_account TEXT NOT NULL,
+ credit_name TEXT NOT NULL,
created INT8 NOT NULL
);
COMMENT ON TABLE tx_in IS 'Outgoing transactions';
@@ -76,7 +78,8 @@ CREATE TABLE initiated(
initiated_id INT8 PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
amount taler_amount NOT NULL,
subject TEXT NOT NULL,
- credit_payto TEXT NOT NULL,
+ credit_account TEXT NOT NULL,
+ credit_name TEXT NOT NULL,
status transfer_status NOT NULL DEFAULT 'pending',
status_msg TEXT,
magnet_code INT8 UNIQUE,
@@ -99,7 +102,8 @@ CREATE FUNCTION register_tx_in(
IN in_code INT8,
IN in_amount taler_amount,
IN in_subject TEXT,
- IN in_debit_payto TEXT,
+ IN in_debit_account TEXT,
+ IN in_debit_name TEXT,
IN in_timestamp INT8,
IN in_type incoming_type,
IN in_metadata BYTEA,
@@ -117,7 +121,7 @@ SELECT tx_in_id, created
INTO out_tx_row_id, out_timestamp
FROM tx_in
WHERE (in_code IS NOT NULL AND magnet_code = in_code) -- Magnet transaction
- OR (in_code IS NULL AND amount = in_amount AND debit_payto = in_debit_payto AND subject = in_subject); -- Admin transaction
+ OR (in_code IS NULL AND amount = in_amount AND debit_account = in_debit_account AND subject = in_subject); -- Admin transaction
out_new = NOT found;
IF NOT out_new THEN
out_reserve_pub_reuse=false;
@@ -136,13 +140,15 @@ INSERT INTO tx_in (
magnet_code,
amount,
subject,
- debit_payto,
+ debit_account,
+ debit_name,
created
) VALUES (
in_code,
in_amount,
in_subject,
- in_debit_payto,
+ in_debit_account,
+ in_debit_name,
in_timestamp
)
RETURNING tx_in_id, created
@@ -170,7 +176,8 @@ CREATE FUNCTION register_tx_out(
IN in_code INT8,
IN in_amount taler_amount,
IN in_subject TEXT,
- IN in_credit_payto TEXT,
+ IN in_credit_account TEXT,
+ IN in_credit_name TEXT,
IN in_timestamp INT8,
IN in_wtid BYTEA,
IN in_origin_exchange_url TEXT,
@@ -193,13 +200,15 @@ IF out_new THEN
magnet_code,
amount,
subject,
- credit_payto,
+ credit_account,
+ credit_name,
created
) VALUES (
in_code,
in_amount,
in_subject,
- in_credit_payto,
+ in_credit_account,
+ in_credit_name,
in_timestamp
)
RETURNING tx_out_id, created
@@ -231,7 +240,8 @@ CREATE FUNCTION taler_transfer(
IN in_subject TEXT,
IN in_amount taler_amount,
IN in_exchange_base_url TEXT,
- IN in_credit_payto TEXT,
+ IN in_credit_account TEXT,
+ IN in_credit_name TEXT,
IN in_timestamp INT8,
-- Error return
OUT out_request_uid_reuse BOOLEAN,
@@ -244,7 +254,7 @@ LANGUAGE plpgsql AS $$
BEGIN
-- Check for idempotence and conflict
SELECT (amount != in_amount
- OR credit_payto != in_credit_payto
+ OR credit_account != in_credit_account
OR exchange_base_url != in_exchange_base_url
OR wtid != in_wtid)
,transfer.initiated_id, created
@@ -265,12 +275,14 @@ END IF;
INSERT INTO initiated (
amount,
subject,
- credit_payto,
+ credit_account,
+ credit_name,
created
) VALUES (
in_amount,
in_subject,
- in_credit_payto,
+ in_credit_account,
+ in_credit_name,
in_timestamp
) RETURNING initiated_id, created
INTO out_tx_row_id, out_timestamp;
diff --git a/wire-gateway/magnet-bank/src/db.rs b/wire-gateway/magnet-bank/src/db.rs
@@ -27,11 +27,11 @@ use taler_common::{
IncomingBankTransaction, OutgoingBankTransaction, TransferListStatus, TransferRequest,
TransferState, TransferStatus,
},
- types::{amount::Amount, payto::Payto, timestamp::Timestamp},
+ types::{amount::Amount, timestamp::Timestamp},
};
use tokio::sync::watch::{Receiver, Sender};
-use crate::constant::CURRENCY;
+use crate::{constant::CURRENCY, MagnetPayto};
pub async fn notification_listener(
pool: PgPool,
@@ -61,7 +61,7 @@ pub struct TxIn {
pub code: u64,
pub amount: Amount,
pub subject: String,
- pub debtor: Payto,
+ pub debtor: MagnetPayto,
pub timestamp: Timestamp,
}
@@ -83,7 +83,7 @@ pub struct TxOut {
pub code: u64,
pub amount: Amount,
pub subject: String,
- pub creditor: Payto,
+ pub creditor: MagnetPayto,
pub timestamp: Timestamp,
}
@@ -104,7 +104,7 @@ impl Display for TxOut {
pub struct TxInAdmin {
pub amount: Amount,
pub subject: String,
- pub debit_payto: Payto,
+ pub debtor: MagnetPayto,
pub timestamp: Timestamp,
pub metadata: IncomingSubject,
}
@@ -127,7 +127,7 @@ pub struct Initiated {
pub id: u64,
pub amount: Amount,
pub subject: String,
- pub creditor: Payto,
+ pub creditor: MagnetPayto,
}
impl Display for Initiated {
@@ -159,12 +159,13 @@ pub async fn register_tx_in_admin(db: &PgPool, tx: &TxInAdmin) -> sqlx::Result<A
sqlx::query(
"
SELECT out_reserve_pub_reuse, out_new, out_tx_row_id, out_timestamp
- FROM register_tx_in(NULL, ($1, $2)::taler_amount, $3, $4, $5, $6, $7)
+ FROM register_tx_in(NULL, ($1, $2)::taler_amount, $3, $4, $5, $6, $7, $8)
",
)
.bind_amount(&tx.amount)
.bind(&tx.subject)
- .bind(tx.debit_payto.raw())
+ .bind(&tx.debtor.number)
+ .bind(&tx.debtor.name)
.bind_timestamp(&tx.timestamp)
.bind(tx.metadata.ty())
.bind(tx.metadata.key())
@@ -191,13 +192,14 @@ pub async fn register_tx_in(
sqlx::query(
"
SELECT out_reserve_pub_reuse, out_new, out_tx_row_id, out_timestamp
- FROM register_tx_in($1, ($2, $3)::taler_amount, $4, $5, $6, $7, $8)
+ FROM register_tx_in($1, ($2, $3)::taler_amount, $4, $5, $6, $7, $8, $9)
",
)
.bind(tx.code as i64)
.bind_amount(&tx.amount)
.bind(&tx.subject)
- .bind(tx.debtor.raw())
+ .bind(&tx.debtor.number)
+ .bind(&tx.debtor.name)
.bind_timestamp(&tx.timestamp)
.bind(subject.as_ref().map(|it| it.ty()))
.bind(subject.as_ref().map(|it| it.key()))
@@ -224,13 +226,14 @@ pub async fn register_tx_out(
sqlx::query(
"
SELECT out_new, out_tx_row_id, out_timestamp
- FROM register_tx_out($1, ($2, $3)::taler_amount, $4, $5, $6, $7, $8)
+ FROM register_tx_out($1, ($2, $3)::taler_amount, $4, $5, $6, $7, $8, $9)
",
)
.bind(tx.code as i64)
.bind_amount(&tx.amount)
.bind(&tx.subject)
- .bind(tx.creditor.raw())
+ .bind(&tx.creditor.number)
+ .bind(&tx.creditor.name)
.bind_timestamp(&tx.timestamp)
.bind(subject.as_ref().map(|it| it.0.as_ref()))
.bind(subject.as_ref().map(|it| it.1.as_str()))
@@ -255,13 +258,14 @@ pub enum TransferResult {
pub async fn make_transfer<'a>(
db: impl PgExecutor<'a>,
req: &TransferRequest,
+ creditor: &MagnetPayto,
timestamp: &Timestamp,
) -> sqlx::Result<TransferResult> {
let subject = format!("{} {}", req.wtid, req.exchange_base_url);
sqlx::query(
"
SELECT out_request_uid_reuse, out_wtid_reuse, out_tx_row_id, out_timestamp
- FROM taler_transfer($1, $2, $3, ($4, $5)::taler_amount, $6, $7, $8)
+ FROM taler_transfer($1, $2, $3, ($4, $5)::taler_amount, $6, $7, $8, $9)
",
)
.bind(req.request_uid.as_ref())
@@ -269,7 +273,8 @@ pub async fn make_transfer<'a>(
.bind(&subject)
.bind_amount(&req.amount)
.bind(req.exchange_base_url.as_str())
- .bind(req.credit_account.raw())
+ .bind(&creditor.number)
+ .bind(&creditor.name)
.bind_timestamp(timestamp)
.try_map(|r: PgRow| {
Ok(if r.try_get(0)? {
@@ -304,7 +309,8 @@ pub async fn transfer_page<'a>(
status,
(amount).val as amount_val,
(amount).frac as amount_frac,
- credit_payto,
+ credit_account,
+ credit_name,
created
FROM transfer
JOIN initiated USING (initiated_id)
@@ -321,8 +327,12 @@ pub async fn transfer_page<'a>(
row_id: r.try_get_safeu64(0)?,
status: r.try_get(1)?,
amount: r.try_get_amount_i(2, CURRENCY)?,
- credit_account: r.try_get_payto(4)?,
- timestamp: r.try_get_timestamp(5)?,
+ credit_account: MagnetPayto {
+ number: r.try_get(4)?,
+ name: r.try_get(4)?,
+ }
+ .as_payto(),
+ timestamp: r.try_get_timestamp(6)?,
})
},
)
@@ -346,7 +356,8 @@ pub async fn outgoing_history(
tx_out_id,
(amount).val as amount_val,
(amount).frac as amount_frac,
- credit_payto,
+ credit_account,
+ credit_name,
created,
exchange_base_url,
wtid
@@ -360,10 +371,14 @@ pub async fn outgoing_history(
Ok(OutgoingBankTransaction {
row_id: r.try_get_safeu64(0)?,
amount: r.try_get_amount_i(1, CURRENCY)?,
- credit_account: r.try_get_payto(3)?,
- date: r.try_get_timestamp(4)?,
- exchange_base_url: r.try_get_url(5)?,
- wtid: r.try_get_base32(6)?,
+ credit_account: MagnetPayto {
+ number: r.try_get(3)?,
+ name: r.try_get(4)?,
+ }
+ .as_payto(),
+ date: r.try_get_timestamp(5)?,
+ exchange_base_url: r.try_get_url(6)?,
+ wtid: r.try_get_base32(7)?,
})
},
)
@@ -384,12 +399,13 @@ pub async fn incoming_history(
QueryBuilder::new(
"
SELECT
+ type,
tx_in_id,
(amount).val as amount_val,
(amount).frac as amount_frac,
- debit_payto,
+ debit_account,
+ debit_name,
created,
- type,
metadata
FROM taler_in
JOIN tx_in USING (tx_in_id)
@@ -398,20 +414,28 @@ pub async fn incoming_history(
)
},
|r: PgRow| {
- Ok(match r.try_get(5)? {
+ Ok(match r.try_get(0)? {
IncomingType::reserve => IncomingBankTransaction::Reserve {
- row_id: r.try_get_safeu64(0)?,
- amount: r.try_get_amount_i(1, CURRENCY)?,
- debit_account: r.try_get_payto(3)?,
- date: r.try_get_timestamp(4)?,
- reserve_pub: r.try_get_base32(6)?,
+ row_id: r.try_get_safeu64(1)?,
+ amount: r.try_get_amount_i(2, CURRENCY)?,
+ debit_account: MagnetPayto {
+ number: r.try_get(4)?,
+ name: r.try_get(5)?,
+ }
+ .as_payto(),
+ date: r.try_get_timestamp(6)?,
+ reserve_pub: r.try_get_base32(7)?,
},
IncomingType::kyc => IncomingBankTransaction::Kyc {
- row_id: r.try_get_safeu64(0)?,
- amount: r.try_get_amount_i(1, CURRENCY)?,
- debit_account: r.try_get_payto(3)?,
- date: r.try_get_timestamp(4)?,
- account_pub: r.try_get_base32(6)?,
+ row_id: r.try_get_safeu64(1)?,
+ amount: r.try_get_amount_i(2, CURRENCY)?,
+ debit_account: MagnetPayto {
+ number: r.try_get(4)?,
+ name: r.try_get(5)?,
+ }
+ .as_payto(),
+ date: r.try_get_timestamp(6)?,
+ account_pub: r.try_get_base32(7)?,
},
IncomingType::wad => {
unimplemented!("WAD is not yet supported")
@@ -435,7 +459,8 @@ pub async fn transfer_by_id<'a>(
(amount).frac as amount_frac,
exchange_base_url,
wtid,
- credit_payto,
+ credit_account,
+ credit_name,
created
FROM transfer
JOIN initiated USING (initiated_id)
@@ -450,8 +475,12 @@ pub async fn transfer_by_id<'a>(
amount: r.try_get_amount_i(2, CURRENCY)?,
origin_exchange_url: r.try_get(4)?,
wtid: r.try_get_base32(5)?,
- credit_account: r.try_get_payto(6)?,
- timestamp: r.try_get_timestamp(7)?,
+ credit_account: MagnetPayto {
+ number: r.try_get(6)?,
+ name: r.try_get(7)?,
+ }
+ .as_payto(),
+ timestamp: r.try_get_timestamp(8)?,
})
})
.fetch_optional(db)
@@ -464,7 +493,7 @@ pub async fn pending_batch<'a>(
) -> sqlx::Result<Vec<Initiated>> {
sqlx::query(
"
- SELECT initiated_id, (amount).val, (amount).frac, subject, credit_payto
+ SELECT initiated_id, (amount).val, (amount).frac, subject, credit_account, credit_name
FROM initiated
WHERE magnet_code IS NULL AND (last_submitted IS NULL OR last_submitted < $1)
LIMIT 100
@@ -476,7 +505,10 @@ pub async fn pending_batch<'a>(
id: r.try_get_u64(0)?,
amount: r.try_get_amount_i(1, CURRENCY)?,
subject: r.try_get(3)?,
- creditor: r.try_get_payto(4)?,
+ creditor: MagnetPayto {
+ number: r.try_get(4)?,
+ name: r.try_get(5)?,
+ },
})
})
.fetch_all(db)
@@ -547,6 +579,7 @@ mod test {
self, make_transfer, register_tx_in, register_tx_in_admin, register_tx_out,
AddIncomingResult, RegisteredTx, TransferResult, TxIn, TxOut,
},
+ MagnetPayto,
};
use super::TxInAdmin;
@@ -581,7 +614,10 @@ mod test {
code: code,
amount: amount("EUR:10"),
subject: "subject".to_owned(),
- debtor: payto("payto://magnet-bank/todo"),
+ debtor: MagnetPayto {
+ number: "number".to_owned(),
+ name: "name".to_owned(),
+ },
timestamp: Timestamp::now_stable(),
};
// Insert
@@ -685,7 +721,10 @@ mod test {
let tx = TxInAdmin {
amount: amount("EUR:10"),
subject: "subject".to_owned(),
- debit_payto: payto("payto://magnet-bank/todo"),
+ debtor: MagnetPayto {
+ number: "number".to_owned(),
+ name: "name".to_owned(),
+ },
timestamp: Timestamp::now_stable(),
metadata: IncomingSubject::Reserve(EddsaPublicKey::rand()),
};
@@ -765,7 +804,10 @@ mod test {
code,
amount: amount("EUR:10"),
subject: "subject".to_owned(),
- creditor: payto("payto://magnet-bank/todo"),
+ creditor: MagnetPayto {
+ number: "number".to_owned(),
+ name: "name".to_owned(),
+ },
timestamp: Timestamp::now_stable(),
};
// Insert
@@ -872,10 +914,14 @@ mod test {
wtid: ShortHashCode::rand(),
credit_account: payto("payto://magnet-bank/todo"),
};
+ let payto = MagnetPayto {
+ number: "number".to_owned(),
+ name: "name".to_owned(),
+ };
let timestamp = Timestamp::now_stable();
// Insert
assert_eq!(
- make_transfer(&mut db, &req, ×tamp)
+ make_transfer(&mut db, &req, &payto, ×tamp)
.await
.expect("transfer"),
TransferResult::Success {
@@ -885,7 +931,7 @@ mod test {
);
// Idempotent
assert_eq!(
- make_transfer(&mut db, &req, &Timestamp::now())
+ make_transfer(&mut db, &req, &payto, &Timestamp::now())
.await
.expect("transfer"),
TransferResult::Success {
@@ -901,6 +947,7 @@ mod test {
wtid: ShortHashCode::rand(),
..req.clone()
},
+ &payto,
&Timestamp::now()
)
.await
@@ -915,6 +962,7 @@ mod test {
request_uid: HashCode::rand(),
..req.clone()
},
+ &payto,
&Timestamp::now()
)
.await
@@ -930,6 +978,7 @@ mod test {
wtid: ShortHashCode::rand(),
..req
},
+ &payto,
×tamp
)
.await
@@ -970,6 +1019,10 @@ mod test {
async fn batch() {
let (mut db, _) = setup().await;
let start = Timestamp::now();
+ let magnet_payto = MagnetPayto {
+ number: "number".to_owned(),
+ name: "name".to_owned(),
+ };
// Empty db
let pendings = db::pending_batch(&mut db, &start)
@@ -988,7 +1041,8 @@ mod test {
wtid: ShortHashCode::rand(),
credit_account: payto("payto://magnet-bank/todo"),
},
- &Timestamp::now(),
+ &magnet_payto,
+ &&Timestamp::now(),
)
.await
.expect("transfer");
@@ -1009,6 +1063,7 @@ mod test {
wtid: ShortHashCode::rand(),
credit_account: payto("payto://magnet-bank/todo"),
},
+ &magnet_payto,
&Timestamp::now(),
)
.await
diff --git a/wire-gateway/magnet-bank/src/dev.rs b/wire-gateway/magnet-bank/src/dev.rs
@@ -20,9 +20,8 @@ use taler_common::{
config::Config,
types::{
amount::Amount,
- payto::{payto, FullPayto, Payto},
+ payto::{FullPayto, Payto},
timestamp::Timestamp,
- url,
},
};
use tracing::info;
@@ -75,10 +74,10 @@ pub async fn dev(cfg: Config, cmd: DevCmd) -> anyhow::Result<()> {
let res = client.list_accounts().await?;
for partner in res.partners {
for account in partner.bank_accounts {
- let mut payto = url(&format!("payto://magnet-bank/{}", account.number));
- payto
- .query_pairs_mut()
- .append_pair("receiver-name", &partner.partner.name);
+ let payto = MagnetPayto {
+ number: account.number,
+ name: partner.partner.name.clone(),
+ };
info!("{} {} {payto}", account.code, account.currency.symbol);
}
}
@@ -105,7 +104,10 @@ pub async fn dev(cfg: Config, cmd: DevCmd) -> anyhow::Result<()> {
code: tx.code,
amount: amount.parse().unwrap(),
subject: tx.subject,
- debtor: payto("payto://magnet-bank/todo"),
+ debtor: MagnetPayto {
+ number: tx.bank_account,
+ name: tx.bank_account_owner,
+ },
timestamp: Timestamp::from(tx.value_date),
};
info!("in {tx}");
@@ -115,7 +117,10 @@ pub async fn dev(cfg: Config, cmd: DevCmd) -> anyhow::Result<()> {
code: tx.code,
amount: amount.parse().unwrap(),
subject: tx.subject,
- creditor: payto("payto://magnet-bank/todo"),
+ creditor: MagnetPayto {
+ number: tx.bank_account,
+ name: tx.bank_account_owner,
+ },
timestamp: Timestamp::from(tx.value_date),
};
info!("out {tx}");
diff --git a/wire-gateway/magnet-bank/src/lib.rs b/wire-gateway/magnet-bank/src/lib.rs
@@ -14,7 +14,7 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-use taler_common::types::payto::{Payto, PaytoErr};
+use taler_common::types::payto::{FullPayto, Payto, PaytoErr};
pub mod config;
pub mod constant;
@@ -27,8 +27,23 @@ pub mod wire_gateway;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MagnetPayto {
pub number: String,
+ pub name: String,
}
+impl MagnetPayto {
+ pub fn as_payto(&self) -> Payto {
+ Payto::from_parts(
+ format_args!("{MAGNET_BANK}/{}", self.number),
+ [("receiver-name", &self.name)],
+ )
+ }
+}
+
+impl std::fmt::Display for MagnetPayto {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.as_payto().fmt(f)
+ }
+}
#[derive(Debug, thiserror::Error)]
pub enum MagnetPaytoErr {
#[error("missing Magnet Bank account number in path")]
@@ -57,8 +72,10 @@ impl TryFrom<&Payto> for MagnetPayto {
if segments.next().is_some() {
return Err(PaytoErr::TooLong(MAGNET_BANK));
}
+ let full: FullPayto = value.query()?;
Ok(Self {
number: account.to_owned(),
+ name: full.receiver_name,
})
}
}
diff --git a/wire-gateway/magnet-bank/src/magnet.rs b/wire-gateway/magnet-bank/src/magnet.rs
@@ -236,7 +236,7 @@ pub struct Transaction {
#[serde(rename = "bankszamla")]
pub bank_account: String,
#[serde(rename = "bankszamlaTulajdonos")]
- pub bank_acount_owner: String,
+ pub bank_account_owner: String,
#[serde(rename = "deviza")]
pub currency: amount::Currency,
#[serde(rename = "osszeg")]
diff --git a/wire-gateway/magnet-bank/src/wire_gateway.rs b/wire-gateway/magnet-bank/src/wire_gateway.rs
@@ -35,6 +35,7 @@ use tokio::sync::watch::Sender;
use crate::{
constant::CURRENCY,
db::{self, AddIncomingResult, TxInAdmin},
+ MagnetPayto,
};
pub struct MagnetWireGateway {
@@ -85,7 +86,8 @@ impl WireGatewayImpl for MagnetWireGateway {
}
async fn transfer(&self, req: TransferRequest) -> ApiResult<TransferResponse> {
- let result = db::make_transfer(&self.pool, &req, &Timestamp::now()).await?;
+ let creditor = MagnetPayto::try_from(&req.credit_account)?;
+ let result = db::make_transfer(&self.pool, &req, &creditor, &Timestamp::now()).await?;
match result {
db::TransferResult::Success { id, timestamp } => Ok(TransferResponse {
timestamp,
@@ -138,12 +140,13 @@ impl WireGatewayImpl for MagnetWireGateway {
&self,
req: AddIncomingRequest,
) -> ApiResult<AddIncomingResponse> {
+ let debtor = MagnetPayto::try_from(&req.debit_account)?;
let res = db::register_tx_in_admin(
&self.pool,
&TxInAdmin {
amount: req.amount,
subject: format!("Admin incoming {}", req.reserve_pub),
- debit_payto: req.debit_account,
+ debtor,
timestamp: Timestamp::now(),
metadata: IncomingSubject::Reserve(req.reserve_pub),
},
@@ -162,12 +165,13 @@ impl WireGatewayImpl for MagnetWireGateway {
}
async fn add_incoming_kyc(&self, req: AddKycauthRequest) -> ApiResult<AddKycauthResponse> {
+ let debtor = MagnetPayto::try_from(&req.debit_account)?;
let res = db::register_tx_in_admin(
&self.pool,
&TxInAdmin {
amount: req.amount,
subject: format!("Admin incoming KYC:{}", req.account_pub),
- debit_payto: req.debit_account,
+ debtor,
timestamp: Timestamp::now(),
metadata: IncomingSubject::Kyc(req.account_pub),
},
diff --git a/wire-gateway/magnet-bank/tests/api.rs b/wire-gateway/magnet-bank/tests/api.rs
@@ -16,7 +16,7 @@
use std::sync::Arc;
-use magnet_bank::{db, wire_gateway::MagnetWireGateway};
+use magnet_bank::{db, wire_gateway::MagnetWireGateway, MagnetPayto};
use sqlx::PgPool;
use taler_api::{auth::AuthMethod, standard_layer, subject::OutgoingSubject};
use taler_common::{
@@ -50,7 +50,7 @@ async fn transfer() {
transfer_routine(
&server,
TransferState::pending,
- &payto("payto://magnet-bank/todo"),
+ &payto("payto://magnet-bank/account?receiver-name=John+Smith"),
)
.await;
}
@@ -78,7 +78,10 @@ async fn outgoing_history() {
code: i as u64,
amount: amount("EUR:10"),
subject: "subject".to_owned(),
- creditor: payto("payto://magnet-bank/todo"),
+ creditor: MagnetPayto {
+ number: "number".to_owned(),
+ name: "name".to_owned(),
+ },
timestamp: Timestamp::now_stable(),
},
&Some(OutgoingSubject(
@@ -97,5 +100,9 @@ async fn outgoing_history() {
#[tokio::test]
async fn admin_add_incoming() {
let (server, _) = setup().await;
- admin_add_incoming_routine(&server, &payto("payto://magnet-bank/todo")).await;
+ admin_add_incoming_routine(
+ &server,
+ &payto("payto://magnet-bank/account?receiver-name=John+Smith"),
+ )
+ .await;
}