gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-rust] branch master updated (7797b39 -> 301a9af)


From: gnunet
Subject: [taler-rust] branch master updated (7797b39 -> 301a9af)
Date: Tue, 21 Jan 2025 14:25:23 +0100

This is an automated email from the git hooks/post-receive script.

antoine pushed a change to branch master
in repository taler-rust.

    from 7797b39  magnet-bank: setup reset and dev cmd
     new 5eca882  magnet-bank: dev tx cmd
     new f043310  common: support payto implementation
     new 301a9af  magnet-bank: dev transfer cmd and magnet-bank payto

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 Cargo.lock                                   |  39 +--
 common/taler-api/src/db.rs                   |  13 +-
 common/taler-api/src/lib.rs                  |  31 ++-
 common/taler-api/tests/api.rs                |  16 +-
 common/taler-api/tests/common/db.rs          |  19 +-
 common/taler-api/tests/common/mod.rs         |  32 ++-
 common/taler-common/Cargo.toml               |   1 +
 common/taler-common/src/api_wire.rs          |  85 +++---
 common/taler-common/src/config.rs            |   4 +-
 common/taler-common/src/types/amount.rs      |  11 +-
 common/taler-common/src/types/payto.rs       | 190 ++++++++++++--
 common/taler-common/src/types/timestamp.rs   |  13 +-
 common/test-utils/src/routine.rs             |  49 ++--
 wire-gateway/magnet-bank/src/db.rs           |  68 +++--
 wire-gateway/magnet-bank/src/dev.rs          | 137 +++++++++-
 wire-gateway/magnet-bank/src/keys.rs         |  10 +-
 wire-gateway/magnet-bank/src/lib.rs          |  34 ++-
 wire-gateway/magnet-bank/src/magnet.rs       | 369 +++++++++++++++++++++++----
 wire-gateway/magnet-bank/src/magnet/error.rs |  31 +--
 wire-gateway/magnet-bank/src/main.rs         |   2 +-
 wire-gateway/magnet-bank/src/wire_gateway.rs |  32 ++-
 wire-gateway/magnet-bank/tests/api.rs        |  17 +-
 22 files changed, 928 insertions(+), 275 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 1b7c65a..139ff54 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -359,9 +359,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.5.26"
+version = "4.5.27"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783"
+checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -369,9 +369,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.5.26"
+version = "4.5.27"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121"
+checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7"
 dependencies = [
  "anstream",
  "anstyle",
@@ -1343,9 +1343,9 @@ dependencies = [
 
 [[package]]
 name = "indexmap"
-version = "2.7.0"
+version = "2.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
+checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
 dependencies = [
  "equivalent",
  "hashbrown 0.15.2",
@@ -1354,9 +1354,9 @@ dependencies = [
 
 [[package]]
 name = "ipnet"
-version = "2.10.1"
+version = "2.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708"
+checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
 
 [[package]]
 name = "is-terminal"
@@ -1450,9 +1450,9 @@ checksum = 
"d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
 
 [[package]]
 name = "listenfd"
-version = "1.0.1"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "e0500463acd96259d219abb05dc57e5a076ef04b2db9a2112846929b5f174c96"
+checksum = "b87bc54a4629b4294d0b3ef041b64c40c611097a677d9dc07b2c67739fe39dba"
 dependencies = [
  "libc",
  "uuid",
@@ -2177,9 +2177,9 @@ dependencies = [
 
 [[package]]
 name = "semver"
-version = "1.0.24"
+version = "1.0.25"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba"
+checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03"
 
 [[package]]
 name = "serde"
@@ -2203,9 +2203,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.135"
+version = "1.0.137"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9"
+checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b"
 dependencies = [
  "itoa",
  "memchr",
@@ -2245,7 +2245,7 @@ dependencies = [
  "chrono",
  "hex",
  "indexmap 1.9.3",
- "indexmap 2.7.0",
+ "indexmap 2.7.1",
  "serde",
  "serde_derive",
  "serde_json",
@@ -2387,7 +2387,7 @@ dependencies = [
  "futures-util",
  "hashbrown 0.15.2",
  "hashlink",
- "indexmap 2.7.0",
+ "indexmap 2.7.1",
  "log",
  "memchr",
  "native-tls",
@@ -2570,11 +2570,12 @@ dependencies = [
  "criterion",
  "fastrand",
  "glob",
- "indexmap 2.7.0",
+ "indexmap 2.7.1",
  "jiff",
  "rand",
  "serde",
  "serde_json",
+ "serde_path_to_error",
  "serde_urlencoded",
  "serde_with",
  "sqlx",
@@ -2968,9 +2969,9 @@ checksum = 
"06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
 
 [[package]]
 name = "uuid"
-version = "1.12.0"
+version = "1.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4"
+checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b"
 
 [[package]]
 name = "valuable"
diff --git a/common/taler-api/src/db.rs b/common/taler-api/src/db.rs
index f456acb..5989383 100644
--- a/common/taler-api/src/db.rs
+++ b/common/taler-api/src/db.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
@@ -27,7 +27,7 @@ use taler_common::{
     types::{
         amount::{Amount, Decimal},
         base32::Base32,
-        payto::Payto,
+        payto::{Payto, PaytoImpl},
         timestamp::Timestamp,
     },
 };
@@ -173,7 +173,14 @@ pub trait TypeHelper {
     fn try_get_url<I: sqlx::ColumnIndex<Self>>(&self, index: I) -> 
sqlx::Result<Url> {
         self.try_get_map(index, Url::parse)
     }
-    fn try_get_payto<I: sqlx::ColumnIndex<Self>>(&self, index: I) -> 
sqlx::Result<Payto> {
+    fn try_get_payto<
+        E: 'static + std::error::Error + Sync + Send,
+        P: PaytoImpl<ParseErr = E>,
+        I: sqlx::ColumnIndex<Self>,
+    >(
+        &self,
+        index: I,
+    ) -> sqlx::Result<Payto<P>> {
         self.try_get_map(index, |s: &str| s.parse())
     }
     fn try_get_amount(&self, index: &str, currency: &str) -> 
sqlx::Result<Amount>;
diff --git a/common/taler-api/src/lib.rs b/common/taler-api/src/lib.rs
index c932093..c6d0e2d 100644
--- a/common/taler-api/src/lib.rs
+++ b/common/taler-api/src/lib.rs
@@ -49,7 +49,10 @@ use taler_common::{
         TransferState, TransferStatus, WireConfig,
     },
     error_code::ErrorCode,
-    types::amount::Amount,
+    types::{
+        amount::Amount,
+        payto::{AnyPayto, PaytoImpl},
+    },
 };
 use tokio::{
     net::{TcpListener, UnixListener},
@@ -180,38 +183,38 @@ where
     }
 }
 
-pub trait WireGatewayImpl: Send + Sync {
+pub trait WireGatewayImpl<P: PaytoImpl>: Send + Sync {
     fn name(&self) -> &str;
     fn currency(&self) -> &str;
     fn implementation(&self) -> Option<&str>;
     fn transfer(
         &self,
-        req: TransferRequest,
+        req: TransferRequest<P>,
     ) -> impl std::future::Future<Output = ApiResult<TransferResponse>> + Send;
     fn transfer_page(
         &self,
         page: Page,
         status: Option<TransferState>,
-    ) -> impl std::future::Future<Output = ApiResult<TransferList>> + Send;
+    ) -> impl std::future::Future<Output = ApiResult<TransferList<AnyPayto>>> 
+ Send;
     fn transfer_by_id(
         &self,
         id: u64,
-    ) -> impl std::future::Future<Output = ApiResult<Option<TransferStatus>>> 
+ Send;
+    ) -> impl std::future::Future<Output = 
ApiResult<Option<TransferStatus<AnyPayto>>>> + Send;
     fn outgoing_history(
         &self,
         params: History,
-    ) -> impl std::future::Future<Output = ApiResult<OutgoingHistory>> + Send;
+    ) -> impl std::future::Future<Output = 
ApiResult<OutgoingHistory<AnyPayto>>> + Send;
     fn incoming_history(
         &self,
         params: History,
-    ) -> impl std::future::Future<Output = ApiResult<IncomingHistory>> + Send;
+    ) -> impl std::future::Future<Output = 
ApiResult<IncomingHistory<AnyPayto>>> + Send;
     fn add_incoming_reserve(
         &self,
-        req: AddIncomingRequest,
+        req: AddIncomingRequest<P>,
     ) -> impl std::future::Future<Output = ApiResult<AddIncomingResponse>> + 
Send;
     fn add_incoming_kyc(
         &self,
-        req: AddKycauthRequest,
+        req: AddKycauthRequest<P>,
     ) -> impl std::future::Future<Output = ApiResult<AddKycauthResponse>> + 
Send;
 
     fn check_currency(&self, amount: &Amount) -> ApiResult<()> {
@@ -410,7 +413,9 @@ async fn logger_middleware(request: Request, next: Next) -> 
Response {
     response
 }
 
-pub fn wire_gateway_api<I: WireGatewayImpl + 'static>(wg: Arc<I>) -> Router {
+pub fn wire_gateway_api<P: PaytoImpl + Send + 'static, I: WireGatewayImpl<P> + 
'static>(
+    wg: Arc<I>,
+) -> Router {
     Router::new()
         .route(
             "/config",
@@ -427,7 +432,7 @@ pub fn wire_gateway_api<I: WireGatewayImpl + 'static>(wg: 
Arc<I>) -> Router {
         .route(
             "/transfer",
             post(
-                |State(state): State<Arc<I>>, Req(req): Req<TransferRequest>| 
async move {
+                |State(state): State<Arc<I>>, Req(req): 
Req<TransferRequest<P>>| async move {
                     state.check_currency(&req.amount)?;
                     ApiResult::Ok(Json(state.transfer(req).await?))
                 },
@@ -492,7 +497,7 @@ pub fn wire_gateway_api<I: WireGatewayImpl + 'static>(wg: 
Arc<I>) -> Router {
         .route(
             "/admin/add-incoming",
             post(
-                |State(state): State<Arc<I>>, Req(req): 
Req<AddIncomingRequest>| async move {
+                |State(state): State<Arc<I>>, Req(req): 
Req<AddIncomingRequest<P>>| async move {
                     state.check_currency(&req.amount)?;
                     ApiResult::Ok(Json(state.add_incoming_reserve(req).await?))
                 },
@@ -501,7 +506,7 @@ pub fn wire_gateway_api<I: WireGatewayImpl + 'static>(wg: 
Arc<I>) -> Router {
         .route(
             "/admin/add-kycauth",
             post(
-                |State(state): State<Arc<I>>, Req(req): 
Req<AddKycauthRequest>| async move {
+                |State(state): State<Arc<I>>, Req(req): 
Req<AddKycauthRequest<P>>| async move {
                     state.check_currency(&req.amount)?;
                     ApiResult::Ok(Json(state.add_incoming_kyc(req).await?))
                 },
diff --git a/common/taler-api/tests/api.rs b/common/taler-api/tests/api.rs
index 3ffac21..48239c6 100644
--- a/common/taler-api/tests/api.rs
+++ b/common/taler-api/tests/api.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
@@ -21,7 +21,11 @@ use taler_common::{
     api_common::{HashCode, ShortHashCode},
     api_wire::{OutgoingHistory, TransferResponse, TransferState},
     error_code::ErrorCode,
-    types::{amount::amount, url},
+    types::{
+        amount::amount,
+        payto::{payto, AnyPayto},
+        url,
+    },
 };
 use test_utils::{
     axum_test::TestServer,
@@ -67,7 +71,7 @@ async fn config() {
 #[tokio::test]
 async fn transfer() {
     let (server, _) = setup().await;
-    transfer_routine(&server, TransferState::success).await;
+    transfer_routine::<AnyPayto>(&server, TransferState::success, 
&payto("payto://test")).await;
 }
 
 #[tokio::test]
@@ -75,7 +79,7 @@ async fn outgoing_history() {
     let (server, _) = setup().await;
     server.get("/history/outgoing").await.assert_no_content();
 
-    routine_pagination::<OutgoingHistory, _>(
+    routine_pagination::<OutgoingHistory<AnyPayto>, _>(
         &server,
         "/history/outgoing",
         |it| {
@@ -92,7 +96,7 @@ async fn outgoing_history() {
                     "amount": amount(&format!("EUR:0.0{i}")),
                     "exchange_base_url": url("http://exchange.taler";),
                     "wtid": ShortHashCode::rand(),
-                    "credit_account": url("payto://todo"),
+                    "credit_account": url("payto://test"),
                 }))
                 .await
                 .assert_ok_json::<TransferResponse>();
@@ -104,5 +108,5 @@ async fn outgoing_history() {
 #[tokio::test]
 async fn admin_add_incoming() {
     let (server, _) = setup().await;
-    admin_add_incoming_routine(&server).await;
+    admin_add_incoming_routine::<AnyPayto>(&server, 
&payto("payto://test")).await;
 }
diff --git a/common/taler-api/tests/common/db.rs 
b/common/taler-api/tests/common/db.rs
index f3b9e8c..d913867 100644
--- a/common/taler-api/tests/common/db.rs
+++ b/common/taler-api/tests/common/db.rs
@@ -23,7 +23,7 @@ use taler_common::{
         IncomingBankTransaction, OutgoingBankTransaction, TransferListStatus, 
TransferRequest,
         TransferResponse, TransferState, TransferStatus,
     },
-    types::{amount::Amount, payto::Payto, timestamp::Timestamp},
+    types::{amount::Amount, payto::{AnyPayto, Payto}, timestamp::Timestamp},
 };
 use tokio::sync::watch::{Receiver, Sender};
 
@@ -47,7 +47,10 @@ pub enum TransferResult {
     RequestUidReuse,
 }
 
-pub async fn transfer(db: &PgPool, transfer: TransferRequest) -> 
sqlx::Result<TransferResult> {
+pub async fn transfer(
+    db: &PgPool,
+    transfer: TransferRequest<AnyPayto>,
+) -> sqlx::Result<TransferResult> {
     sqlx::query(
         "
             SELECT out_request_uid_reuse, out_tx_row_id, out_timestamp
@@ -80,7 +83,7 @@ pub async fn transfer_page(
     status: &Option<TransferState>,
     params: &Page,
     currency: &str,
-) -> sqlx::Result<Vec<TransferListStatus>> {
+) -> sqlx::Result<Vec<TransferListStatus<AnyPayto>>> {
     page(
         db,
         "transfer_id",
@@ -120,7 +123,7 @@ pub async fn transfer_by_id(
     db: &PgPool,
     id: u64,
     currency: &str,
-) -> sqlx::Result<Option<TransferStatus>> {
+) -> sqlx::Result<Option<TransferStatus<AnyPayto>>> {
     sqlx::query(
         "
             SELECT
@@ -156,7 +159,7 @@ pub async fn outgoing_page(
     params: &History,
     currency: &str,
     listen: impl FnOnce() -> Receiver<i64>,
-) -> sqlx::Result<Vec<OutgoingBankTransaction>> {
+) -> sqlx::Result<Vec<OutgoingBankTransaction<AnyPayto>>> {
     history(
         db,
         "transfer_id",
@@ -199,7 +202,7 @@ pub enum AddIncomingResult {
 pub async fn add_incoming(
     db: &PgPool,
     amount: &Amount,
-    debit_account: &Payto,
+    debit_account: &Payto<AnyPayto>,
     subject: &str,
     timestamp: &Timestamp,
     kind: IncomingType,
@@ -214,7 +217,7 @@ pub async fn add_incoming(
     .bind(key.as_slice())
     .bind(subject)
     .bind_amount(amount)
-    .bind(debit_account.raw())
+    .bind(debit_account .raw())
     .bind_timestamp(timestamp)
     .bind(kind)
     .try_map(|r: PgRow| {
@@ -233,7 +236,7 @@ pub async fn incoming_page(
     params: &History,
     currency: &str,
     listen: impl FnOnce() -> Receiver<i64>,
-) -> sqlx::Result<Vec<IncomingBankTransaction>> {
+) -> sqlx::Result<Vec<IncomingBankTransaction<AnyPayto>>> {
     history(
         db,
         "incoming_transaction_id",
diff --git a/common/taler-api/tests/common/mod.rs 
b/common/taler-api/tests/common/mod.rs
index 83f642b..3829061 100644
--- a/common/taler-api/tests/common/mod.rs
+++ b/common/taler-api/tests/common/mod.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
@@ -32,7 +32,10 @@ use taler_common::{
         TransferState, TransferStatus,
     },
     error_code::ErrorCode,
-    types::{payto::payto, timestamp::Timestamp},
+    types::{
+        payto::{payto, AnyPayto},
+        timestamp::Timestamp,
+    },
 };
 use tokio::sync::watch::Sender;
 
@@ -46,7 +49,7 @@ pub struct SampleState {
     incoming_channel: Sender<i64>,
 }
 
-impl WireGatewayImpl for SampleState {
+impl WireGatewayImpl<AnyPayto> for SampleState {
     fn name(&self) -> &str {
         "taler-wire-gateway"
     }
@@ -59,7 +62,7 @@ impl WireGatewayImpl for SampleState {
         None
     }
 
-    async fn transfer(&self, req: TransferRequest) -> 
ApiResult<TransferResponse> {
+    async fn transfer(&self, req: TransferRequest<AnyPayto>) -> 
ApiResult<TransferResponse> {
         let result = db::transfer(&self.pool, req).await?;
         match result {
             db::TransferResult::Success(transfer_response) => 
Ok(transfer_response),
@@ -74,42 +77,42 @@ impl WireGatewayImpl for SampleState {
         &self,
         page: Page,
         status: Option<TransferState>,
-    ) -> ApiResult<TransferList> {
+    ) -> ApiResult<TransferList<AnyPayto>> {
         Ok(TransferList {
             transfers: db::transfer_page(&self.pool, &status, &page, 
&self.currency).await?,
-            debit_account: payto("payto://todo"),
+            debit_account: payto("payto://test"),
         })
     }
 
-    async fn transfer_by_id(&self, id: u64) -> 
ApiResult<Option<TransferStatus>> {
+    async fn transfer_by_id(&self, id: u64) -> 
ApiResult<Option<TransferStatus<AnyPayto>>> {
         Ok(db::transfer_by_id(&self.pool, id, &self.currency).await?)
     }
 
-    async fn outgoing_history(&self, params: History) -> 
ApiResult<OutgoingHistory> {
+    async fn outgoing_history(&self, params: History) -> 
ApiResult<OutgoingHistory<AnyPayto>> {
         let txs = db::outgoing_page(&self.pool, &params, &self.currency, || {
             self.outgoing_channel.subscribe()
         })
         .await?;
         Ok(OutgoingHistory {
             outgoing_transactions: txs,
-            debit_account: payto("payto://todo"),
+            debit_account: payto("payto://test"),
         })
     }
 
-    async fn incoming_history(&self, params: History) -> 
ApiResult<IncomingHistory> {
+    async fn incoming_history(&self, params: History) -> 
ApiResult<IncomingHistory<AnyPayto>> {
         let txs = db::incoming_page(&self.pool, &params, &self.currency, || {
             self.incoming_channel.subscribe()
         })
         .await?;
         Ok(IncomingHistory {
             incoming_transactions: txs,
-            credit_account: payto("payto://todo"),
+            credit_account: payto("payto://test"),
         })
     }
 
     async fn add_incoming_reserve(
         &self,
-        req: AddIncomingRequest,
+        req: AddIncomingRequest<AnyPayto>,
     ) -> ApiResult<AddIncomingResponse> {
         let timestamp = Timestamp::now();
         let res = db::add_incoming(
@@ -131,7 +134,10 @@ impl WireGatewayImpl for SampleState {
         }
     }
 
-    async fn add_incoming_kyc(&self, req: AddKycauthRequest) -> 
ApiResult<AddKycauthResponse> {
+    async fn add_incoming_kyc(
+        &self,
+        req: AddKycauthRequest<AnyPayto>,
+    ) -> ApiResult<AddKycauthResponse> {
         let timestamp = Timestamp::now();
         let res = db::add_incoming(
             &self.pool,
diff --git a/common/taler-common/Cargo.toml b/common/taler-common/Cargo.toml
index 93888b6..d84aa4e 100644
--- a/common/taler-common/Cargo.toml
+++ b/common/taler-common/Cargo.toml
@@ -13,6 +13,7 @@ tempfile.workspace = true
 jiff.workspace = true
 serde.workspace = true
 serde_json = { workspace = true, features = ["raw_value"] }
+serde_path_to_error.workspace = true
 url.workspace = true
 thiserror.workspace = true
 fastrand.workspace = true
diff --git a/common/taler-common/src/api_wire.rs 
b/common/taler-common/src/api_wire.rs
index e10318a..776fe9d 100644
--- a/common/taler-common/src/api_wire.rs
+++ b/common/taler-common/src/api_wire.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
@@ -18,7 +18,11 @@
 
 use url::Url;
 
-use crate::types::{amount::Amount, payto::Payto, timestamp::Timestamp};
+use crate::types::{
+    amount::Amount,
+    payto::{Payto, PaytoImpl},
+    timestamp::Timestamp,
+};
 
 use super::api_common::{EddsaPublicKey, HashCode, SafeU64, ShortHashCode, 
WadId};
 use serde::{Deserialize, Serialize};
@@ -41,78 +45,85 @@ pub struct TransferResponse {
 
 /// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-TransferRequest>
 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
-pub struct TransferRequest {
+#[serde(bound = "")]
+pub struct TransferRequest<P: PaytoImpl> {
     pub request_uid: HashCode,
     pub amount: Amount,
     pub exchange_base_url: Url,
     pub wtid: ShortHashCode,
-    pub credit_account: Payto,
+    pub credit_account: Payto<P>,
 }
 
 /// <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: Payto,
+#[serde(bound = "")]
+pub struct TransferList<P: PaytoImpl> {
+    pub transfers: Vec<TransferListStatus<P>>,
+    pub debit_account: Payto<P>,
 }
 
 /// 
<https://docs.taler.net/core/api-bank-wire.html#tsref-type-TransferListStatus>
 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
-pub struct TransferListStatus {
+#[serde(bound = "")]
+pub struct TransferListStatus<P: PaytoImpl> {
     pub row_id: SafeU64,
     pub status: TransferState,
     pub amount: Amount,
-    pub credit_account: Payto,
+    pub credit_account: Payto<P>,
     pub timestamp: Timestamp,
 }
 
 /// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-TransfertSatus>
 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
-pub struct TransferStatus {
+#[serde(bound = "")]
+pub struct TransferStatus<P: PaytoImpl> {
     pub status: TransferState,
     pub status_msg: Option<String>,
     pub amount: Amount,
     pub origin_exchange_url: String,
     pub wtid: ShortHashCode,
-    pub credit_account: Payto,
+    pub credit_account: Payto<P>,
     pub timestamp: Timestamp,
 }
 
-#[derive(Debug, Clone, Serialize, Deserialize)]
 /// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-OutgoingHistory>
-pub struct OutgoingHistory {
-    pub outgoing_transactions: Vec<OutgoingBankTransaction>,
-    pub debit_account: Payto,
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(bound = "")]
+pub struct OutgoingHistory<P: PaytoImpl> {
+    pub outgoing_transactions: Vec<OutgoingBankTransaction<P>>,
+    pub debit_account: Payto<P>,
 }
 
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
 /// 
<https://docs.taler.net/core/api-bank-wire.html#tsref-type-OutgoingBankTransaction>
-pub struct OutgoingBankTransaction {
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(bound = "")]
+pub struct OutgoingBankTransaction<P: PaytoImpl> {
     pub row_id: SafeU64,
     pub date: Timestamp,
     pub amount: Amount,
-    pub credit_account: Payto,
+    pub credit_account: Payto<P>,
     pub wtid: ShortHashCode,
     pub exchange_base_url: Url,
 }
 
-#[derive(Debug, Clone, Serialize, Deserialize)]
 /// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-IncomingHistory>
-pub struct IncomingHistory {
-    pub credit_account: Payto,
-    pub incoming_transactions: Vec<IncomingBankTransaction>,
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(bound = "")]
+pub struct IncomingHistory<P: PaytoImpl> {
+    pub credit_account: Payto<P>,
+    pub incoming_transactions: Vec<IncomingBankTransaction<P>>,
 }
 
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
-#[serde(tag = "type")]
 /// 
<https://docs.taler.net/core/api-bank-wire.html#tsref-type-IncomingBankTransaction>
-pub enum IncomingBankTransaction {
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(bound = "")]
+pub enum IncomingBankTransaction<P: PaytoImpl> {
     #[serde(rename = "RESERVE")]
     Reserve {
         row_id: SafeU64,
         date: Timestamp,
         amount: Amount,
-        debit_account: Payto,
+        debit_account: Payto<P>,
         reserve_pub: EddsaPublicKey,
     },
     #[serde(rename = "WAD")]
@@ -120,7 +131,7 @@ pub enum IncomingBankTransaction {
         row_id: SafeU64,
         date: Timestamp,
         amount: Amount,
-        debit_account: Payto,
+        debit_account: Payto<P>,
         origin_exchange_url: Url,
         wad_id: WadId,
     },
@@ -129,36 +140,38 @@ pub enum IncomingBankTransaction {
         row_id: SafeU64,
         date: Timestamp,
         amount: Amount,
-        debit_account: Payto,
+        debit_account: Payto<P>,
         account_pub: EddsaPublicKey,
     },
 }
 
-#[derive(Debug, Clone, Serialize, Deserialize)]
 /// 
<https://docs.taler.net/core/api-bank-wire.html#tsref-type-AddIncomingRequest>
-pub struct AddIncomingRequest {
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(bound = "")]
+pub struct AddIncomingRequest<P: PaytoImpl> {
     pub amount: Amount,
     pub reserve_pub: EddsaPublicKey,
-    pub debit_account: Payto,
+    pub debit_account: Payto<P>,
 }
 
-#[derive(Debug, Clone, Serialize, Deserialize)]
 /// 
<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: Timestamp,
 }
 
-#[derive(Debug, Clone, Serialize, Deserialize)]
 /// 
<https://docs.taler.net/core/api-bank-wire.html#tsref-type-AddKycauthRequest>
-pub struct AddKycauthRequest {
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(bound = "")]
+pub struct AddKycauthRequest<P: PaytoImpl> {
     pub amount: Amount,
     pub account_pub: EddsaPublicKey,
-    pub debit_account: Payto,
+    pub debit_account: Payto<P>,
 }
 
-#[derive(Debug, Clone, Serialize, Deserialize)]
 /// 
<https://docs.taler.net/core/api-bank-wire.html#tsref-type-AddKycauthResponse>
+#[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct AddKycauthResponse {
     pub row_id: SafeU64,
     pub timestamp: Timestamp,
diff --git a/common/taler-common/src/config.rs 
b/common/taler-common/src/config.rs
index 59cb421..19cba32 100644
--- a/common/taler-common/src/config.rs
+++ b/common/taler-common/src/config.rs
@@ -26,7 +26,7 @@ use url::Url;
 
 use crate::types::{
     amount::{Amount, Currency},
-    payto::Payto,
+    payto::{AnyPayto, Payto},
 };
 
 pub mod parser {
@@ -721,7 +721,7 @@ impl<'cfg, 'arg> Section<'cfg, 'arg> {
     }
 
     /** Access [option] as payto */
-    pub fn payto(&self, option: &'arg str) -> Value<'arg, Payto> {
+    pub fn payto(&self, option: &'arg str) -> Value<'arg, Payto<AnyPayto>> {
         self.parse("payto", option)
     }
 
diff --git a/common/taler-common/src/types/amount.rs 
b/common/taler-common/src/types/amount.rs
index 0e63a13..0202464 100644
--- a/common/taler-common/src/types/amount.rs
+++ b/common/taler-common/src/types/amount.rs
@@ -206,7 +206,16 @@ impl FromStr for Decimal {
 
 impl Display for Decimal {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.write_fmt(format_args!("{}.{:08}", self.val, self.frac))
+        if self.frac == 0 {
+            f.write_fmt(format_args!("{}", self.val))
+        } else {
+            let num = format!("{:08}", self.frac);
+            f.write_fmt(format_args!(
+                "{}.{:08}",
+                self.val,
+                num.trim_end_matches('0')
+            ))
+        }
     }
 }
 
diff --git a/common/taler-common/src/types/payto.rs 
b/common/taler-common/src/types/payto.rs
index 92121f7..a011325 100644
--- a/common/taler-common/src/types/payto.rs
+++ b/common/taler-common/src/types/payto.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
@@ -14,56 +14,192 @@
   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */
 
-use std::{fmt::Debug, str::FromStr};
+use serde::de::DeserializeOwned;
+use std::{fmt::Debug, ops::Deref, str::FromStr};
 use url::Url;
 
-#[derive(PartialEq, Eq, Clone, serde_with::DeserializeFromStr, 
serde_with::SerializeDisplay)]
-pub struct Payto(Url);
+use super::url;
 
-impl Payto {
+/// Parse a payto URI, panic if malformed
+pub fn payto<Impl: PaytoImpl>(url: impl AsRef<str>) -> Payto<Impl> {
+    url.as_ref().parse().expect("invalid payto")
+}
+
+/// A payto implementation
+pub trait PaytoImpl: Sized {
+    type ParseErr: std::error::Error;
+
+    fn kind() -> &'static str;
+
+    fn parse(path: &mut std::str::Split<'_, char>) -> Result<Self, 
Self::ParseErr>;
+
+    fn path(&self) -> String;
+}
+
+/// A generic payto that accept any kind
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct AnyPayto {}
+
+impl PaytoImpl for AnyPayto {
+    type ParseErr = std::convert::Infallible;
+
+    fn parse(path: &mut std::str::Split<'_, char>) -> Result<Self, 
Self::ParseErr> {
+        for _ in path {}
+        Ok(Self {})
+    }
+
+    fn kind() -> &'static str {
+        ""
+    }
+
+    fn path(&self) -> String {
+        "".to_owned()
+    }
+}
+
+/// RFC 8905 payto URI
+#[derive(
+    Debug, Clone, PartialEq, Eq, serde_with::DeserializeFromStr, 
serde_with::SerializeDisplay,
+)]
+pub struct Payto<Impl: PaytoImpl> {
+    raw: Url,
+    parsed: Impl,
+}
+
+impl<Impl: PaytoImpl> Payto<Impl> {
     pub fn raw(&self) -> &str {
-        self.0.as_str()
+        self.raw.as_str()
+    }
+
+    pub fn generic(self) -> Payto<AnyPayto> {
+        Payto {
+            raw: self.raw,
+            parsed: AnyPayto {},
+        }
     }
 }
 
-impl std::fmt::Display for Payto {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        std::fmt::Display::fmt(&self.0, f)
+impl<Impl: PaytoImpl> Deref for Payto<Impl> {
+    type Target = Impl;
+
+    fn deref(&self) -> &Self::Target {
+        &self.parsed
     }
 }
 
-impl std::fmt::Debug for Payto {
+impl<Impl: PaytoImpl> std::fmt::Display for Payto<Impl> {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        std::fmt::Debug::fmt(&self.0, f)
+        std::fmt::Display::fmt(&self.raw, f)
     }
 }
 
-pub fn payto(url: impl AsRef<str>) -> Payto {
-    url.as_ref().parse().expect("invalid payto")
+impl<Impl: PaytoImpl> AsRef<Url> for Payto<Impl> {
+    fn as_ref(&self) -> &Url {
+        &self.raw
+    }
+}
+
+impl<Impl: PaytoImpl> From<Impl> for Payto<Impl> {
+    fn from(parsed: Impl) -> Self {
+        let raw = url(&format!("payto://{}{}", Impl::kind(), parsed.path()));
+        Self { raw, parsed }
+    }
 }
 
 #[derive(Debug, thiserror::Error)]
-pub enum ParsePaytoError {
-    #[error("invalid URI: {0}")]
+pub enum ParsePaytoErr<E> {
+    #[error("invalid payto URI: {0}")]
     Url(#[from] url::ParseError),
-    #[error("not a payto URI")]
-    NotPayto,
+    #[error("malformed payto URI: {0}")]
+    Malformed(#[from] serde_path_to_error::Error<serde_urlencoded::de::Error>),
+    #[error("expected a payto URI got {0}")]
+    NotPayto(String),
+    #[error("unsupported payto kind, expected {0} got {1}")]
+    UnsupportedKind(&'static str, String),
+    #[error("to much path segment for a {0} payto uri")]
+    TooLong(&'static str),
+    #[error(transparent)]
+    Custom(E),
 }
 
-impl FromStr for Payto {
-    type Err = ParsePaytoError;
+fn parse_payto<Impl: PaytoImpl, Query: DeserializeOwned>(
+    s: &str,
+) -> Result<(Payto<Impl>, Query), ParsePaytoErr<Impl::ParseErr>> {
+    // Parse url
+    let raw: Url = s.parse()?;
+    // Check scheme
+    if raw.scheme() != "payto" {
+        return Err(ParsePaytoErr::NotPayto(raw.scheme().to_owned()));
+    }
+    // Check domain
+    let domain = raw.domain().unwrap_or_default();
+    let kind = Impl::kind();
+    if !kind.is_empty() && domain != kind {
+        return Err(ParsePaytoErr::UnsupportedKind(kind, domain.to_owned()));
+    }
+    // Parse path
+    let mut segments = raw.path_segments().unwrap_or_else(|| "".split('/'));
+    let parsed = Impl::parse(&mut segments).map_err(ParsePaytoErr::Custom)?;
+    if segments.next().is_some() {
+        return Err(ParsePaytoErr::TooLong(kind));
+    }
+    // Parse query
+    let de = serde_urlencoded::Deserializer::new(url::form_urlencoded::parse(
+        raw.query().unwrap_or_default().as_bytes(),
+    ));
+    let query: Query = serde_path_to_error::deserialize(de)?;
+
+    Ok((Payto { raw, parsed }, query))
+}
+
+impl<Impl: PaytoImpl> FromStr for Payto<Impl> {
+    type Err = ParsePaytoErr<Impl::ParseErr>;
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let url: Url = s.parse()?;
-        if url.scheme() != "payto" {
-            return Err(ParsePaytoError::NotPayto);
-        }
-        Ok(Self(url))
+        #[derive(serde::Deserialize)]
+        struct Query {}
+        let (payto, _): (_, Query) = parse_payto(s)?;
+        Ok(payto)
     }
 }
 
-impl AsRef<Url> for Payto {
-    fn as_ref(&self) -> &Url {
-        &self.0
+/// RFC 8905 payto URI
+#[derive(
+    Debug, Clone, PartialEq, Eq, serde_with::DeserializeFromStr, 
serde_with::SerializeDisplay,
+)]
+pub struct FullPayto<Impl: PaytoImpl> {
+    payto: Payto<Impl>,
+    pub receiver_name: String,
+}
+
+impl<Impl: PaytoImpl> Deref for FullPayto<Impl> {
+    type Target = Payto<Impl>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.payto
+    }
+}
+
+impl<Impl: PaytoImpl> std::fmt::Display for FullPayto<Impl> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        std::fmt::Display::fmt(&self.payto, f)
+    }
+}
+
+impl<Impl: PaytoImpl> FromStr for FullPayto<Impl> {
+    type Err = ParsePaytoErr<Impl::ParseErr>;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        #[derive(serde::Deserialize)]
+        struct Query {
+            #[serde(rename = "receiver-name")]
+            receiver_name: String,
+        }
+        let (payto, query): (_, Query) = parse_payto(s)?;
+
+        Ok(Self {
+            payto,
+            receiver_name: query.receiver_name,
+        })
     }
 }
diff --git a/common/taler-common/src/types/timestamp.rs 
b/common/taler-common/src/types/timestamp.rs
index 1ceb383..f773b22 100644
--- a/common/taler-common/src/types/timestamp.rs
+++ b/common/taler-common/src/types/timestamp.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
@@ -14,6 +14,8 @@
   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */
 
+use std::fmt::Display;
+
 use serde::{de::Error, ser::SerializeStruct, Deserialize, Deserializer, 
Serialize, Serializer};
 use serde_json::Value;
 
@@ -103,6 +105,15 @@ impl Serialize for Timestamp {
     }
 }
 
+impl Display for Timestamp {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Timestamp::Never => f.write_str("never"),
+            Timestamp::Time(timestamp) => timestamp.fmt(f),
+        }
+    }
+}
+
 impl From<jiff::Timestamp> for Timestamp {
     fn from(time: jiff::Timestamp) -> Self {
         Self::Time(time)
diff --git a/common/test-utils/src/routine.rs b/common/test-utils/src/routine.rs
index 41465d3..5edfe46 100644
--- a/common/test-utils/src/routine.rs
+++ b/common/test-utils/src/routine.rs
@@ -34,7 +34,12 @@ use taler_common::{
         TransferStatus,
     },
     error_code::ErrorCode,
-    types::{amount::amount, base32::Base32, url},
+    types::{
+        amount::amount,
+        base32::Base32,
+        payto::{Payto, PaytoImpl},
+        url,
+    },
 };
 use tokio::time::sleep;
 
@@ -263,7 +268,11 @@ async fn get_currency(server: &TestServer) -> String {
 }
 
 /// Test standard behavior of the transfer endpoints
-pub async fn transfer_routine(server: &TestServer, default_status: 
TransferState) {
+pub async fn transfer_routine<P: PaytoImpl + Eq + Debug>(
+    server: &TestServer,
+    default_status: TransferState,
+    credit_account: &Payto<P>,
+) {
     let currency = &get_currency(server).await;
     let default_amount = amount(format!("{currency}:42"));
     let transfer_request = json!({
@@ -271,7 +280,7 @@ pub async fn transfer_routine(server: &TestServer, 
default_status: TransferState
         "amount": default_amount,
         "exchange_base_url": "http://exchange.taler";,
         "wtid": ShortHashCode::rand(),
-        "credit_account": "payto://todo",
+        "credit_account": credit_account,
     });
 
     // Check empty db
@@ -333,7 +342,7 @@ pub async fn transfer_routine(server: &TestServer, 
default_status: TransferState
         let tx = server
             .get(&format!("/transfers/{}", resp.row_id))
             .await
-            .assert_ok_json::<TransferStatus>();
+            .assert_ok_json::<TransferStatus<P>>();
         assert_eq!(default_status, tx.status);
         assert_eq!(default_amount, tx.amount);
         assert_eq!("http://exchange.taler/";, tx.origin_exchange_url);
@@ -363,19 +372,19 @@ pub async fn transfer_routine(server: &TestServer, 
default_status: TransferState
             let list = server
                 .get("/transfers")
                 .await
-                .assert_ok_json::<TransferList>();
+                .assert_ok_json::<TransferList<P>>();
             assert_eq!(list.transfers.len(), 6);
             assert_eq!(
                 list,
                 server
                     .get(&format!("/transfers?status={}", 
default_status.as_ref()))
                     .await
-                    .assert_ok_json::<TransferList>()
+                    .assert_ok_json::<TransferList<P>>()
             );
         }
 
         // Pagination test
-        routine_pagination::<TransferList, _>(
+        routine_pagination::<TransferList<P>, _>(
             server,
             "/transfers",
             |it| {
@@ -392,7 +401,7 @@ pub async fn transfer_routine(server: &TestServer, 
default_status: TransferState
                         "amount": amount(format!("{currency}:0.0{i}")),
                         "exchange_base_url": url("http://exchange.taler";),
                         "wtid": ShortHashCode::rand(),
-                        "credit_account": url("payto://todo"),
+                        "credit_account": credit_account,
                     }))
                     .await
                     .assert_ok_json::<TransferResponse>();
@@ -402,7 +411,12 @@ pub async fn transfer_routine(server: &TestServer, 
default_status: TransferState
     }
 }
 
-async fn add_incoming_routine(server: &TestServer, currency: &str, kind: 
IncomingType) {
+async fn add_incoming_routine<P: PaytoImpl>(
+    server: &TestServer,
+    currency: &str,
+    kind: IncomingType,
+    debit_acount: &Payto<P>,
+) {
     let (path, key) = match kind {
         IncomingType::reserve => ("/admin/add-incoming", "reserve_pub"),
         IncomingType::kyc => ("/admin/add-kycauth", "account_pub"),
@@ -411,7 +425,7 @@ async fn add_incoming_routine(server: &TestServer, 
currency: &str, kind: Incomin
     let valid_req = json!({
         "amount": format!("{currency}:44"),
         key: EddsaPublicKey::rand(),
-        "debit_account": "payto://todo",
+        "debit_account": debit_acount,
     });
 
     // Check OK
@@ -470,7 +484,10 @@ async fn add_incoming_routine(server: &TestServer, 
currency: &str, kind: Incomin
 }
 
 /// Test standard behavior of the admin add incoming endpoints
-pub async fn admin_add_incoming_routine(server: &TestServer) {
+pub async fn admin_add_incoming_routine<P: PaytoImpl>(
+    server: &TestServer,
+    debit_acount: &Payto<P>,
+) {
     let currency = &get_currency(server).await;
 
     // History
@@ -478,7 +495,7 @@ pub async fn admin_add_incoming_routine(server: 
&TestServer) {
     routine_history(
         server,
         "/history/incoming",
-        |it: IncomingHistory| {
+        |it: IncomingHistory<P>| {
             it.incoming_transactions
                 .into_iter()
                 .map(|it| match it {
@@ -496,7 +513,7 @@ pub async fn admin_add_incoming_routine(server: 
&TestServer) {
                     .json(&json!({
                         "amount": format!("{currency}:0.0{i}"),
                         "reserve_pub": EddsaPublicKey::rand(),
-                        "debit_account": "payto://todo",
+                        "debit_account": debit_acount,
                     }))
                     .await
                     .assert_ok_json::<TransferResponse>();
@@ -506,7 +523,7 @@ pub async fn admin_add_incoming_routine(server: 
&TestServer) {
                     .json(&json!({
                         "amount": format!("{currency}:0.0{i}"),
                         "account_pub": EddsaPublicKey::rand(),
-                        "debit_account": "payto://todo",
+                        "debit_account": debit_acount,
                     }))
                     .await
                     .assert_ok_json::<TransferResponse>();
@@ -517,7 +534,7 @@ pub async fn admin_add_incoming_routine(server: 
&TestServer) {
     )
     .await;
     // Add incoming reserve
-    add_incoming_routine(server, currency, IncomingType::reserve).await;
+    add_incoming_routine(server, currency, IncomingType::reserve, 
debit_acount).await;
     // Add incoming kyc
-    add_incoming_routine(server, currency, IncomingType::kyc).await;
+    add_incoming_routine(server, currency, IncomingType::kyc, 
debit_acount).await;
 }
diff --git a/wire-gateway/magnet-bank/src/db.rs 
b/wire-gateway/magnet-bank/src/db.rs
index 602cdc6..ce22d5a 100644
--- a/wire-gateway/magnet-bank/src/db.rs
+++ b/wire-gateway/magnet-bank/src/db.rs
@@ -27,11 +27,15 @@ use taler_common::{
         IncomingBankTransaction, OutgoingBankTransaction, TransferListStatus, 
TransferRequest,
         TransferState, TransferStatus,
     },
-    types::{amount::Amount, payto::Payto, timestamp::Timestamp},
+    types::{
+        amount::Amount,
+        payto::{AnyPayto, Payto},
+        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,24 +65,50 @@ pub struct TxIn {
     pub code: u64,
     pub amount: Amount,
     pub subject: String,
-    pub debit_payto: Payto,
+    pub debtor: Payto<MagnetPayto>,
     pub timestamp: Timestamp,
 }
 
+impl Display for TxIn {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let TxIn {
+            code,
+            amount,
+            subject,
+            debtor,
+            timestamp,
+        } = self;
+        write!(f, "{timestamp} {amount} {code} {debtor} '{subject}'")
+    }
+}
+
 #[derive(Debug, Clone)]
 pub struct TxOut {
     pub code: u64,
     pub amount: Amount,
     pub subject: String,
-    pub credit_payto: Payto,
+    pub creditor: Payto<MagnetPayto>,
     pub timestamp: Timestamp,
 }
 
+impl Display for TxOut {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let TxOut {
+            code,
+            amount,
+            subject,
+            creditor,
+            timestamp,
+        } = self;
+        write!(f, "{timestamp} {amount} {code} {creditor} '{subject}'")
+    }
+}
+
 #[derive(Debug, Clone)]
 pub struct TxInAdmin {
     pub amount: Amount,
     pub subject: String,
-    pub debit_payto: Payto,
+    pub debit_payto: Payto<MagnetPayto>,
     pub timestamp: Timestamp,
     pub metadata: IncomingSubject,
 }
@@ -101,7 +131,7 @@ pub struct Initiated {
     pub id: u64,
     pub amount: Amount,
     pub subject: String,
-    pub creditor: Payto,
+    pub creditor: Payto<MagnetPayto>,
 }
 
 impl Display for Initiated {
@@ -171,7 +201,7 @@ pub async fn register_tx_in(
     .bind(tx.code as i64)
     .bind_amount(&tx.amount)
     .bind(&tx.subject)
-    .bind(tx.debit_payto.raw())
+    .bind(tx.debtor.raw())
     .bind_timestamp(&tx.timestamp)
     .bind(subject.as_ref().map(|it| it.ty()))
     .bind(subject.as_ref().map(|it| it.key()))
@@ -204,7 +234,7 @@ pub async fn register_tx_out(
     .bind(tx.code as i64)
     .bind_amount(&tx.amount)
     .bind(&tx.subject)
-    .bind(tx.credit_payto.raw())
+    .bind(tx.creditor.raw())
     .bind_timestamp(&tx.timestamp)
     .bind(subject.as_ref().map(|it| it.0.as_ref()))
     .bind(subject.as_ref().map(|it| it.1.as_str()))
@@ -228,7 +258,7 @@ pub enum TransferResult {
 
 pub async fn make_transfer<'a>(
     db: impl PgExecutor<'a>,
-    req: &TransferRequest,
+    req: &TransferRequest<MagnetPayto>,
     timestamp: &Timestamp,
 ) -> sqlx::Result<TransferResult> {
     let subject = format!("{} {}", req.wtid, req.exchange_base_url);
@@ -265,7 +295,7 @@ pub async fn transfer_page<'a>(
     db: impl PgExecutor<'a>,
     status: &Option<TransferState>,
     params: &Page,
-) -> sqlx::Result<Vec<TransferListStatus>> {
+) -> sqlx::Result<Vec<TransferListStatus<AnyPayto>>> {
     page(
         db,
         "initiated_id",
@@ -307,7 +337,7 @@ pub async fn outgoing_history(
     db: &PgPool,
     params: &History,
     listen: impl FnOnce() -> Receiver<i64>,
-) -> sqlx::Result<Vec<OutgoingBankTransaction>> {
+) -> sqlx::Result<Vec<OutgoingBankTransaction<AnyPayto>>> {
     history(
         db,
         "tx_out_id",
@@ -348,7 +378,7 @@ pub async fn incoming_history(
     db: &PgPool,
     params: &History,
     listen: impl FnOnce() -> Receiver<i64>,
-) -> sqlx::Result<Vec<IncomingBankTransaction>> {
+) -> sqlx::Result<Vec<IncomingBankTransaction<AnyPayto>>> {
     history(
         db,
         "tx_in_id",
@@ -399,7 +429,7 @@ pub async fn incoming_history(
 pub async fn transfer_by_id<'a>(
     db: impl PgExecutor<'a>,
     id: u64,
-) -> sqlx::Result<Option<TransferStatus>> {
+) -> sqlx::Result<Option<TransferStatus<AnyPayto>>> {
     sqlx::query(
         "
             SELECT
@@ -555,7 +585,7 @@ mod test {
                 code: code,
                 amount: amount("EUR:10"),
                 subject: "subject".to_owned(),
-                debit_payto: payto("payto://"),
+                debtor: payto("payto://magnet-bank/todo"),
                 timestamp: Timestamp::now_stable(),
             };
             // Insert
@@ -659,7 +689,7 @@ mod test {
         let tx = TxInAdmin {
             amount: amount("EUR:10"),
             subject: "subject".to_owned(),
-            debit_payto: payto("payto://"),
+            debit_payto: payto("payto://magnet-bank/todo"),
             timestamp: Timestamp::now_stable(),
             metadata: IncomingSubject::Reserve(EddsaPublicKey::rand()),
         };
@@ -739,7 +769,7 @@ mod test {
                 code,
                 amount: amount("EUR:10"),
                 subject: "subject".to_owned(),
-                credit_payto: payto("payto://"),
+                creditor: payto("payto://magnet-bank/todo"),
                 timestamp: Timestamp::now_stable(),
             };
             // Insert
@@ -844,7 +874,7 @@ mod test {
             amount: amount("EUR:10"),
             exchange_base_url: url("https://exchange.test.com/";),
             wtid: ShortHashCode::rand(),
-            credit_account: payto("payto://"),
+            credit_account: payto("payto://magnet-bank/todo"),
         };
         let timestamp = Timestamp::now_stable();
         // Insert
@@ -960,7 +990,7 @@ mod test {
                     amount: amount(format!("{CURRENCY}:{}", i + 1)),
                     exchange_base_url: url("https://exchange.test.com/";),
                     wtid: ShortHashCode::rand(),
-                    credit_account: payto("payto://"),
+                    credit_account: payto("payto://magnet-bank/todo"),
                 },
                 &Timestamp::now(),
             )
@@ -981,7 +1011,7 @@ mod test {
                     amount: amount(format!("{CURRENCY}:{}", i + 1)),
                     exchange_base_url: url("https://exchange.test.com/";),
                     wtid: ShortHashCode::rand(),
-                    credit_account: payto("payto://"),
+                    credit_account: payto("payto://magnet-bank/todo"),
                 },
                 &Timestamp::now(),
             )
diff --git a/wire-gateway/magnet-bank/src/dev.rs 
b/wire-gateway/magnet-bank/src/dev.rs
index 0749791..a662b57 100644
--- a/wire-gateway/magnet-bank/src/dev.rs
+++ b/wire-gateway/magnet-bank/src/dev.rs
@@ -14,32 +14,147 @@
   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */
 
-use sqlx::PgPool;
-use taler_common::config::Config;
+use clap::ValueEnum;
+use jiff::Zoned;
+use taler_common::{
+    config::Config,
+    types::{
+        amount::Amount,
+        payto::{payto, FullPayto, Payto},
+        timestamp::Timestamp,
+        url,
+    },
+};
+use tracing::info;
 
 use crate::{
-    config::{DbConfig, MagnetConfig},
+    config::MagnetConfig,
+    db::{TxIn, TxOut},
     keys,
-    magnet::AuthClient,
+    magnet::{AuthClient, Direction},
+    MagnetPayto,
 };
 
+#[derive(Debug, Clone, PartialEq, Eq, ValueEnum)]
+pub enum DirArg {
+    #[value(alias("in"))]
+    Incoming,
+    #[value(alias("out"))]
+    Outgoing,
+    Both,
+}
+
 #[derive(clap::Subcommand, Debug)]
 pub enum DevCmd {
     /// Print account info
-    Account,
+    Accounts,
+    Tx {
+        account: Payto<MagnetPayto>,
+        #[clap(long, short, value_enum, default_value_t = DirArg::Both)]
+        direction: DirArg,
+    },
+    Transfer {
+        #[clap(long)]
+        debtor: Payto<MagnetPayto>,
+        #[clap(long)]
+        creditor: FullPayto<MagnetPayto>,
+        #[clap(long)]
+        amount: Amount,
+        #[clap(long)]
+        subject: String,
+    },
 }
 
 pub async fn dev(cfg: Config, cmd: DevCmd) -> anyhow::Result<()> {
-    let db = DbConfig::parse(&cfg)?;
-    let pool = PgPool::connect_with(db.cfg).await?;
     let cfg = MagnetConfig::parse(&cfg)?;
     let keys = keys::load(&cfg)?;
-    let client = AuthClient::new(reqwest::Client::new(), cfg.api_url, 
cfg.consumer)
-        .upgrade(keys.access_token);
+    let client = reqwest::Client::new();
+    let client = AuthClient::new(&client, &cfg.api_url, 
&cfg.consumer).upgrade(&keys.access_token);
     match cmd {
-        DevCmd::Account => {
+        DevCmd::Accounts => {
             let res = client.list_accounts().await?;
-            dbg!(res);
+            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);
+                    info!("{} {} {payto}", account.code, 
account.currency.symbol);
+                }
+            }
+        }
+        DevCmd::Tx { account, direction } => {
+            let dir = match direction {
+                DirArg::Incoming => Direction::Incoming,
+                DirArg::Outgoing => Direction::Outgoing,
+                DirArg::Both => Direction::Both,
+            };
+            // Register incoming
+            let mut next = None;
+            loop {
+                let page = client
+                    .page_tx(dir, 5, &account.account, &next, &None)
+                    .await?;
+                next = page.next;
+                for item in page.list {
+                    let tx = item.tx;
+                    if tx.amount.is_sign_positive() {
+                        let amount = format!("{}:{}", tx.currency, tx.amount);
+                        let tx = TxIn {
+                            code: tx.code,
+                            amount: amount.parse().unwrap(),
+                            subject: tx.subject,
+                            debtor: payto("payto://magnet-bank/todo"),
+                            timestamp: Timestamp::from(tx.value_date),
+                        };
+                        info!("in {tx}");
+                    } else {
+                        let amount = format!("{}:{}", tx.currency, -tx.amount);
+                        let tx = TxOut {
+                            code: tx.code,
+                            amount: amount.parse().unwrap(),
+                            subject: tx.subject,
+                            creditor: payto("payto://magnet-bank/todo"),
+                            timestamp: Timestamp::from(tx.value_date),
+                        };
+                        info!("out {tx}");
+                    }
+                }
+                if next.is_none() {
+                    break;
+                }
+            }
+        }
+        DevCmd::Transfer {
+            debtor,
+            creditor,
+            amount,
+            subject,
+        } => {
+            let debtor = client.account(&debtor.account).await?;
+            let now = Zoned::now();
+            let date = now.date();
+
+            let init = client
+                .init_tx(
+                    debtor.code,
+                    amount.val as f64,
+                    &subject,
+                    date,
+                    &creditor.receiver_name,
+                    &creditor.account,
+                )
+                .await?;
+            client
+                .sign_tx(
+                    &keys.signing_key,
+                    &debtor.number,
+                    init.code,
+                    init.amount,
+                    date,
+                    &creditor.account,
+                )
+                .await?;
         }
     }
     Ok(())
diff --git a/wire-gateway/magnet-bank/src/keys.rs 
b/wire-gateway/magnet-bank/src/keys.rs
index 49b6bea..099f7c1 100644
--- a/wire-gateway/magnet-bank/src/keys.rs
+++ b/wire-gateway/magnet-bank/src/keys.rs
@@ -78,12 +78,8 @@ pub async fn setup(cfg: MagnetConfig, reset: bool) -> 
anyhow::Result<()> {
         Err(e) if e.kind() == ErrorKind::NotFound => KeysFile::default(),
         Err(e) => Err(e)?,
     };
-
-    let client = AuthClient::new(
-        reqwest::Client::new(),
-        cfg.api_url.clone(),
-        cfg.consumer.clone(),
-    );
+    let client = reqwest::Client::new();
+    let client = AuthClient::new(&client, &cfg.api_url, &cfg.consumer);
 
     info!("Setup OAuth access token");
     if keys.access_token.is_none() {
@@ -107,7 +103,7 @@ pub async fn setup(cfg: MagnetConfig, reset: bool) -> 
anyhow::Result<()> {
         json_file::persist(&cfg.keys_path, &keys)?;
     }
 
-    let client = client.upgrade(keys.access_token.clone().unwrap());
+    let client = client.upgrade(keys.access_token.as_ref().unwrap());
 
     info!("Setup Strong Customer Authentication");
     // TODO find a proper way to check if SCA is required without trigerring 
SCA.GLOBAL_FEATURE_NOT_ENABLED
diff --git a/wire-gateway/magnet-bank/src/lib.rs 
b/wire-gateway/magnet-bank/src/lib.rs
index 4358df6..d002e88 100644
--- a/wire-gateway/magnet-bank/src/lib.rs
+++ b/wire-gateway/magnet-bank/src/lib.rs
@@ -14,10 +14,42 @@
   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */
 
+use taler_common::types::payto::PaytoImpl;
+
 pub mod config;
 pub mod constant;
 pub mod db;
+pub mod dev;
 pub mod keys;
 pub mod magnet;
 pub mod wire_gateway;
-pub mod dev;
\ No newline at end of file
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct MagnetPayto {
+    pub account: String,
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum MagnetPaytoErr {
+    #[error("missing Magnet Bank account number in path")]
+    MissingAccount,
+}
+
+impl PaytoImpl for MagnetPayto {
+    type ParseErr = MagnetPaytoErr;
+
+    fn kind() -> &'static str {
+        "magnet-bank"
+    }
+
+    fn parse(path: &mut std::str::Split<'_, char>) -> Result<Self, 
Self::ParseErr> {
+        let account = path.next().ok_or(MagnetPaytoErr::MissingAccount)?;
+        Ok(Self {
+            account: account.to_owned(),
+        })
+    }
+
+    fn path(&self) -> String {
+        format!("/{}", self.account)
+    }
+}
diff --git a/wire-gateway/magnet-bank/src/magnet.rs 
b/wire-gateway/magnet-bank/src/magnet.rs
index 4fb8a10..87aca69 100644
--- a/wire-gateway/magnet-bank/src/magnet.rs
+++ b/wire-gateway/magnet-bank/src/magnet.rs
@@ -16,7 +16,12 @@
 
 use base64::{prelude::BASE64_STANDARD, Engine};
 use error::ApiResult;
-use p256::{ecdsa::SigningKey, PublicKey};
+use jiff::Timestamp;
+use p256::{
+    ecdsa::{signature::Signer as _, DerSignature, SigningKey},
+    PublicKey,
+};
+use serde::{Deserialize, Serialize};
 use serde_json::{json, Value};
 use spki::EncodePublicKey;
 use taler_common::types::amount;
@@ -26,7 +31,7 @@ use crate::magnet::{error::MagnetBuilder, 
oauth::OAuthBuilder};
 pub mod error;
 mod oauth;
 
-#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct Token {
     #[serde(rename = "oauth_token")]
     pub key: String,
@@ -34,13 +39,13 @@ pub struct Token {
     pub secret: String,
 }
 
-#[derive(serde::Deserialize, Debug)]
+#[derive(Debug, Deserialize)]
 pub struct TokenAuth {
     pub oauth_token: String,
     pub oauth_verifier: String,
 }
 
-#[derive(serde::Deserialize, Debug)]
+#[derive(Debug, Deserialize)]
 pub struct Consumer {
     #[serde(rename = "consumerKey")]
     pub key: String,
@@ -52,7 +57,7 @@ pub struct Consumer {
     pub lifetime: u64,
 }
 
-#[derive(serde::Deserialize, Debug)]
+#[derive(Debug, Deserialize)]
 pub struct TokenInfo {
     #[serde(rename = "keszult")]
     pub created: jiff::Timestamp,
@@ -65,7 +70,7 @@ pub struct TokenInfo {
     pub authenticated: bool,
 }
 
-#[derive(serde::Deserialize, Debug)]
+#[derive(Debug, Deserialize)]
 pub struct SmsCodeSubmission {
     #[serde(rename = "csatorna")]
     pub channel: String,
@@ -73,7 +78,7 @@ pub struct SmsCodeSubmission {
     pub sent_to: Vec<String>,
 }
 
-#[derive(serde::Deserialize, Debug)]
+#[derive(Debug, Deserialize)]
 pub struct ScaResult {
     #[serde(rename = "csatorna")]
     pub channel: String,
@@ -81,7 +86,7 @@ pub struct ScaResult {
     pub sent_to: Vec<String>,
 }
 
-#[derive(serde::Deserialize, Debug)]
+#[derive(Debug, Deserialize)]
 pub struct Partner {
     #[serde(rename = "megnevezes")]
     pub name: String,
@@ -93,68 +98,199 @@ pub struct Partner {
     pub status: String, // TODO enum
 }
 
-#[derive(serde::Deserialize, Debug)]
+#[derive(Debug, Deserialize)]
 pub struct AccountType {
     #[serde(rename = "kod")]
-    code: u64,
+    pub code: u64,
     #[serde(rename = "megnevezes")]
-    name: String,
+    pub name: String,
 }
 
-#[derive(serde::Deserialize, Debug)]
+#[derive(Debug, Deserialize)]
 pub struct Currency {
     #[serde(rename = "jel")]
-    symbol: amount::Currency,
+    pub symbol: amount::Currency,
     #[serde(rename = "megnevezes")]
-    name: String,
+    pub name: String,
 }
 
-#[derive(serde::Deserialize, Debug)]
+#[derive(Debug, Deserialize)]
 pub struct Account {
     #[serde(rename = "alapertelmezett")]
-    default: bool,
+    pub default: bool,
     #[serde(rename = "bankszamlaTipus")]
-    ty: AccountType,
+    pub ty: AccountType,
     #[serde(rename = "deviza")]
-    currency: Currency,
+    pub currency: Currency,
     #[serde(rename = "ibanSzamlaszam")]
-    iban: String,
+    pub iban: String,
     #[serde(rename = "kod")]
-    code: u64,
+    pub code: u64,
     #[serde(rename = "szamlaszam")]
-    number: String,
+    pub number: String,
     #[serde(rename = "tulajdonosKod")]
-    owner_code: u64,
+    pub owner_code: u64,
     #[serde(rename = "lakossagi")]
-    resident: bool,
+    pub resident: bool,
     #[serde(rename = "megnevezes")]
-    name: Option<String>,
-    partner: Partner,
+    pub name: Option<String>,
+    pub partner: Partner,
 }
 
-#[derive(serde::Deserialize, Debug)]
+#[derive(Debug, Deserialize)]
 pub struct PartnerAccounts {
-    partner: Partner,
+    pub partner: Partner,
     #[serde(rename = "bankszamlaList")]
-    bank_accounts: Vec<Account>,
+    pub bank_accounts: Vec<Account>,
     #[serde(rename = "kertJogosultsag")]
-    requested_permission: u64,
+    pub requested_permission: u64,
 }
 
-#[derive(serde::Deserialize, Debug)]
+#[derive(Debug, Deserialize)]
 pub struct PartnerList {
     #[serde(rename = "partnerSzamlaList")]
-    parteners: Vec<PartnerAccounts>,
+    pub partners: Vec<PartnerAccounts>,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
+pub enum TxStatus {
+    #[serde(rename = "G")]
+    ToBeRecorded,
+    #[serde(rename = "1")]
+    PendingFirstSignature,
+    #[serde(rename = "2")]
+    PendingSecondSignature,
+    #[serde(rename = "F")]
+    PendingProcessing,
+    #[serde(rename = "L")]
+    Verified,
+    #[serde(rename = "R")]
+    PartiallyCompleted,
+    #[serde(rename = "T")]
+    Completed,
+    #[serde(rename = "E")]
+    Rejected,
+    #[serde(rename = "M")]
+    Canceled,
+    #[serde(rename = "P")]
+    UnderReview,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
+pub enum Direction {
+    #[serde(rename = "T")]
+    Outgoing,
+    #[serde(rename = "J")]
+    Incoming,
+    #[serde(rename = "M")]
+    Both,
 }
 
-pub struct AuthClient {
-    client: reqwest::Client,
-    api_url: reqwest::Url,
-    consumer: Token,
+#[derive(Debug, Deserialize)]
+pub struct TxInfo {
+    #[serde(rename = "alairas1idopont")]
+    pub first_signature: Option<Timestamp>,
+    #[serde(rename = "alairas2idopont")]
+    pub second_signature: Option<Timestamp>,
+    #[serde(rename = "alairo1")]
+    pub first_signatory: Option<Partner>,
+    #[serde(rename = "alairo2")]
+    pub second_signatory: Option<Partner>,
+    #[serde(rename = "kod")]
+    pub code: u64,
+    #[serde(rename = "bankszamla")]
+    pub account: Account,
+    #[serde(rename = "deviza")]
+    pub currency: Currency,
+    #[serde(rename = "eszamla")]
+    pub counter_account: String,
+    #[serde(rename = "epartner")]
+    pub counter_name: String,
+    #[serde(rename = "statusz")]
+    pub status: TxStatus,
+    #[serde(rename = "osszegSigned")]
+    pub amount: f64,
+    #[serde(rename = "reszteljesites")]
+    pub partial_execution: bool,
+    #[serde(rename = "sorbaallitas")]
+    pub queued: bool,
+    pub eam: Option<u64>,
 }
 
-impl AuthClient {
-    pub fn new(client: reqwest::Client, api_url: reqwest::Url, consumer: 
Token) -> Self {
+#[derive(Debug, Deserialize)]
+struct TxInfoWrapper {
+    #[serde(rename = "tranzakcio")]
+    info: TxInfo,
+}
+
+#[derive(Debug, Deserialize)]
+struct AccountWrapper {
+    #[serde(rename = "bankszamla")]
+    account: Account,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct Transaction {
+    #[serde(rename = "kod")]
+    pub code: u64,
+    #[serde(rename = "bankszamla")]
+    pub bank_account: String,
+    #[serde(rename = "bankszamlaTulajdonos")]
+    pub bank_acount_owner: String,
+    #[serde(rename = "deviza")]
+    pub currency: amount::Currency,
+    #[serde(rename = "osszeg")]
+    pub amount: f64,
+    #[serde(rename = "kozlemeny")]
+    pub subject: String,
+    #[serde(rename = "statusz")]
+    pub status: TxStatus,
+    #[serde(rename = "tranzakcioAltipus")]
+    pub kind: Option<String>,
+    #[serde(rename = "eredetiErteknap")]
+    pub tx_date: jiff::Timestamp,
+    #[serde(rename = "erteknap")]
+    pub value_date: jiff::Timestamp,
+    #[serde(rename = "eszamla")]
+    pub debtor: String,
+    #[serde(rename = "tranzakcioTipus")]
+    pub ty: Option<String>,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct Next {
+    #[serde(rename = "next")]
+    pub next_id: u64,
+    #[serde(rename = "nextTipus")]
+    pub next_type: String,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct TransactionPage {
+    #[serde(flatten)]
+    pub next: Option<Next>,
+    #[serde(rename = "tranzakcioList", default)]
+    pub list: Vec<TransactionWrapper>,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct TransactionWrapper {
+    #[serde(rename = "tranzakcioDto")]
+    pub tx: Transaction,
+}
+
+pub struct AuthClient<'a> {
+    client: &'a reqwest::Client,
+    api_url: &'a reqwest::Url,
+    consumer: &'a Token,
+}
+
+impl<'a> AuthClient<'a> {
+    pub fn new(
+        client: &'a reqwest::Client,
+        api_url: &'a reqwest::Url,
+        consumer: &'a Token,
+    ) -> Self {
         Self {
             client,
             api_url,
@@ -170,7 +306,7 @@ impl AuthClient {
         self.client
             .get(self.join("/NetBankOAuth/token/request"))
             .query(&[("oauth_callback", "oob")])
-            .oauth(&self.consumer, None, None)
+            .oauth(self.consumer, None, None)
             .await
             .magnet_call_encoded()
             .await
@@ -184,7 +320,7 @@ impl AuthClient {
         self.client
             .get(self.join("/NetBankOAuth/token/access"))
             .oauth(
-                &self.consumer,
+                self.consumer,
                 Some(token_request),
                 Some(&token_auth.oauth_verifier),
             )
@@ -193,7 +329,7 @@ impl AuthClient {
             .await
     }
 
-    pub fn upgrade(self, access: Token) -> ApiClient {
+    pub fn upgrade(self, access: &'a Token) -> ApiClient<'a> {
         ApiClient {
             client: self.client,
             api_url: self.api_url,
@@ -203,22 +339,22 @@ impl AuthClient {
     }
 }
 
-pub struct ApiClient {
-    client: reqwest::Client,
-    api_url: reqwest::Url,
-    consumer: Token,
-    access: Token,
+pub struct ApiClient<'a> {
+    client: &'a reqwest::Client,
+    api_url: &'a reqwest::Url,
+    consumer: &'a Token,
+    access: &'a Token,
 }
 
-impl ApiClient {
-    pub fn join(&self, path: &str) -> reqwest::Url {
+impl ApiClient<'_> {
+    fn join(&self, path: &str) -> reqwest::Url {
         self.api_url.join(path).unwrap()
     }
 
     pub async fn token_info(&self) -> ApiResult<TokenInfo> {
         self.client
             .get(self.join("/RESTApi/resources/v2/token"))
-            .oauth(&self.consumer, Some(&self.access), None)
+            .oauth(self.consumer, Some(self.access), None)
             .await
             .magnet_json()
             .await
@@ -227,7 +363,7 @@ impl ApiClient {
     pub async fn request_sms_code(&self) -> ApiResult<SmsCodeSubmission> {
         self.client
             .get(self.join("/RESTApi/resources/v2/kodszo/sms/token"))
-            .oauth(&self.consumer, Some(&self.access), None)
+            .oauth(self.consumer, Some(self.access), None)
             .await
             .magnet_json()
             .await
@@ -239,7 +375,7 @@ impl ApiClient {
             .json(&json!({
                 "kodszo": code
             }))
-            .oauth(&self.consumer, Some(&self.access), None)
+            .oauth(self.consumer, Some(self.access), None)
             .await
             .magnet_empty()
             .await
@@ -253,7 +389,7 @@ impl ApiClient {
             .json(&json!({
                 "keyData": BASE64_STANDARD.encode(der)
             }))
-            .oauth(&self.consumer, Some(&self.access), None)
+            .oauth(self.consumer, Some(self.access), None)
             .await
             .magnet_json()
             .await
@@ -262,9 +398,138 @@ impl ApiClient {
     pub async fn list_accounts(&self) -> ApiResult<PartnerList> {
         self.client
             .get(self.join("/RESTApi/resources/v2/partnerszamla/0"))
-            .oauth(&self.consumer, Some(&self.access), None)
+            .oauth(self.consumer, Some(self.access), None)
             .await
             .magnet_json()
             .await
     }
+
+    pub async fn account(&self, account: &str) -> ApiResult<Account> {
+        Ok(self
+            .client
+            
.get(self.join(&format!("/RESTApi/resources/v2/bankszamla/{account}")))
+            .oauth(self.consumer, Some(self.access), None)
+            .await
+            .magnet_json::<AccountWrapper>()
+            .await?
+            .account)
+    }
+
+    pub async fn page_tx(
+        &self,
+        direction: Direction,
+        limit: u16,
+        account: &str,
+        next: &Option<Next>,
+        status: &Option<TxStatus>,
+    ) -> ApiResult<TransactionPage> {
+        let mut req = self.client.get(self.join(&format!(
+            "/RESTApi/resources/v2/tranzakcio/paginator/{account}/{limit}"
+        )));
+        if let Some(next) = next {
+            req = req
+                .query(&[("nextId", next.next_id)])
+                .query(&[("nextTipus", &next.next_type)]);
+        }
+        if let Some(status) = status {
+            req = req.query(&[("statusz", status)]);
+        }
+        if direction != Direction::Both {
+            req = req.query(&[("terheles", direction)])
+        }
+
+        req.query(&[("tranzakciofrissites", "true")])
+            .oauth(self.consumer, Some(self.access), None)
+            .await
+            .magnet_call()
+            .await
+    }
+
+    pub async fn init_tx(
+        &self,
+        account_code: u64,
+        amount: f64,
+        subject: &str,
+        date: jiff::civil::Date,
+        creditor_name: &str,
+        creditor_account: &str,
+    ) -> ApiResult<TxInfo> {
+        #[derive(Serialize)]
+        struct Req<'a> {
+            #[serde(rename = "bankszamlaKod")]
+            account_code: u64,
+            #[serde(rename = "osszeg")]
+            amount: f64,
+            #[serde(rename = "kozlemeny")]
+            subject: &'a str,
+            #[serde(rename = "ertekNap")]
+            date: jiff::civil::Date,
+            #[serde(rename = "ellenpartner")]
+            creditor_name: &'a str,
+            #[serde(rename = "ellenszamla")]
+            creditor_account: &'a str,
+        }
+
+        Ok(self
+            .client
+            .post(self.join("/RESTApi/resources/v2/esetiatutalas"))
+            .json(&Req {
+                account_code,
+                amount,
+                subject,
+                date,
+                creditor_name,
+                creditor_account,
+            })
+            .oauth(self.consumer, Some(self.access), None)
+            .await
+            .magnet_call::<TxInfoWrapper>()
+            .await?
+            .info)
+    }
+
+    pub async fn sign_tx(
+        &self,
+        signing_key: &SigningKey,
+        account: &str,
+        tx_code: u64,
+        amount: f64,
+        date: jiff::civil::Date,
+        creditor: &str,
+    ) -> ApiResult<TxInfo> {
+        #[derive(Serialize)]
+        struct Req<'a> {
+            #[serde(rename = "tranzakcioKod")]
+            tx_code: u64,
+            #[serde(rename = "forrasszamla")]
+            debtor: &'a str,
+            #[serde(rename = "ellenszamla")]
+            creditor: &'a str,
+            #[serde(rename = "osszeg")]
+            amount: f64,
+            #[serde(rename = "ertekNap")]
+            date: jiff::civil::Date,
+            signature: &'a str,
+        }
+
+        let content: String = 
format!("{tx_code};{account};{creditor};{amount};{date};");
+        let signature: DerSignature = signing_key.sign(content.as_bytes());
+        let encoded = BASE64_STANDARD.encode(signature.as_bytes());
+        Ok(self
+            .client
+            .put(self.join("/RESTApi/resources/v2/tranzakcio/alairas"))
+            .json(&Req {
+                tx_code,
+                debtor: account,
+                creditor,
+                amount,
+                date,
+                signature: &encoded,
+            })
+            .oauth(self.consumer, Some(self.access), None)
+            .await
+            .magnet_call::<TxInfoWrapper>()
+            .await?
+            .info)
+    }
 }
diff --git a/wire-gateway/magnet-bank/src/magnet/error.rs 
b/wire-gateway/magnet-bank/src/magnet/error.rs
index 983e7ef..0354910 100644
--- a/wire-gateway/magnet-bank/src/magnet/error.rs
+++ b/wire-gateway/magnet-bank/src/magnet/error.rs
@@ -20,21 +20,13 @@ use thiserror::Error;
 use tracing::error;
 
 #[derive(Deserialize, Debug)]
-pub struct MagnetResponse<T> {
-    timestamp: jiff::civil::DateTime,
-    #[serde(flatten)]
-    body: MagnetBody<T>,
+struct Header {
+    #[serde(alias = "errorCode")]
+    pub error_code: Option<u16>,
 }
 
 #[derive(Deserialize, Debug)]
-pub struct Empty {}
-
-#[derive(Deserialize, Debug)]
-#[serde(untagged)]
-pub enum MagnetBody<T> {
-    Error(MagnetError),
-    Ok(T),
-}
+struct Empty {}
 
 #[derive(Deserialize, Error, Debug)]
 #[error("{error_code} {short_message} '{long_message}'")]
@@ -128,13 +120,14 @@ async fn error_handling(res: reqwest::Result<Response>) 
-> ApiResult<String> {
 /** Parse magnet JSON response */
 async fn magnet_json<T: DeserializeOwned>(res: reqwest::Result<Response>) -> 
ApiResult<T> {
     let body = error_handling(res).await?;
-    let deserializer = &mut serde_json::Deserializer::from_str(&body);
-
-    let body: MagnetResponse<T> =
-        
serde_path_to_error::deserialize(deserializer).map_err(ApiError::Json)?;
-    match body.body {
-        MagnetBody::Error(e) => Err(ApiError::Magnet(e)),
-        MagnetBody::Ok(t) => Ok(t),
+    fn parse<T: DeserializeOwned>(str: &str) -> ApiResult<T> {
+        let deserializer = &mut serde_json::Deserializer::from_str(str);
+        serde_path_to_error::deserialize(deserializer).map_err(ApiError::Json)
+    }
+    let header: Header = parse(&body)?;
+    match header.error_code {
+        Some(_) => Err(ApiError::Magnet(parse(&body)?)),
+        None => parse(&body),
     }
 }
 
diff --git a/wire-gateway/magnet-bank/src/main.rs 
b/wire-gateway/magnet-bank/src/main.rs
index 78adc81..c8a1f64 100644
--- a/wire-gateway/magnet-bank/src/main.rs
+++ b/wire-gateway/magnet-bank/src/main.rs
@@ -110,7 +110,7 @@ async fn app(args: Args) -> anyhow::Result<()> {
             let db = DbConfig::parse(&cfg)?;
             let pool = PgPool::connect_with(db.cfg).await?;
             let cfg = WireGatewayConfig::parse(&cfg)?;
-            let gateway = MagnetWireGateway::start(pool, 
payto("payto://todo")).await;
+            let gateway = MagnetWireGateway::start(pool, 
payto("payto://magnet-bank/todo")).await;
             taler_api::server(
                 taler_api::wire_gateway_api(Arc::new(gateway)),
                 cfg.serve,
diff --git a/wire-gateway/magnet-bank/src/wire_gateway.rs 
b/wire-gateway/magnet-bank/src/wire_gateway.rs
index 0ae723a..274a102 100644
--- a/wire-gateway/magnet-bank/src/wire_gateway.rs
+++ b/wire-gateway/magnet-bank/src/wire_gateway.rs
@@ -29,7 +29,7 @@ use taler_common::{
     },
     error_code::ErrorCode,
     types::{
-        payto::{payto, Payto},
+        payto::{AnyPayto, Payto},
         timestamp::Timestamp,
     },
 };
@@ -38,11 +38,12 @@ use tokio::sync::watch::Sender;
 use crate::{
     constant::CURRENCY,
     db::{self, AddIncomingResult, TxInAdmin},
+    MagnetPayto,
 };
 
 pub struct MagnetWireGateway {
     pub pool: sqlx::PgPool,
-    pub payto: Payto,
+    pub payto: Payto<MagnetPayto>,
     pub in_channel: Sender<i64>,
     pub taler_in_channel: Sender<i64>,
     pub out_channel: Sender<i64>,
@@ -50,7 +51,7 @@ pub struct MagnetWireGateway {
 }
 
 impl MagnetWireGateway {
-    pub async fn start(pool: sqlx::PgPool, payto: Payto) -> Self {
+    pub async fn start(pool: sqlx::PgPool, payto: Payto<MagnetPayto>) -> Self {
         let in_channel = Sender::new(0);
         let taler_in_channel = Sender::new(0);
         let out_channel = Sender::new(0);
@@ -74,7 +75,7 @@ impl MagnetWireGateway {
     }
 }
 
-impl WireGatewayImpl for MagnetWireGateway {
+impl WireGatewayImpl<MagnetPayto> for MagnetWireGateway {
     fn name(&self) -> &str {
         "magnet-bank"
     }
@@ -87,7 +88,7 @@ impl WireGatewayImpl for MagnetWireGateway {
         None
     }
 
-    async fn transfer(&self, req: TransferRequest) -> 
ApiResult<TransferResponse> {
+    async fn transfer(&self, req: TransferRequest<MagnetPayto>) -> 
ApiResult<TransferResponse> {
         let result = db::make_transfer(&self.pool, &req, 
&Timestamp::now()).await?;
         match result {
             db::TransferResult::Success { id, timestamp } => 
Ok(TransferResponse {
@@ -106,40 +107,40 @@ impl WireGatewayImpl for MagnetWireGateway {
         &self,
         page: Page,
         status: Option<TransferState>,
-    ) -> ApiResult<TransferList> {
+    ) -> ApiResult<TransferList<AnyPayto>> {
         Ok(TransferList {
             transfers: db::transfer_page(&self.pool, &status, &page).await?,
-            debit_account: payto("payto://todo"),
+            debit_account: self.payto.clone().generic(),
         })
     }
 
-    async fn transfer_by_id(&self, id: u64) -> 
ApiResult<Option<TransferStatus>> {
+    async fn transfer_by_id(&self, id: u64) -> 
ApiResult<Option<TransferStatus<AnyPayto>>> {
         Ok(db::transfer_by_id(&self.pool, id).await?)
     }
 
-    async fn outgoing_history(&self, params: History) -> 
ApiResult<OutgoingHistory> {
+    async fn outgoing_history(&self, params: History) -> 
ApiResult<OutgoingHistory<AnyPayto>> {
         Ok(OutgoingHistory {
             outgoing_transactions: db::outgoing_history(&self.pool, &params, 
|| {
                 self.taler_out_channel.subscribe()
             })
             .await?,
-            debit_account: self.payto.clone(),
+            debit_account: self.payto.clone().generic(),
         })
     }
 
-    async fn incoming_history(&self, params: History) -> 
ApiResult<IncomingHistory> {
+    async fn incoming_history(&self, params: History) -> 
ApiResult<IncomingHistory<AnyPayto>> {
         Ok(IncomingHistory {
             incoming_transactions: db::incoming_history(&self.pool, &params, 
|| {
                 self.taler_in_channel.subscribe()
             })
             .await?,
-            credit_account: self.payto.clone(),
+            credit_account: self.payto.clone().generic(),
         })
     }
 
     async fn add_incoming_reserve(
         &self,
-        req: AddIncomingRequest,
+        req: AddIncomingRequest<MagnetPayto>,
     ) -> ApiResult<AddIncomingResponse> {
         let res = db::register_tx_in_admin(
             &self.pool,
@@ -164,7 +165,10 @@ impl WireGatewayImpl for MagnetWireGateway {
         }
     }
 
-    async fn add_incoming_kyc(&self, req: AddKycauthRequest) -> 
ApiResult<AddKycauthResponse> {
+    async fn add_incoming_kyc(
+        &self,
+        req: AddKycauthRequest<MagnetPayto>,
+    ) -> ApiResult<AddKycauthResponse> {
         let res = db::register_tx_in_admin(
             &self.pool,
             &TxInAdmin {
diff --git a/wire-gateway/magnet-bank/tests/api.rs 
b/wire-gateway/magnet-bank/tests/api.rs
index 0fca263..0a5c14b 100644
--- 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::{
@@ -34,7 +34,7 @@ use test_utils::{
 async fn setup() -> (TestServer, PgPool) {
     let pool = db_test_setup().await;
     db::db_init(&pool, false).await.unwrap();
-    let gateway = MagnetWireGateway::start(pool.clone(), 
payto("payto://test")).await;
+    let gateway = MagnetWireGateway::start(pool.clone(), 
payto("payto://magnet-bank/todo")).await;
     let server = TestServer::new(standard_layer(
         taler_api::wire_gateway_api(Arc::new(gateway)),
         AuthMethod::None,
@@ -47,14 +47,19 @@ async fn setup() -> (TestServer, PgPool) {
 #[tokio::test]
 async fn transfer() {
     let (server, _) = setup().await;
-    transfer_routine(&server, TransferState::pending).await;
+    transfer_routine::<MagnetPayto>(
+        &server,
+        TransferState::pending,
+        &payto("payto://magnet-bank/todo"),
+    )
+    .await;
 }
 
 #[tokio::test]
 async fn outgoing_history() {
     let (server, pool) = setup().await;
     server.get("/history/outgoing").await.assert_no_content();
-    routine_pagination::<OutgoingHistory, _>(
+    routine_pagination::<OutgoingHistory<MagnetPayto>, _>(
         &server,
         "/history/outgoing",
         |it| {
@@ -73,7 +78,7 @@ async fn outgoing_history() {
                         code: i as u64,
                         amount: amount("EUR:10"),
                         subject: "subject".to_owned(),
-                        credit_payto: payto("payto://"),
+                        creditor: payto("payto://magnet-bank/todo"),
                         timestamp: Timestamp::now_stable(),
                     },
                     &Some(OutgoingSubject(
@@ -92,5 +97,5 @@ async fn outgoing_history() {
 #[tokio::test]
 async fn admin_add_incoming() {
     let (server, _) = setup().await;
-    admin_add_incoming_routine(&server).await;
+    admin_add_incoming_routine::<MagnetPayto>(&server, 
&payto("payto://magnet-bank/todo")).await;
 }

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]