taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

commit a21fa61c42d0c20807af46ef435f529f87889edd
parent 31a87a2793ddd720cf8163b49db12e49cd063a18
Author: Antoine A <>
Date:   Tue, 21 Jan 2025 20:15:57 +0100

common: remove overenginered generic payto logic

Diffstat:
Mcommon/taler-api/src/db.rs | 11++---------
Mcommon/taler-api/src/lib.rs | 31+++++++++++++------------------
Mcommon/taler-api/tests/api.rs | 12++++--------
Mcommon/taler-api/tests/common/db.rs | 19++++++++-----------
Mcommon/taler-api/tests/common/mod.rs | 24+++++++++---------------
Mcommon/taler-common/src/api_wire.rs | 66++++++++++++++++++++++++++----------------------------------------
Mcommon/taler-common/src/config.rs | 4++--
Mcommon/taler-common/src/types/payto.rs | 146++++++++++++++++++++-----------------------------------------------------------
Mcommon/test-utils/src/routine.rs | 30+++++++++++-------------------
Mwire-gateway/magnet-bank/src/db.rs | 26+++++++++++---------------
Mwire-gateway/magnet-bank/src/dev.rs | 20++++++++++++--------
Mwire-gateway/magnet-bank/src/lib.rs | 26++++++++++++--------------
Mwire-gateway/magnet-bank/src/wire_gateway.rs | 35++++++++++++++---------------------
Mwire-gateway/magnet-bank/tests/api.rs | 8++++----
14 files changed, 164 insertions(+), 294 deletions(-)

diff --git a/common/taler-api/src/db.rs b/common/taler-api/src/db.rs @@ -27,7 +27,7 @@ use taler_common::{ types::{ amount::{Amount, Decimal}, base32::Base32, - payto::{Payto, PaytoImpl}, + payto::Payto, timestamp::Timestamp, }, }; @@ -173,14 +173,7 @@ 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< - E: 'static + std::error::Error + Sync + Send, - P: PaytoImpl<ParseErr = E>, - I: sqlx::ColumnIndex<Self>, - >( - &self, - index: I, - ) -> sqlx::Result<Payto<P>> { + fn try_get_payto<I: sqlx::ColumnIndex<Self>>(&self, index: I) -> sqlx::Result<Payto> { 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 @@ -49,10 +49,7 @@ use taler_common::{ TransferState, TransferStatus, WireConfig, }, error_code::ErrorCode, - types::{ - amount::Amount, - payto::{AnyPayto, PaytoImpl}, - }, + types::amount::Amount, }; use tokio::{ net::{TcpListener, UnixListener}, @@ -183,38 +180,38 @@ where } } -pub trait WireGatewayImpl<P: PaytoImpl>: Send + Sync { +pub trait WireGatewayImpl: Send + Sync { fn name(&self) -> &str; fn currency(&self) -> &str; fn implementation(&self) -> Option<&str>; fn transfer( &self, - req: TransferRequest<P>, + req: TransferRequest, ) -> impl std::future::Future<Output = ApiResult<TransferResponse>> + Send; fn transfer_page( &self, page: Page, status: Option<TransferState>, - ) -> impl std::future::Future<Output = ApiResult<TransferList<AnyPayto>>> + Send; + ) -> impl std::future::Future<Output = ApiResult<TransferList>> + Send; fn transfer_by_id( &self, id: u64, - ) -> impl std::future::Future<Output = ApiResult<Option<TransferStatus<AnyPayto>>>> + Send; + ) -> impl std::future::Future<Output = ApiResult<Option<TransferStatus>>> + Send; fn outgoing_history( &self, params: History, - ) -> impl std::future::Future<Output = ApiResult<OutgoingHistory<AnyPayto>>> + Send; + ) -> impl std::future::Future<Output = ApiResult<OutgoingHistory>> + Send; fn incoming_history( &self, params: History, - ) -> impl std::future::Future<Output = ApiResult<IncomingHistory<AnyPayto>>> + Send; + ) -> impl std::future::Future<Output = ApiResult<IncomingHistory>> + Send; fn add_incoming_reserve( &self, - req: AddIncomingRequest<P>, + req: AddIncomingRequest, ) -> impl std::future::Future<Output = ApiResult<AddIncomingResponse>> + Send; fn add_incoming_kyc( &self, - req: AddKycauthRequest<P>, + req: AddKycauthRequest, ) -> impl std::future::Future<Output = ApiResult<AddKycauthResponse>> + Send; fn check_currency(&self, amount: &Amount) -> ApiResult<()> { @@ -413,9 +410,7 @@ async fn logger_middleware(request: Request, next: Next) -> Response { response } -pub fn wire_gateway_api<P: PaytoImpl + Send + 'static, I: WireGatewayImpl<P> + 'static>( - wg: Arc<I>, -) -> Router { +pub fn wire_gateway_api<I: WireGatewayImpl + 'static>(wg: Arc<I>) -> Router { Router::new() .route( "/config", @@ -432,7 +427,7 @@ pub fn wire_gateway_api<P: PaytoImpl + Send + 'static, I: WireGatewayImpl<P> + ' .route( "/transfer", post( - |State(state): State<Arc<I>>, Req(req): Req<TransferRequest<P>>| async move { + |State(state): State<Arc<I>>, Req(req): Req<TransferRequest>| async move { state.check_currency(&req.amount)?; ApiResult::Ok(Json(state.transfer(req).await?)) }, @@ -497,7 +492,7 @@ pub fn wire_gateway_api<P: PaytoImpl + Send + 'static, I: WireGatewayImpl<P> + ' .route( "/admin/add-incoming", post( - |State(state): State<Arc<I>>, Req(req): Req<AddIncomingRequest<P>>| async move { + |State(state): State<Arc<I>>, Req(req): Req<AddIncomingRequest>| async move { state.check_currency(&req.amount)?; ApiResult::Ok(Json(state.add_incoming_reserve(req).await?)) }, @@ -506,7 +501,7 @@ pub fn wire_gateway_api<P: PaytoImpl + Send + 'static, I: WireGatewayImpl<P> + ' .route( "/admin/add-kycauth", post( - |State(state): State<Arc<I>>, Req(req): Req<AddKycauthRequest<P>>| async move { + |State(state): State<Arc<I>>, Req(req): Req<AddKycauthRequest>| 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 @@ -21,11 +21,7 @@ use taler_common::{ api_common::{HashCode, ShortHashCode}, api_wire::{OutgoingHistory, TransferResponse, TransferState}, error_code::ErrorCode, - types::{ - amount::amount, - payto::{payto, AnyPayto}, - url, - }, + types::{amount::amount, payto::payto, url}, }; use test_utils::{ axum_test::TestServer, @@ -71,7 +67,7 @@ async fn config() { #[tokio::test] async fn transfer() { let (server, _) = setup().await; - transfer_routine::<AnyPayto>(&server, TransferState::success, &payto("payto://test")).await; + transfer_routine(&server, TransferState::success, &payto("payto://test")).await; } #[tokio::test] @@ -79,7 +75,7 @@ async fn outgoing_history() { let (server, _) = setup().await; server.get("/history/outgoing").await.assert_no_content(); - routine_pagination::<OutgoingHistory<AnyPayto>, _>( + routine_pagination::<OutgoingHistory, _>( &server, "/history/outgoing", |it| { @@ -108,5 +104,5 @@ async fn outgoing_history() { #[tokio::test] async fn admin_add_incoming() { let (server, _) = setup().await; - admin_add_incoming_routine::<AnyPayto>(&server, &payto("payto://test")).await; + admin_add_incoming_routine(&server, &payto("payto://test")).await; } diff --git 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::{AnyPayto, Payto}, timestamp::Timestamp}, + types::{amount::Amount, payto::Payto, timestamp::Timestamp}, }; use tokio::sync::watch::{Receiver, Sender}; @@ -47,10 +47,7 @@ pub enum TransferResult { RequestUidReuse, } -pub async fn transfer( - db: &PgPool, - transfer: TransferRequest<AnyPayto>, -) -> sqlx::Result<TransferResult> { +pub async fn transfer(db: &PgPool, transfer: TransferRequest) -> sqlx::Result<TransferResult> { sqlx::query( " SELECT out_request_uid_reuse, out_tx_row_id, out_timestamp @@ -83,7 +80,7 @@ pub async fn transfer_page( status: &Option<TransferState>, params: &Page, currency: &str, -) -> sqlx::Result<Vec<TransferListStatus<AnyPayto>>> { +) -> sqlx::Result<Vec<TransferListStatus>> { page( db, "transfer_id", @@ -123,7 +120,7 @@ pub async fn transfer_by_id( db: &PgPool, id: u64, currency: &str, -) -> sqlx::Result<Option<TransferStatus<AnyPayto>>> { +) -> sqlx::Result<Option<TransferStatus>> { sqlx::query( " SELECT @@ -159,7 +156,7 @@ pub async fn outgoing_page( params: &History, currency: &str, listen: impl FnOnce() -> Receiver<i64>, -) -> sqlx::Result<Vec<OutgoingBankTransaction<AnyPayto>>> { +) -> sqlx::Result<Vec<OutgoingBankTransaction>> { history( db, "transfer_id", @@ -202,7 +199,7 @@ pub enum AddIncomingResult { pub async fn add_incoming( db: &PgPool, amount: &Amount, - debit_account: &Payto<AnyPayto>, + debit_account: &Payto, subject: &str, timestamp: &Timestamp, kind: IncomingType, @@ -217,7 +214,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| { @@ -236,7 +233,7 @@ pub async fn incoming_page( params: &History, currency: &str, listen: impl FnOnce() -> Receiver<i64>, -) -> sqlx::Result<Vec<IncomingBankTransaction<AnyPayto>>> { +) -> sqlx::Result<Vec<IncomingBankTransaction>> { history( db, "incoming_transaction_id", diff --git a/common/taler-api/tests/common/mod.rs b/common/taler-api/tests/common/mod.rs @@ -32,10 +32,7 @@ use taler_common::{ TransferState, TransferStatus, }, error_code::ErrorCode, - types::{ - payto::{payto, AnyPayto}, - timestamp::Timestamp, - }, + types::{payto::payto, timestamp::Timestamp}, }; use tokio::sync::watch::Sender; @@ -49,7 +46,7 @@ pub struct SampleState { incoming_channel: Sender<i64>, } -impl WireGatewayImpl<AnyPayto> for SampleState { +impl WireGatewayImpl for SampleState { fn name(&self) -> &str { "taler-wire-gateway" } @@ -62,7 +59,7 @@ impl WireGatewayImpl<AnyPayto> for SampleState { None } - async fn transfer(&self, req: TransferRequest<AnyPayto>) -> ApiResult<TransferResponse> { + async fn transfer(&self, req: TransferRequest) -> ApiResult<TransferResponse> { let result = db::transfer(&self.pool, req).await?; match result { db::TransferResult::Success(transfer_response) => Ok(transfer_response), @@ -77,18 +74,18 @@ impl WireGatewayImpl<AnyPayto> for SampleState { &self, page: Page, status: Option<TransferState>, - ) -> ApiResult<TransferList<AnyPayto>> { + ) -> ApiResult<TransferList> { Ok(TransferList { transfers: db::transfer_page(&self.pool, &status, &page, &self.currency).await?, debit_account: payto("payto://test"), }) } - async fn transfer_by_id(&self, id: u64) -> ApiResult<Option<TransferStatus<AnyPayto>>> { + async fn transfer_by_id(&self, id: u64) -> ApiResult<Option<TransferStatus>> { Ok(db::transfer_by_id(&self.pool, id, &self.currency).await?) } - async fn outgoing_history(&self, params: History) -> ApiResult<OutgoingHistory<AnyPayto>> { + async fn outgoing_history(&self, params: History) -> ApiResult<OutgoingHistory> { let txs = db::outgoing_page(&self.pool, &params, &self.currency, || { self.outgoing_channel.subscribe() }) @@ -99,7 +96,7 @@ impl WireGatewayImpl<AnyPayto> for SampleState { }) } - async fn incoming_history(&self, params: History) -> ApiResult<IncomingHistory<AnyPayto>> { + async fn incoming_history(&self, params: History) -> ApiResult<IncomingHistory> { let txs = db::incoming_page(&self.pool, &params, &self.currency, || { self.incoming_channel.subscribe() }) @@ -112,7 +109,7 @@ impl WireGatewayImpl<AnyPayto> for SampleState { async fn add_incoming_reserve( &self, - req: AddIncomingRequest<AnyPayto>, + req: AddIncomingRequest, ) -> ApiResult<AddIncomingResponse> { let timestamp = Timestamp::now(); let res = db::add_incoming( @@ -134,10 +131,7 @@ impl WireGatewayImpl<AnyPayto> for SampleState { } } - async fn add_incoming_kyc( - &self, - req: AddKycauthRequest<AnyPayto>, - ) -> ApiResult<AddKycauthResponse> { + async fn add_incoming_kyc(&self, req: AddKycauthRequest) -> ApiResult<AddKycauthResponse> { let timestamp = Timestamp::now(); let res = db::add_incoming( &self.pool, diff --git a/common/taler-common/src/api_wire.rs b/common/taler-common/src/api_wire.rs @@ -18,11 +18,7 @@ use url::Url; -use crate::types::{ - amount::Amount, - payto::{Payto, PaytoImpl}, - timestamp::Timestamp, -}; +use crate::types::{amount::Amount, payto::Payto, timestamp::Timestamp}; use super::api_common::{EddsaPublicKey, HashCode, SafeU64, ShortHashCode, WadId}; use serde::{Deserialize, Serialize}; @@ -45,85 +41,77 @@ pub struct TransferResponse { /// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-TransferRequest> #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(bound = "")] -pub struct TransferRequest<P: PaytoImpl> { +pub struct TransferRequest { pub request_uid: HashCode, pub amount: Amount, pub exchange_base_url: Url, pub wtid: ShortHashCode, - pub credit_account: Payto<P>, + pub credit_account: Payto, } /// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-TransferList> #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(bound = "")] -pub struct TransferList<P: PaytoImpl> { - pub transfers: Vec<TransferListStatus<P>>, - pub debit_account: Payto<P>, +pub struct TransferList { + pub transfers: Vec<TransferListStatus>, + pub debit_account: Payto, } /// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-TransferListStatus> #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(bound = "")] -pub struct TransferListStatus<P: PaytoImpl> { +pub struct TransferListStatus { pub row_id: SafeU64, pub status: TransferState, pub amount: Amount, - pub credit_account: Payto<P>, + pub credit_account: Payto, pub timestamp: Timestamp, } /// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-TransfertSatus> #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(bound = "")] -pub struct TransferStatus<P: PaytoImpl> { +pub struct TransferStatus { pub status: TransferState, pub status_msg: Option<String>, pub amount: Amount, pub origin_exchange_url: String, pub wtid: ShortHashCode, - pub credit_account: Payto<P>, + pub credit_account: Payto, pub timestamp: Timestamp, } /// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-OutgoingHistory> #[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(bound = "")] -pub struct OutgoingHistory<P: PaytoImpl> { - pub outgoing_transactions: Vec<OutgoingBankTransaction<P>>, - pub debit_account: Payto<P>, +pub struct OutgoingHistory { + pub outgoing_transactions: Vec<OutgoingBankTransaction>, + pub debit_account: Payto, } /// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-OutgoingBankTransaction> #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(bound = "")] -pub struct OutgoingBankTransaction<P: PaytoImpl> { +pub struct OutgoingBankTransaction { pub row_id: SafeU64, pub date: Timestamp, pub amount: Amount, - pub credit_account: Payto<P>, + pub credit_account: Payto, pub wtid: ShortHashCode, pub exchange_base_url: Url, } /// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-IncomingHistory> #[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(bound = "")] -pub struct IncomingHistory<P: PaytoImpl> { - pub credit_account: Payto<P>, - pub incoming_transactions: Vec<IncomingBankTransaction<P>>, +pub struct IncomingHistory { + pub credit_account: Payto, + pub incoming_transactions: Vec<IncomingBankTransaction>, } /// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-IncomingBankTransaction> #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(bound = "")] -pub enum IncomingBankTransaction<P: PaytoImpl> { +pub enum IncomingBankTransaction { #[serde(rename = "RESERVE")] Reserve { row_id: SafeU64, date: Timestamp, amount: Amount, - debit_account: Payto<P>, + debit_account: Payto, reserve_pub: EddsaPublicKey, }, #[serde(rename = "WAD")] @@ -131,7 +119,7 @@ pub enum IncomingBankTransaction<P: PaytoImpl> { row_id: SafeU64, date: Timestamp, amount: Amount, - debit_account: Payto<P>, + debit_account: Payto, origin_exchange_url: Url, wad_id: WadId, }, @@ -140,18 +128,17 @@ pub enum IncomingBankTransaction<P: PaytoImpl> { row_id: SafeU64, date: Timestamp, amount: Amount, - debit_account: Payto<P>, + debit_account: Payto, account_pub: EddsaPublicKey, }, } /// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-AddIncomingRequest> #[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(bound = "")] -pub struct AddIncomingRequest<P: PaytoImpl> { +pub struct AddIncomingRequest { pub amount: Amount, pub reserve_pub: EddsaPublicKey, - pub debit_account: Payto<P>, + pub debit_account: Payto, } /// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-AddIncomingResponse> @@ -163,11 +150,10 @@ pub struct AddIncomingResponse { /// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-AddKycauthRequest> #[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(bound = "")] -pub struct AddKycauthRequest<P: PaytoImpl> { +pub struct AddKycauthRequest { pub amount: Amount, pub account_pub: EddsaPublicKey, - pub debit_account: Payto<P>, + pub debit_account: Payto, } /// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-AddKycauthResponse> diff --git 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::{AnyPayto, Payto}, + payto::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<AnyPayto>> { + pub fn payto(&self, option: &'arg str) -> Value<'arg, Payto> { self.parse("payto", option) } diff --git a/common/taler-common/src/types/payto.rs b/common/taler-common/src/types/payto.rs @@ -14,158 +14,84 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -use serde::de::DeserializeOwned; -use std::{fmt::Debug, ops::Deref, str::FromStr}; +use serde::{de::DeserializeOwned, Deserialize}; +use std::{fmt::Debug, str::FromStr}; use url::Url; /// Parse a payto URI, panic if malformed -pub fn payto<Impl: PaytoImpl>(url: impl AsRef<str>) -> Payto<Impl> { +pub fn payto(url: impl AsRef<str>) -> Payto { url.as_ref().parse().expect("invalid payto") } -/// A payto implementation -pub trait PaytoImpl: Sized { - type ParseErr: From<ParsePaytoErr> + std::error::Error + Sync + Send + 'static; - - fn parse(url: &Url) -> Result<Self, Self::ParseErr>; -} - -/// A generic payto that accept any kind -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct AnyPayto {} - -impl PaytoImpl for AnyPayto { - type ParseErr = ParsePaytoErr; - - fn parse(_url: &Url) -> Result<Self, Self::ParseErr> { - Ok(Self {}) - } -} - -/// RFC 8905 payto URI +/// A generic RFC 8905 payto URI #[derive( Debug, Clone, PartialEq, Eq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay, )] -pub struct Payto<Impl: PaytoImpl> { - raw: Url, - parsed: Impl, -} +pub struct Payto(Url); -impl<Impl: PaytoImpl> Payto<Impl> { +impl Payto { pub fn raw(&self) -> &str { - self.raw.as_str() + self.0.as_str() } - pub fn generic(self) -> Payto<AnyPayto> { - Payto { - raw: self.raw, - parsed: AnyPayto {}, - } + pub fn query<Q: DeserializeOwned>(&self) -> Result<Q, PaytoErr> { + let query = self.0.query().unwrap_or_default().as_bytes(); + let de = serde_urlencoded::Deserializer::new(url::form_urlencoded::parse(query)); + serde_path_to_error::deserialize(de).map_err(PaytoErr::Query) } } -impl<Impl: PaytoImpl> Deref for Payto<Impl> { - type Target = Impl; - - fn deref(&self) -> &Self::Target { - &self.parsed +impl AsRef<Url> for Payto { + fn as_ref(&self) -> &Url { + &self.0 } } -impl<Impl: PaytoImpl> std::fmt::Display for Payto<Impl> { +impl std::fmt::Display for Payto { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Display::fmt(&self.raw, f) - } -} - -impl<Impl: PaytoImpl> AsRef<Url> for Payto<Impl> { - fn as_ref(&self) -> &Url { - &self.raw + std::fmt::Display::fmt(self.raw(), f) } } #[derive(Debug, thiserror::Error)] -pub enum ParsePaytoErr { +pub enum PaytoErr { #[error("invalid payto URI: {0}")] Url(#[from] url::ParseError), - #[error("malformed payto URI: {0}")] - Malformed(#[from] serde_path_to_error::Error<serde_urlencoded::de::Error>), + #[error("malformed payto URI query: {0}")] + Query(#[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(Box<dyn std::error::Error + Sync + Send + 'static>), } -fn parse_payto<Impl: PaytoImpl, Query: DeserializeOwned>( - s: &str, -) -> Result<(Payto<Impl>, Query), Impl::ParseErr> { - // Parse url - let raw: Url = s.parse().map_err(ParsePaytoErr::Url)?; - // Check scheme - if raw.scheme() != "payto" { - Err(ParsePaytoErr::NotPayto(raw.scheme().to_owned()))?; +impl PaytoErr { + pub fn custom<E: std::error::Error + Sync + Send + 'static>(e: E) -> Self { + Self::Custom(Box::new(e)) } - // Parse implementation - let parsed = Impl::parse(&raw)?; - // 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).map_err(ParsePaytoErr::Malformed)?; - - Ok((Payto { raw, parsed }, query)) } -impl<Impl: PaytoImpl> FromStr for Payto<Impl> { - type Err = Impl::ParseErr; +impl FromStr for Payto { + type Err = PaytoErr; fn from_str(s: &str) -> Result<Self, Self::Err> { - #[derive(serde::Deserialize)] - struct Query {} - let (payto, _) = parse_payto::<Impl, Query>(s)?; - Ok(payto) + // Parse url + let url: Url = s.parse()?; + // Check scheme + if url.scheme() != "payto" { + return Err(PaytoErr::NotPayto(url.scheme().to_owned())); + } + Ok(Self(url)) } } /// RFC 8905 payto URI -#[derive( - Debug, Clone, PartialEq, Eq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay, -)] -pub struct FullPayto<Impl: PaytoImpl> { - payto: Payto<Impl>, +#[derive(Debug, Clone, Deserialize)] +pub struct FullPayto { + #[serde(rename = "receiver-name")] 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 = 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) = parse_payto::<Impl, Query>(s)?; - - Ok(Self { - payto, - receiver_name: query.receiver_name, - }) - } -} diff --git a/common/test-utils/src/routine.rs b/common/test-utils/src/routine.rs @@ -34,12 +34,7 @@ use taler_common::{ TransferStatus, }, error_code::ErrorCode, - types::{ - amount::amount, - base32::Base32, - payto::{Payto, PaytoImpl}, - url, - }, + types::{amount::amount, base32::Base32, payto::Payto, url}, }; use tokio::time::sleep; @@ -268,10 +263,10 @@ async fn get_currency(server: &TestServer) -> String { } /// Test standard behavior of the transfer endpoints -pub async fn transfer_routine<P: PaytoImpl + Eq + Debug>( +pub async fn transfer_routine( server: &TestServer, default_status: TransferState, - credit_account: &Payto<P>, + credit_account: &Payto, ) { let currency = &get_currency(server).await; let default_amount = amount(format!("{currency}:42")); @@ -342,7 +337,7 @@ pub async fn transfer_routine<P: PaytoImpl + Eq + Debug>( let tx = server .get(&format!("/transfers/{}", resp.row_id)) .await - .assert_ok_json::<TransferStatus<P>>(); + .assert_ok_json::<TransferStatus>(); assert_eq!(default_status, tx.status); assert_eq!(default_amount, tx.amount); assert_eq!("http://exchange.taler/", tx.origin_exchange_url); @@ -372,19 +367,19 @@ pub async fn transfer_routine<P: PaytoImpl + Eq + Debug>( let list = server .get("/transfers") .await - .assert_ok_json::<TransferList<P>>(); + .assert_ok_json::<TransferList>(); assert_eq!(list.transfers.len(), 6); assert_eq!( list, server .get(&format!("/transfers?status={}", default_status.as_ref())) .await - .assert_ok_json::<TransferList<P>>() + .assert_ok_json::<TransferList>() ); } // Pagination test - routine_pagination::<TransferList<P>, _>( + routine_pagination::<TransferList, _>( server, "/transfers", |it| { @@ -411,11 +406,11 @@ pub async fn transfer_routine<P: PaytoImpl + Eq + Debug>( } } -async fn add_incoming_routine<P: PaytoImpl>( +async fn add_incoming_routine( server: &TestServer, currency: &str, kind: IncomingType, - debit_acount: &Payto<P>, + debit_acount: &Payto, ) { let (path, key) = match kind { IncomingType::reserve => ("/admin/add-incoming", "reserve_pub"), @@ -484,10 +479,7 @@ async fn add_incoming_routine<P: PaytoImpl>( } /// Test standard behavior of the admin add incoming endpoints -pub async fn admin_add_incoming_routine<P: PaytoImpl>( - server: &TestServer, - debit_acount: &Payto<P>, -) { +pub async fn admin_add_incoming_routine(server: &TestServer, debit_acount: &Payto) { let currency = &get_currency(server).await; // History @@ -495,7 +487,7 @@ pub async fn admin_add_incoming_routine<P: PaytoImpl>( routine_history( server, "/history/incoming", - |it: IncomingHistory<P>| { + |it: IncomingHistory| { it.incoming_transactions .into_iter() .map(|it| match it { diff --git a/wire-gateway/magnet-bank/src/db.rs b/wire-gateway/magnet-bank/src/db.rs @@ -27,15 +27,11 @@ use taler_common::{ IncomingBankTransaction, OutgoingBankTransaction, TransferListStatus, TransferRequest, TransferState, TransferStatus, }, - types::{ - amount::Amount, - payto::{AnyPayto, Payto}, - timestamp::Timestamp, - }, + types::{amount::Amount, payto::Payto, timestamp::Timestamp}, }; use tokio::sync::watch::{Receiver, Sender}; -use crate::{constant::CURRENCY, MagnetPayto}; +use crate::constant::CURRENCY; pub async fn notification_listener( pool: PgPool, @@ -65,7 +61,7 @@ pub struct TxIn { pub code: u64, pub amount: Amount, pub subject: String, - pub debtor: Payto<MagnetPayto>, + pub debtor: Payto, pub timestamp: Timestamp, } @@ -87,7 +83,7 @@ pub struct TxOut { pub code: u64, pub amount: Amount, pub subject: String, - pub creditor: Payto<MagnetPayto>, + pub creditor: Payto, pub timestamp: Timestamp, } @@ -108,7 +104,7 @@ impl Display for TxOut { pub struct TxInAdmin { pub amount: Amount, pub subject: String, - pub debit_payto: Payto<MagnetPayto>, + pub debit_payto: Payto, pub timestamp: Timestamp, pub metadata: IncomingSubject, } @@ -131,7 +127,7 @@ pub struct Initiated { pub id: u64, pub amount: Amount, pub subject: String, - pub creditor: Payto<MagnetPayto>, + pub creditor: Payto, } impl Display for Initiated { @@ -258,7 +254,7 @@ pub enum TransferResult { pub async fn make_transfer<'a>( db: impl PgExecutor<'a>, - req: &TransferRequest<MagnetPayto>, + req: &TransferRequest, timestamp: &Timestamp, ) -> sqlx::Result<TransferResult> { let subject = format!("{} {}", req.wtid, req.exchange_base_url); @@ -295,7 +291,7 @@ pub async fn transfer_page<'a>( db: impl PgExecutor<'a>, status: &Option<TransferState>, params: &Page, -) -> sqlx::Result<Vec<TransferListStatus<AnyPayto>>> { +) -> sqlx::Result<Vec<TransferListStatus>> { page( db, "initiated_id", @@ -337,7 +333,7 @@ pub async fn outgoing_history( db: &PgPool, params: &History, listen: impl FnOnce() -> Receiver<i64>, -) -> sqlx::Result<Vec<OutgoingBankTransaction<AnyPayto>>> { +) -> sqlx::Result<Vec<OutgoingBankTransaction>> { history( db, "tx_out_id", @@ -378,7 +374,7 @@ pub async fn incoming_history( db: &PgPool, params: &History, listen: impl FnOnce() -> Receiver<i64>, -) -> sqlx::Result<Vec<IncomingBankTransaction<AnyPayto>>> { +) -> sqlx::Result<Vec<IncomingBankTransaction>> { history( db, "tx_in_id", @@ -429,7 +425,7 @@ pub async fn incoming_history( pub async fn transfer_by_id<'a>( db: impl PgExecutor<'a>, id: u64, -) -> sqlx::Result<Option<TransferStatus<AnyPayto>>> { +) -> sqlx::Result<Option<TransferStatus>> { sqlx::query( " SELECT diff --git a/wire-gateway/magnet-bank/src/dev.rs b/wire-gateway/magnet-bank/src/dev.rs @@ -49,15 +49,15 @@ pub enum DevCmd { /// Print account info Accounts, Tx { - account: Payto<MagnetPayto>, + account: Payto, #[clap(long, short, value_enum, default_value_t = DirArg::Both)] direction: DirArg, }, Transfer { #[clap(long)] - debtor: Payto<MagnetPayto>, + debtor: Payto, #[clap(long)] - creditor: FullPayto<MagnetPayto>, + creditor: Payto, #[clap(long)] amount: Amount, #[clap(long)] @@ -84,6 +84,7 @@ pub async fn dev(cfg: Config, cmd: DevCmd) -> anyhow::Result<()> { } } DevCmd::Tx { account, direction } => { + let account = MagnetPayto::try_from(&account)?; let dir = match direction { DirArg::Incoming => Direction::Incoming, DirArg::Outgoing => Direction::Outgoing, @@ -93,7 +94,7 @@ pub async fn dev(cfg: Config, cmd: DevCmd) -> anyhow::Result<()> { let mut next = None; loop { let page = client - .page_tx(dir, 5, &account.account, &next, &None) + .page_tx(dir, 5, &account.number, &next, &None) .await?; next = page.next; for item in page.list { @@ -131,7 +132,10 @@ pub async fn dev(cfg: Config, cmd: DevCmd) -> anyhow::Result<()> { amount, subject, } => { - let debtor = client.account(&debtor.account).await?; + let full: FullPayto = creditor.query()?; + let debtor = MagnetPayto::try_from(&debtor)?; + let creditor = MagnetPayto::try_from(&creditor)?; + let debtor = client.account(&debtor.number).await?; let now = Zoned::now(); let date = now.date(); @@ -141,8 +145,8 @@ pub async fn dev(cfg: Config, cmd: DevCmd) -> anyhow::Result<()> { amount.val as f64, &subject, date, - &creditor.receiver_name, - &creditor.account, + &full.receiver_name, + &creditor.number, ) .await?; client @@ -152,7 +156,7 @@ pub async fn dev(cfg: Config, cmd: DevCmd) -> anyhow::Result<()> { init.code, init.amount, date, - &creditor.account, + &creditor.number, ) .await?; } diff --git a/wire-gateway/magnet-bank/src/lib.rs b/wire-gateway/magnet-bank/src/lib.rs @@ -14,8 +14,7 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -use reqwest::Url; -use taler_common::types::payto::{ParsePaytoErr, PaytoImpl}; +use taler_common::types::payto::{Payto, PaytoErr}; pub mod config; pub mod constant; @@ -27,40 +26,39 @@ pub mod wire_gateway; #[derive(Debug, Clone, PartialEq, Eq)] pub struct MagnetPayto { - pub account: String, + pub number: String, } #[derive(Debug, thiserror::Error)] pub enum MagnetPaytoErr { #[error("missing Magnet Bank account number in path")] MissingAccount, - #[error(transparent)] - Other(#[from] ParsePaytoErr), } const MAGNET_BANK: &str = "magnet-bank"; -impl PaytoImpl for MagnetPayto { - type ParseErr = MagnetPaytoErr; +impl TryFrom<&Payto> for MagnetPayto { + type Error = PaytoErr; - fn parse(url: &Url) -> Result<Self, Self::ParseErr> { + fn try_from(value: &Payto) -> Result<Self, Self::Error> { + let url = value.as_ref(); if url.domain() != Some(MAGNET_BANK) { - Err(ParsePaytoErr::UnsupportedKind( + return Err(PaytoErr::UnsupportedKind( MAGNET_BANK, url.domain().unwrap_or_default().to_owned(), - ))?; + )); } let Some(mut segments) = url.path_segments() else { - return Err(MagnetPaytoErr::MissingAccount); + return Err(PaytoErr::custom(MagnetPaytoErr::MissingAccount)); }; let Some(account) = segments.next() else { - return Err(MagnetPaytoErr::MissingAccount); + return Err(PaytoErr::custom(MagnetPaytoErr::MissingAccount)); }; if segments.next().is_some() { - Err(ParsePaytoErr::TooLong(MAGNET_BANK))?; + return Err(PaytoErr::TooLong(MAGNET_BANK)); } Ok(Self { - account: account.to_owned(), + number: account.to_owned(), }) } } diff --git a/wire-gateway/magnet-bank/src/wire_gateway.rs b/wire-gateway/magnet-bank/src/wire_gateway.rs @@ -28,22 +28,18 @@ use taler_common::{ TransferState, TransferStatus, }, error_code::ErrorCode, - types::{ - payto::{AnyPayto, Payto}, - timestamp::Timestamp, - }, + types::{payto::Payto, timestamp::Timestamp}, }; use tokio::sync::watch::Sender; use crate::{ constant::CURRENCY, db::{self, AddIncomingResult, TxInAdmin}, - MagnetPayto, }; pub struct MagnetWireGateway { pub pool: sqlx::PgPool, - pub payto: Payto<MagnetPayto>, + pub payto: Payto, pub in_channel: Sender<i64>, pub taler_in_channel: Sender<i64>, pub out_channel: Sender<i64>, @@ -51,7 +47,7 @@ pub struct MagnetWireGateway { } impl MagnetWireGateway { - pub async fn start(pool: sqlx::PgPool, payto: Payto<MagnetPayto>) -> Self { + pub async fn start(pool: sqlx::PgPool, payto: Payto) -> Self { let in_channel = Sender::new(0); let taler_in_channel = Sender::new(0); let out_channel = Sender::new(0); @@ -75,7 +71,7 @@ impl MagnetWireGateway { } } -impl WireGatewayImpl<MagnetPayto> for MagnetWireGateway { +impl WireGatewayImpl for MagnetWireGateway { fn name(&self) -> &str { "magnet-bank" } @@ -88,7 +84,7 @@ impl WireGatewayImpl<MagnetPayto> for MagnetWireGateway { None } - async fn transfer(&self, req: TransferRequest<MagnetPayto>) -> ApiResult<TransferResponse> { + async fn transfer(&self, req: TransferRequest) -> ApiResult<TransferResponse> { let result = db::make_transfer(&self.pool, &req, &Timestamp::now()).await?; match result { db::TransferResult::Success { id, timestamp } => Ok(TransferResponse { @@ -107,40 +103,40 @@ impl WireGatewayImpl<MagnetPayto> for MagnetWireGateway { &self, page: Page, status: Option<TransferState>, - ) -> ApiResult<TransferList<AnyPayto>> { + ) -> ApiResult<TransferList> { Ok(TransferList { transfers: db::transfer_page(&self.pool, &status, &page).await?, - debit_account: self.payto.clone().generic(), + debit_account: self.payto.clone(), }) } - async fn transfer_by_id(&self, id: u64) -> ApiResult<Option<TransferStatus<AnyPayto>>> { + async fn transfer_by_id(&self, id: u64) -> ApiResult<Option<TransferStatus>> { Ok(db::transfer_by_id(&self.pool, id).await?) } - async fn outgoing_history(&self, params: History) -> ApiResult<OutgoingHistory<AnyPayto>> { + async fn outgoing_history(&self, params: History) -> ApiResult<OutgoingHistory> { Ok(OutgoingHistory { outgoing_transactions: db::outgoing_history(&self.pool, &params, || { self.taler_out_channel.subscribe() }) .await?, - debit_account: self.payto.clone().generic(), + debit_account: self.payto.clone(), }) } - async fn incoming_history(&self, params: History) -> ApiResult<IncomingHistory<AnyPayto>> { + async fn incoming_history(&self, params: History) -> ApiResult<IncomingHistory> { Ok(IncomingHistory { incoming_transactions: db::incoming_history(&self.pool, &params, || { self.taler_in_channel.subscribe() }) .await?, - credit_account: self.payto.clone().generic(), + credit_account: self.payto.clone(), }) } async fn add_incoming_reserve( &self, - req: AddIncomingRequest<MagnetPayto>, + req: AddIncomingRequest, ) -> ApiResult<AddIncomingResponse> { let res = db::register_tx_in_admin( &self.pool, @@ -165,10 +161,7 @@ impl WireGatewayImpl<MagnetPayto> for MagnetWireGateway { } } - async fn add_incoming_kyc( - &self, - req: AddKycauthRequest<MagnetPayto>, - ) -> ApiResult<AddKycauthResponse> { + async fn add_incoming_kyc(&self, req: AddKycauthRequest) -> 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 @@ -16,7 +16,7 @@ use std::sync::Arc; -use magnet_bank::{db, wire_gateway::MagnetWireGateway, MagnetPayto}; +use magnet_bank::{db, wire_gateway::MagnetWireGateway}; use sqlx::PgPool; use taler_api::{auth::AuthMethod, standard_layer, subject::OutgoingSubject}; use taler_common::{ @@ -47,7 +47,7 @@ async fn setup() -> (TestServer, PgPool) { #[tokio::test] async fn transfer() { let (server, _) = setup().await; - transfer_routine::<MagnetPayto>( + transfer_routine( &server, TransferState::pending, &payto("payto://magnet-bank/todo"), @@ -59,7 +59,7 @@ async fn transfer() { async fn outgoing_history() { let (server, pool) = setup().await; server.get("/history/outgoing").await.assert_no_content(); - routine_pagination::<OutgoingHistory<MagnetPayto>, _>( + routine_pagination::<OutgoingHistory, _>( &server, "/history/outgoing", |it| { @@ -97,5 +97,5 @@ async fn outgoing_history() { #[tokio::test] async fn admin_add_incoming() { let (server, _) = setup().await; - admin_add_incoming_routine::<MagnetPayto>(&server, &payto("payto://magnet-bank/todo")).await; + admin_add_incoming_routine(&server, &payto("payto://magnet-bank/todo")).await; }