taler-rust

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

commit f0e0fa6ba55894ad5bd63f1b3b94767eb42def2e
parent 664cd180f65c314bae731743dbd481e4314a8ed7
Author: Antoine A <>
Date:   Fri, 29 May 2026 12:35:41 +0200

common: update Prepared Transfer API and improve routines

Diffstat:
MMakefile | 6+++++-
Mcommon/taler-api/src/api/wire.rs | 5++++-
Mcommon/taler-api/src/test.rs | 32+++++++++++++++++++++-----------
Mcommon/taler-api/src/test/api.rs | 42++++++++++++++++++++++--------------------
Mcommon/taler-common/src/api/prepared.rs | 6++++--
Mcommon/taler-common/src/config.rs | 25+++----------------------
Mcommon/taler-common/src/types.rs | 25+++++++++++++++++++++++++
Mcommon/taler-common/src/types/payto.rs | 24++++++++++++++----------
Mcommon/taler-test-utils/src/routine.rs | 180+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mtaler-cyclos/src/api.rs | 43+++++++++++++++++++++++++++----------------
Mtaler-cyclos/src/config.rs | 6+++---
Mtaler-magnet-bank/src/api.rs | 35+++++++++++++++++++++--------------
Mtaler-magnet-bank/src/config.rs | 5++---
13 files changed, 273 insertions(+), 161 deletions(-)

diff --git a/Makefile b/Makefile @@ -7,7 +7,7 @@ abs_destdir=$(abspath $(DESTDIR)) bin_dir=$(abs_destdir)$(prefix)/bin lib_dir=$(abs_destdir)$(prefix)/lib share_dir=$(abs_destdir)$(prefix)/share -man_dir=$(abs_destdir)$(prefix)/share/man +man_dir=$(share_dir)/man all: build @@ -59,6 +59,10 @@ deb: ci: contrib/ci/run-all-jobs.sh +.PHONY: fmt +fmt: + rustfmt-unstable --apply + .PHONY: coverage-cyclos coverage-cyclos: cargo llvm-cov clean --workspace diff --git a/common/taler-api/src/api/wire.rs b/common/taler-api/src/api/wire.rs @@ -34,7 +34,7 @@ use taler_common::{ }, }, error_code::ErrorCode, - types::amount::Currency, + types::{amount::Currency, validate_base_url}, }; use super::TalerApi; @@ -109,6 +109,9 @@ impl Validation for TransferRequest { METADATA_PATTERN.as_str() ))); } + if let Err(err) = validate_base_url(&self.exchange_base_url) { + return Err(bad_request(err).with_path("exchange_base_url")); + } check_currency(currency, &self.amount) } } diff --git a/common/taler-api/src/test.rs b/common/taler-api/src/test.rs @@ -1,4 +1,7 @@ -use std::sync::{Arc, LazyLock}; +use std::{ + str::FromStr, + sync::{Arc, LazyLock}, +}; use axum::{ Router, @@ -18,7 +21,7 @@ use taler_common::{ types::{ amount::{Amount, Currency, amount}, base32::Base32, - payto::{PaytoURI, payto}, + payto::{FullIbanPayto, PaytoURI, payto}, url, }, }; @@ -44,12 +47,21 @@ use crate::{ mod api; mod db; +static PAYTO: LazyLock<FullIbanPayto> = LazyLock::new(|| { + FullIbanPayto::from_str("payto://iban/HU02162000031000164800000000?receiver-name=Smith") + .unwrap() +}); +static EXCHANGE: LazyLock<PaytoURI> = LazyLock::new(|| PAYTO.as_uri()); +static UNKNOWN: LazyLock<PaytoURI> = + LazyLock::new(|| payto("payto://iban/HU60162006491000639900000000?receiver-name=Unknown")); + fn test_api(pool: PgPool, currency: Currency) -> Router { let outgoing_channel = Sender::new(0); let incoming_channel = Sender::new(0); let wg = TestApi { currency, pool: pool.clone(), + payto: PAYTO.clone(), outgoing_channel: outgoing_channel.clone(), incoming_channel: incoming_channel.clone(), }; @@ -81,7 +93,7 @@ async fn body_parsing() { amount: Amount::zero(&Currency::EUR), exchange_base_url: url("https://test.com"), wtid: Base32::rand(), - credit_account: payto("payto:://test?receiver-name=lol"), + credit_account: EXCHANGE.clone(), metadata: None, }; @@ -156,8 +168,6 @@ async fn body_parsing() { .assert_error(ErrorCode::GENERIC_UPLOAD_EXCEEDS_LIMIT); } -static PAYTO: LazyLock<PaytoURI> = LazyLock::new(|| payto("payto://test?receiver-name=Test")); - #[tokio::test] async fn errors() { let (server, _) = setup().await; @@ -191,7 +201,7 @@ async fn config() { #[tokio::test] async fn transfer() { let (server, _) = setup().await; - transfer_routine(&server, TransferState::success, &PAYTO).await; + transfer_routine(&server, TransferState::success, &EXCHANGE).await; } #[tokio::test] @@ -207,7 +217,7 @@ async fn outgoing_history() { "amount": amount("EUR:1"), "exchange_base_url": url("http://exchange.taler"), "wtid": ShortHashCode::rand(), - "credit_account": PAYTO.clone(), + "credit_account": EXCHANGE.clone(), })) .await .assert_ok_json::<TransferResponse>(); @@ -220,19 +230,19 @@ async fn outgoing_history() { #[tokio::test] async fn admin_add_incoming() { let (server, _) = setup().await; - admin_add_incoming_routine(&server, &PAYTO, true).await; + admin_add_incoming_routine(&server, &EXCHANGE, &EXCHANGE, true).await; } #[tokio::test] async fn in_history() { let (server, _) = setup().await; - in_history_routine(&server, &PAYTO, true, tasks!(), tasks!()).await; + in_history_routine(&server, &EXCHANGE, &EXCHANGE, true, tasks!(), tasks!()).await; } #[tokio::test] async fn revenue() { let (server, _) = setup().await; - revenue_routine(&server, &PAYTO, true, tasks!(), tasks!()).await; + revenue_routine(&server, &EXCHANGE, true, tasks!(), tasks!()).await; } #[tokio::test] @@ -280,5 +290,5 @@ async fn check_in(pool: &PgPool) -> Vec<Status> { #[tokio::test] async fn registration() { let (server, pool) = setup().await; - registration_routine(&server, &PAYTO, || check_in(&pool)).await; + registration_routine(&server, &EXCHANGE, &EXCHANGE, &UNKNOWN, || check_in(&pool)).await; } diff --git a/common/taler-api/src/test/api.rs b/common/taler-api/src/test/api.rs @@ -28,12 +28,8 @@ use taler_common::{ }, }, db::IncomingType, - error_code::ErrorCode, - types::{ - amount::Currency, - payto::{FullQuery, payto}, - timestamp::TalerTimestamp, - }, + error_code::ErrorCode::{self}, + types::{amount::Currency, payto::FullIbanPayto, timestamp::TalerTimestamp}, }; use tokio::sync::watch::Sender; @@ -45,7 +41,7 @@ use crate::{ wire::WireGateway, }, error::{ApiResult, failure_code}, - test::db::{self, AddIncomingResult}, + test::db::{self, AddIncomingResult, RegistrationResult, TransferResult}, }; /// Taler API implementation for tests @@ -54,6 +50,7 @@ pub struct TestApi { pub pool: PgPool, pub outgoing_channel: Sender<i64>, pub incoming_channel: Sender<i64>, + pub payto: FullIbanPayto, } impl TalerApi for TestApi { @@ -68,16 +65,14 @@ impl TalerApi for TestApi { impl WireGateway for TestApi { async fn transfer(&self, req: TransferRequest) -> ApiResult<TransferResponse> { - req.credit_account.query::<FullQuery>()?; + FullIbanPayto::try_from(&req.credit_account)?; let result = db::transfer(&self.pool, &req).await?; match result { - db::TransferResult::Success(transfer_response) => Ok(transfer_response), - db::TransferResult::RequestUidReuse => { + TransferResult::Success(transfer_response) => Ok(transfer_response), + TransferResult::RequestUidReuse => { Err(failure_code(ErrorCode::BANK_TRANSFER_REQUEST_UID_REUSED)) } - db::TransferResult::WtidReuse => { - Err(failure_code(ErrorCode::BANK_TRANSFER_WTID_REUSED)) - } + TransferResult::WtidReuse => Err(failure_code(ErrorCode::BANK_TRANSFER_WTID_REUSED)), } } @@ -88,7 +83,7 @@ impl WireGateway for TestApi { ) -> ApiResult<TransferList> { Ok(TransferList { transfers: db::transfer_page(&self.pool, &status, &page, &self.currency).await?, - debit_account: payto("payto://test"), + debit_account: self.payto.as_uri(), }) } @@ -103,7 +98,7 @@ impl WireGateway for TestApi { .await?; Ok(OutgoingHistory { outgoing_transactions: txs, - debit_account: payto("payto://test"), + debit_account: self.payto.as_uri(), }) } @@ -114,7 +109,7 @@ impl WireGateway for TestApi { .await?; Ok(IncomingHistory { incoming_transactions: txs, - credit_account: payto("payto://test"), + credit_account: self.payto.as_uri(), }) } @@ -122,6 +117,7 @@ impl WireGateway for TestApi { &self, req: AddIncomingRequest, ) -> ApiResult<AddIncomingResponse> { + FullIbanPayto::try_from(&req.debit_account)?; let res = db::add_incoming( &self.pool, &req.amount, @@ -147,6 +143,7 @@ impl WireGateway for TestApi { } async fn add_incoming_kyc(&self, req: AddKycauthRequest) -> ApiResult<AddIncomingResponse> { + FullIbanPayto::try_from(&req.debit_account)?; let res = db::add_incoming( &self.pool, &req.amount, @@ -172,6 +169,7 @@ impl WireGateway for TestApi { } async fn add_incoming_mapped(&self, req: AddMappedRequest) -> ApiResult<AddIncomingResponse> { + FullIbanPayto::try_from(&req.debit_account)?; let res = db::add_incoming( &self.pool, &req.amount, @@ -212,7 +210,7 @@ impl Revenue for TestApi { .await?; Ok(RevenueIncomingHistory { incoming_transactions: txs, - credit_account: payto("payto://test"), + credit_account: self.payto.as_uri(), }) } } @@ -223,13 +221,17 @@ impl PreparedTransfer for TestApi { } async fn registration(&self, req: RegistrationRequest) -> ApiResult<RegistrationResponse> { + let creditor = FullIbanPayto::try_from(&req.credit_account)?; + if *creditor != *self.payto { + return Err(failure_code(ErrorCode::BANK_UNKNOWN_CREDITOR)); + } match db::transfer_register(&self.pool, &req).await? { - db::RegistrationResult::Success => ApiResult::Ok(RegistrationResponse { + RegistrationResult::Success => Ok(RegistrationResponse { subjects: vec![simple_subject(req)], expiration: TalerTimestamp::Never, }), - db::RegistrationResult::ReservePubReuse => { - ApiResult::Err(failure_code(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT)) + RegistrationResult::ReservePubReuse => { + Err(failure_code(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT)) } } } diff --git a/common/taler-common/src/api/prepared.rs b/common/taler-common/src/api/prepared.rs @@ -27,6 +27,7 @@ use crate::{ db::IncomingType, types::{ amount::{Amount, Currency}, + payto::PaytoURI, timestamp::TalerTimestamp, }, }; @@ -72,13 +73,14 @@ pub enum PublicKeyAlg { /// <https://docs.taler.net/core/api-bank-transfer.html#tsref-type-RegistrationRequest> #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RegistrationRequest { - pub credit_amount: Amount, + pub credit_account: PaytoURI, pub r#type: TransferType, + pub recurrent: bool, + pub credit_amount: Amount, pub alg: PublicKeyAlg, pub account_pub: EddsaPublicKey, pub authorization_pub: EddsaPublicKey, pub authorization_sig: EddsaSignature, - pub recurrent: bool, } /// <https://docs.taler.net/core/api-bank-transfer.html#tsref-type-TransferSubject> diff --git a/common/taler-common/src/config.rs b/common/taler-common/src/config.rs @@ -31,6 +31,7 @@ use url::Url; use crate::types::{ amount::{Amount, Currency}, payto::PaytoURI, + validate_base_url, }; pub mod parser { @@ -900,28 +901,8 @@ impl<'cfg, 'arg> Section<'cfg, 'arg> { pub fn base_url(&self, option: &'arg str) -> Value<'arg, Url> { self.value("url", option, |s| { let url = Url::from_str(s).map_err(|e| e.to_string())?; - if url.scheme() != "http" && url.scheme() != "https" { - Err(format!( - "only 'http' and 'https' are accepted for baseURL got '{}''", - url.scheme() - )) - } else if !url.has_host() { - Err(format!("missing host in baseURL got '{url}'")) - } else if url.query().is_some() { - Err(format!( - "require no query in baseURL got '{}'", - url.query().unwrap() - )) - } else if url.fragment().is_some() { - Err(format!( - "require no fragment in baseURL got '{}'", - url.fragment().unwrap() - )) - } else if !url.path().ends_with('/') { - Err(format!("baseURL path must end with / got '{}'", url.path())) - } else { - Ok(url) - } + validate_base_url(&url)?; + Ok::<_, String>(url) }) } diff --git a/common/taler-common/src/types.rs b/common/taler-common/src/types.rs @@ -26,3 +26,28 @@ use url::Url; pub fn url(url: &str) -> Url { url.parse().expect("Invalid url") } + +pub fn validate_base_url(url: &Url) -> Result<(), String> { + if url.scheme() != "http" && url.scheme() != "https" { + Err(format!( + "only 'http' and 'https' are accepted for baseURL got '{}''", + url.scheme() + )) + } else if !url.has_host() { + Err(format!("missing host in baseURL got '{url}'")) + } else if url.query().is_some() { + Err(format!( + "require no query in baseURL got '{}'", + url.query().unwrap() + )) + } else if url.fragment().is_some() { + Err(format!( + "require no fragment in baseURL got '{}'", + url.fragment().unwrap() + )) + } else if !url.path().ends_with('/') { + Err(format!("baseURL path must end with / got '{}'", url.path())) + } else { + Ok(()) + } +} diff --git a/common/taler-common/src/types/payto.rs b/common/taler-common/src/types/payto.rs @@ -266,25 +266,29 @@ pub struct ParsedQuery { } #[derive(Debug, Clone, Copy, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] -pub struct Payto<P>(P); +pub struct Payto<P> { + inner: P, +} impl<P> Payto<P> { pub fn convert<T: From<P>>(self) -> Payto<T> { - Payto(self.0.into()) + Payto { + inner: self.inner.into(), + } } } impl<P: PaytoImpl> Payto<P> { pub fn new(inner: P) -> Self { - Self(inner) + Self { inner } } pub fn as_uri(&self) -> PaytoURI { - self.0.as_uri() + self.inner.as_uri() } pub fn into_inner(self) -> P { - self.0 + self.inner } } @@ -292,19 +296,19 @@ impl<P: PaytoImpl> TryFrom<&PaytoURI> for Payto<P> { type Error = PaytoErr; fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> { - Ok(Self(P::parse(value)?)) + Ok(Self::new(P::parse(value)?)) } } impl<P: PaytoImpl> From<FullPayto<P>> for Payto<P> { fn from(value: FullPayto<P>) -> Payto<P> { - Payto(value.inner) + Self::new(value.inner) } } impl<P: PaytoImpl> From<TransferPayto<P>> for Payto<P> { fn from(value: TransferPayto<P>) -> Payto<P> { - Payto(value.inner) + Self::new(value.inner) } } @@ -327,13 +331,13 @@ impl<P: PaytoImpl> Deref for Payto<P> { type Target = P; fn deref(&self) -> &Self::Target { - &self.0 + &self.inner } } impl<P: PaytoImpl> DerefMut for Payto<P> { fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 + &mut self.inner } } diff --git a/common/taler-test-utils/src/routine.rs b/common/taler-test-utils/src/routine.rs @@ -50,6 +50,8 @@ use crate::{ server::{TestResponse, TestServer as _}, }; +const UNKNOWN: &str = "payto://malformed/unused?receiver-name=Malformed"; + pub trait Page: DeserializeOwned + Debug { fn ids(&self) -> Vec<i64>; } @@ -330,7 +332,7 @@ pub async fn transfer_routine( let default_amount = amount(format!("{currency}:42")); let request_uid = HashCode::rand(); let wtid = ShortHashCode::rand(); - let transfer_req = json!({ + let valid_req = json!({ "request_uid": request_uid, "amount": default_amount, "exchange_base_url": "http://exchange.taler/", @@ -421,7 +423,7 @@ pub async fn transfer_routine( // Check request uid reuse server .post("/taler-wire-gateway/transfer") - .json(&json!(transfer_req + { + .json(&json!(valid_req + { "wtid": ShortHashCode::rand() })) .await @@ -429,7 +431,7 @@ pub async fn transfer_routine( // Check wtid reuse server .post("/taler-wire-gateway/transfer") - .json(&json!(transfer_req + { + .json(&json!(valid_req + { "request_uid": HashCode::rand(), })) .await @@ -438,7 +440,7 @@ pub async fn transfer_routine( // Check currency mismatch server .post("/taler-wire-gateway/transfer") - .json(&json!(transfer_req + { + .json(&json!(valid_req + { "amount": "BAD:42" })) .await @@ -447,28 +449,28 @@ pub async fn transfer_routine( // Base Base32 server .post("/taler-wire-gateway/transfer") - .json(&json!(transfer_req + { + .json(&json!(valid_req + { "wtid": "I love chocolate" })) .await .assert_error(ErrorCode::GENERIC_JSON_INVALID); server .post("/taler-wire-gateway/transfer") - .json(&json!(transfer_req + { + .json(&json!(valid_req + { "wtid": Base32::<31>::rand() })) .await .assert_error(ErrorCode::GENERIC_JSON_INVALID); server .post("/taler-wire-gateway/transfer") - .json(&json!(transfer_req + { + .json(&json!(valid_req + { "request_uid": "I love chocolate" })) .await .assert_error(ErrorCode::GENERIC_JSON_INVALID); server .post("/taler-wire-gateway/transfer") - .json(&json!(transfer_req + { + .json(&json!(valid_req + { "request_uid": Base32::<65>::rand() })) .await @@ -477,7 +479,7 @@ pub async fn transfer_routine( // Missing receiver-name let res = server .post("/taler-wire-gateway/transfer") - .json(&json!(transfer_req + { + .json(&json!(valid_req + { "credit_account": credit_account.as_ref().as_str().split('?').next().unwrap() })) .await; @@ -485,9 +487,21 @@ pub async fn transfer_routine( res.assert_error(ErrorCode::GENERIC_PAYTO_URI_MALFORMED); } - // TODO check bad payto - // TODO Bad base URL - /*for base_url in [ + // Unsupported payto kind + server + .post("/taler-wire-gateway/transfer") + .json(&json!(valid_req + { "credit_account": UNKNOWN })) + .await + .assert_error(ErrorCode::GENERIC_PAYTO_URI_MALFORMED); + // Malformed payto + server + .post("/taler-wire-gateway/transfer") + .json(&json!(valid_req + { "credit_account": "http://email@test.com" })) + .await + .assert_error(ErrorCode::GENERIC_JSON_INVALID); + + // Bad base URL + for base_url in [ "not-a-url", "file://not.http.com/", "no.transport.com/", @@ -495,19 +509,16 @@ pub async fn transfer_routine( ] { server .post("/taler-wire-gateway/transfer") - .json(&json!(transfer_req + { - "exchange_base_url": base_url - })) + .json(&json!(valid_req + { "exchange_base_url": base_url })) .await .assert_error(ErrorCode::GENERIC_JSON_INVALID); - }*/ + } + // Malformed metadata for metadata in ["bad_id", "bad id", "bad@id.com", &"A".repeat(41)] { server .post("/taler-wire-gateway/transfer") - .json(&json!(transfer_req + { - "metadata": metadata - })) + .json(&json!(valid_req + { "metadata": metadata })) .await .assert_error(ErrorCode::GENERIC_JSON_INVALID); } @@ -527,7 +538,7 @@ pub async fn transfer_routine( for _ in 0..4 { server .post("/taler-wire-gateway/transfer") - .json(&json!(transfer_req + { + .json(&json!(valid_req + { "request_uid": HashCode::rand(), "wtid": ShortHashCode::rand(), })) @@ -579,6 +590,7 @@ async fn add_incoming_routine( currency: &str, kind: IncomingType, debit_acount: &PaytoURI, + credit_account: &PaytoURI, ) { let (path, key) = match kind { IncomingType::reserve => ("/taler-wire-gateway/admin/add-incoming", "reserve_pub"), @@ -591,13 +603,14 @@ async fn add_incoming_routine( server .post("/taler-prepared-transfer/registration") .json(json!({ + "credit_account": credit_account, "type": "reserve", + "recurrent": false, "credit_amount": format!("{currency}:44"), "alg": "EdDSA", "account_pub": pub_key, "authorization_pub": pub_key, "authorization_sig": eddsa_sign(&key_pair, pub_key.as_ref()), - "recurrent": false })) .await .assert_ok_json::<RegistrationResponse>(); @@ -646,35 +659,33 @@ async fn add_incoming_routine( // Currency mismatch server .post(path) - .json(&json!(valid_req + { - "amount": "BAD:33" - })) + .json(&json!(valid_req + { "amount": "BAD:33" })) .await .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH); // Bad BASE32 reserve_pub server .post(path) - .json(&json!(valid_req + { - key: "I love chocolate" - })) + .json(&json!(valid_req + { key: "I love chocolate" })) .await .assert_error(ErrorCode::GENERIC_JSON_INVALID); - server .post(path) - .json(&json!(valid_req + { - key: Base32::<31>::rand() - })) + .json(&json!(valid_req + { key: Base32::<31>::rand() })) .await .assert_error(ErrorCode::GENERIC_JSON_INVALID); - // Bad payto kind + // Unsupported payto kind server .post(path) - .json(&json!(valid_req + { - "debit_account": "http://email@test.com" - })) + .json(&json!(valid_req + { "debit_account": UNKNOWN })) + .await + .assert_error(ErrorCode::GENERIC_PAYTO_URI_MALFORMED); + + // Malformed payto + server + .post(path) + .json(&json!(valid_req + { "debit_account": "http://email@test.com" })) .await .assert_error(ErrorCode::GENERIC_JSON_INVALID); } @@ -834,7 +845,8 @@ pub async fn out_history_routine( /// Test standard behavior of the incoming history endpoint pub async fn in_history_routine( server: &Router, - debit_acount: &PaytoURI, + debit_account: &PaytoURI, + credit_account: &PaytoURI, kyc: bool, register: Tasks<impl AsyncFnMut(usize)>, ignored: Tasks<impl AsyncFnMut(usize)>, @@ -852,7 +864,7 @@ pub async fn in_history_routine( .json(json!({ "amount": format!("{currency}:1"), "reserve_pub": EddsaPublicKey::rand(), - "debit_account": debit_acount, + "debit_account": debit_account, })) .await .assert_ok_json::<TransferResponse>(); @@ -865,6 +877,7 @@ pub async fn in_history_routine( server .post("/taler-prepared-transfer/registration") .json(json!({ + "credit_account": credit_account, "credit_amount": amount, "type": "reserve", "alg": "EdDSA", @@ -880,7 +893,7 @@ pub async fn in_history_routine( .json(json!({ "amount": amount, "authorization_pub": auth_pub, - "debit_account": debit_acount, + "debit_account": debit_account, })) .await .assert_ok_json::<TransferResponse>(); @@ -889,7 +902,7 @@ pub async fn in_history_routine( .json(json!({ "amount": amount, "authorization_pub": auth_pub, - "debit_account": debit_acount, + "debit_account": debit_account, })) .await .assert_ok_json::<TransferResponse>(); @@ -900,6 +913,7 @@ pub async fn in_history_routine( server .post("/taler-prepared-transfer/registration") .json(json!({ + "credit_account": credit_account, "credit_amount": format!("{currency}:3"), "type": "reserve", "alg": "EdDSA", @@ -917,7 +931,7 @@ pub async fn in_history_routine( .json(json!({ "amount": format!("{currency}:4"), "account_pub": EddsaPublicKey::rand(), - "debit_account": debit_acount, + "debit_account": debit_account, })) .await .assert_ok_json::<TransferResponse>(); @@ -930,6 +944,7 @@ pub async fn in_history_routine( server .post("/taler-prepared-transfer/registration") .json(json!({ + "credit_account": credit_account, "credit_amount": amount, "type": "kyc", "alg": "EdDSA", @@ -945,7 +960,7 @@ pub async fn in_history_routine( .json(json!({ "amount": amount, "authorization_pub": auth_pub, - "debit_account": debit_acount, + "debit_account": debit_account, })) .await .assert_ok_json::<TransferResponse>(); @@ -954,7 +969,7 @@ pub async fn in_history_routine( .json(json!({ "amount": amount, "authorization_pub": auth_pub, - "debit_account": debit_acount, + "debit_account": debit_account, })) .await .assert_ok_json::<TransferResponse>(); @@ -965,6 +980,7 @@ pub async fn in_history_routine( server .post("/taler-prepared-transfer/registration") .json(json!({ + "credit_account": credit_account, "credit_amount": format!("{currency}:6"), "type": "kyc", "alg": "EdDSA", @@ -983,12 +999,38 @@ pub async fn in_history_routine( } /// Test standard behavior of the admin add incoming endpoints -pub async fn admin_add_incoming_routine(server: &Router, debit_acount: &PaytoURI, kyc: bool) { +pub async fn admin_add_incoming_routine( + server: &Router, + debit_acount: &PaytoURI, + credit_account: &PaytoURI, + kyc: bool, +) { let currency = &get_currency(server).await; - add_incoming_routine(server, currency, IncomingType::reserve, debit_acount).await; - add_incoming_routine(server, currency, IncomingType::map, debit_acount).await; + add_incoming_routine( + server, + currency, + IncomingType::reserve, + debit_acount, + credit_account, + ) + .await; + add_incoming_routine( + server, + currency, + IncomingType::map, + debit_acount, + credit_account, + ) + .await; if kyc { - add_incoming_routine(server, currency, IncomingType::kyc, debit_acount).await; + add_incoming_routine( + server, + currency, + IncomingType::kyc, + debit_acount, + credit_account, + ) + .await; } } @@ -1005,7 +1047,9 @@ pub enum Status { /// Test standard registration behavior of the registration endpoints pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( server: &Router, - account: &PaytoURI, + debit_acount: &PaytoURI, + credit_account: &PaytoURI, + unknown_account: &PaytoURI, mut in_status: impl FnMut() -> F1, ) { pub use Status::*; @@ -1019,13 +1063,14 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( let key_pair1 = Ed25519KeyPair::generate().unwrap(); let auth_pub1 = EddsaPublicKey::try_from(key_pair1.public_key().as_ref()).unwrap(); let req = json!({ - "credit_amount": amount, + "credit_account": credit_account, "type": "reserve", + "recurrent": false, + "credit_amount": amount, "alg": "EdDSA", "account_pub": auth_pub1, "authorization_pub": auth_pub1, "authorization_sig": eddsa_sign(&key_pair1, auth_pub1.as_ref()), - "recurrent": false }); let register = async |auth_pub: &EddsaPublicKey| { @@ -1034,7 +1079,7 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( .json(json!({ "amount": format!("{currency}:42"), "authorization_pub": auth_pub, - "debit_account": account, + "debit_account": debit_acount, })) .await }; @@ -1090,17 +1135,36 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( // Bad signature server .post("/taler-prepared-transfer/registration") - .json(&json!(req + { - "authorization_sig": eddsa_sign(&key_pair1, b"lol"), - })) + .json(&json!(req + { "authorization_sig": eddsa_sign(&key_pair1, b"lol")})) .await .assert_error(ErrorCode::BANK_BAD_SIGNATURE); + // Unknown account + server + .post("/taler-prepared-transfer/registration") + .json(&json!(req + { "credit_account": unknown_account })) + .await + .assert_error(ErrorCode::BANK_UNKNOWN_CREDITOR); + + // Unsupported payto kind + server + .post("/taler-prepared-transfer/registration") + .json(&json!(req + { "credit_account": UNKNOWN })) + .await + .assert_error(ErrorCode::GENERIC_PAYTO_URI_MALFORMED); + + // Malformed payto + server + .post("/taler-prepared-transfer/registration") + .json(&json!(req + {"credit_account": "http://email@test.com" })) + .await + .assert_error(ErrorCode::GENERIC_JSON_INVALID); + // Reserve pub reuse server .post("/taler-prepared-transfer/registration") .json(&json!(req + { - "account_pub": acc_pub1, + "account_pub": acc_pub1, "authorization_sig": eddsa_sign(&key_pair1, acc_pub1.as_ref()), })) .await @@ -1151,7 +1215,7 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( .json(json!({ "amount": amount, "reserve_pub": acc_pub2, - "debit_account": account, + "debit_account": debit_acount, })) .await .assert_ok(); @@ -1225,7 +1289,7 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( .json(json!({ "amount": amount, "account_pub": acc_pub4, - "debit_account": account, + "debit_account": debit_acount, })) .await .assert_ok_json::<TransferResponse>(); @@ -1317,7 +1381,7 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( .json(json!({ "amount": amount, "reserve_pub": acc_pub5, - "debit_account": account, + "debit_account": debit_acount, })) .await .assert_ok(); @@ -1369,7 +1433,7 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( .json(json!({ "amount": amount, "account_pub": acc_pub5, - "debit_account": account, + "debit_account": debit_acount, })) .await .assert_ok(); diff --git a/taler-cyclos/src/api.rs b/taler-cyclos/src/api.rs @@ -37,19 +37,19 @@ use taler_common::{ }, db::IncomingType, error_code::ErrorCode, - types::{amount::Currency, payto::PaytoURI, timestamp::TalerTimestamp}, + types::{amount::Currency, timestamp::TalerTimestamp}, }; use tokio::sync::watch::Sender; use crate::{ db::{self, AddIncomingResult, Transfer, TxInAdmin}, - payto::FullCyclosPayto, + payto::{CyclosPayto, FullCyclosPayto}, }; pub struct CyclosApi { pub pool: sqlx::PgPool, pub currency: Currency, - pub payto: PaytoURI, + pub payto: FullCyclosPayto, pub in_channel: Sender<i64>, pub taler_in_channel: Sender<i64>, pub out_channel: Sender<i64>, @@ -61,7 +61,7 @@ impl CyclosApi { pub fn start( pool: sqlx::PgPool, root: CompactString, - payto: PaytoURI, + payto: FullCyclosPayto, currency: Currency, ) -> Self { let in_channel = Sender::new(0); @@ -138,7 +138,7 @@ impl WireGateway for CyclosApi { Ok(TransferList { transfers: db::transfer_page(&self.pool, &status, &self.currency, &self.root, &page) .await?, - debit_account: self.payto.clone(), + debit_account: self.payto.as_uri(), }) } @@ -156,7 +156,7 @@ impl WireGateway for CyclosApi { || self.taler_out_channel.subscribe(), ) .await?, - debit_account: self.payto.clone(), + debit_account: self.payto.as_uri(), }) } @@ -170,7 +170,7 @@ impl WireGateway for CyclosApi { || self.taler_in_channel.subscribe(), ) .await?, - credit_account: self.payto.clone(), + credit_account: self.payto.as_uri(), }) } @@ -286,7 +286,7 @@ impl Revenue for CyclosApi { || self.in_channel.subscribe(), ) .await?, - credit_account: self.payto.clone(), + credit_account: self.payto.as_uri(), }) } } @@ -297,6 +297,10 @@ impl PreparedTransfer for CyclosApi { } async fn registration(&self, req: RegistrationRequest) -> ApiResult<RegistrationResponse> { + let creditor = CyclosPayto::try_from(&req.credit_account)?; + if *creditor != *self.payto { + return Err(failure_code(ErrorCode::BANK_UNKNOWN_CREDITOR)); + } match db::transfer_register(&self.pool, &req).await? { db::RegistrationResult::Success => { let simple = TransferSubject::Simple { @@ -367,17 +371,23 @@ mod test { api::CyclosApi, constants::CONFIG_SOURCE, db::{self, TxIn, TxOutKind}, + payto::{FullCyclosPayto, cyclos_payto}, }; - static ACCOUNT: LazyLock<PaytoURI> = - LazyLock::new(|| payto("payto://cyclos/localhost/7762070814178012479?receiver-name=name")); + static PAYTO: LazyLock<FullCyclosPayto> = LazyLock::new(|| { + cyclos_payto("payto://cyclos/localhost/7762070814178012479?receiver-name=Smith") + }); + static EXCHANGE: LazyLock<PaytoURI> = LazyLock::new(|| PAYTO.as_uri()); + static UNKNOWN: LazyLock<PaytoURI> = LazyLock::new(|| { + payto("payto://cyclos/localhost/7762070814178012478?receiver-name=Unknown") + }); async fn setup() -> (Router, PgPool) { let (_, pool) = db_test_setup(CONFIG_SOURCE).await; let api = Arc::new(CyclosApi::start( pool.clone(), CompactString::const_new("localhost"), - ACCOUNT.clone(), + PAYTO.clone(), Currency::TEST, )); let server = Router::new() @@ -409,7 +419,7 @@ mod test { #[tokio::test] async fn transfer() { let (server, _) = setup().await; - transfer_routine(&server, TransferState::pending, &ACCOUNT).await; + transfer_routine(&server, TransferState::pending, &EXCHANGE).await; } static CODE: AtomicI64 = AtomicI64::new(0); @@ -495,7 +505,7 @@ mod test { #[tokio::test] async fn admin_add_incoming() { let (server, _) = setup().await; - admin_add_incoming_routine(&server, &ACCOUNT, true).await; + admin_add_incoming_routine(&server, &EXCHANGE, &EXCHANGE, true).await; } #[tokio::test] @@ -503,7 +513,8 @@ mod test { let (server, db) = &setup().await; in_history_routine( server, - &ACCOUNT, + &EXCHANGE, + &EXCHANGE, true, tasks!({ in_talerable(db).await }), tasks!( @@ -521,7 +532,7 @@ mod test { let (server, db) = &setup().await; revenue_routine( server, - &ACCOUNT, + &EXCHANGE, true, tasks!({ in_malformed(db).await }, { in_talerable(db).await },), tasks!({ out_malformed(db).await }, { out_talerable(db).await }, { @@ -566,6 +577,6 @@ mod test { #[tokio::test] async fn registration() { let (server, pool) = setup().await; - registration_routine(&server, &ACCOUNT, || check_in(&pool)).await; + registration_routine(&server, &EXCHANGE, &EXCHANGE, &UNKNOWN, || check_in(&pool)).await; } } diff --git a/taler-cyclos/src/config.rs b/taler-cyclos/src/config.rs @@ -24,7 +24,7 @@ use taler_api::{ use taler_common::{ config::{Config, ValueErr}, map_config, - types::{amount::Currency, payto::PaytoURI}, + types::amount::Currency, }; use url::Url; @@ -98,7 +98,7 @@ impl HostCfg { /// taler-cyclos httpd config pub struct ServeCfg { - pub payto: PaytoURI, + pub payto: FullCyclosPayto, pub currency: Currency, pub root: CompactString, pub serve: Serve, @@ -119,7 +119,7 @@ impl ServeCfg { let revenue = ApiCfg::parse(cfg.section("cyclos-httpd-revenue-api"))?; Ok(Self { - payto: payto.as_uri(), + payto, currency: main.currency, root: main.root, serve, diff --git a/taler-magnet-bank/src/api.rs b/taler-magnet-bank/src/api.rs @@ -36,7 +36,7 @@ use taler_common::{ }, db::IncomingType, error_code::ErrorCode, - types::{amount::Currency, payto::PaytoURI, timestamp::TalerTimestamp, utils::date_to_utc_ts}, + types::{amount::Currency, timestamp::TalerTimestamp, utils::date_to_utc_ts}, }; use tokio::sync::watch::Sender; @@ -48,7 +48,7 @@ use crate::{ pub struct MagnetApi { pub pool: sqlx::PgPool, - pub payto: PaytoURI, + pub payto: FullHuPayto, pub in_channel: Sender<i64>, pub taler_in_channel: Sender<i64>, pub out_channel: Sender<i64>, @@ -56,7 +56,7 @@ pub struct MagnetApi { } impl MagnetApi { - pub async fn start(pool: sqlx::PgPool, payto: PaytoURI) -> Self { + pub async fn start(pool: sqlx::PgPool, payto: FullHuPayto) -> Self { let in_channel = Sender::new(0); let taler_in_channel = Sender::new(0); let out_channel = Sender::new(0); @@ -127,7 +127,7 @@ impl WireGateway for MagnetApi { ) -> ApiResult<TransferList> { Ok(TransferList { transfers: db::transfer_page(&self.pool, &status, &page).await?, - debit_account: self.payto.clone(), + debit_account: self.payto.as_uri(), }) } @@ -141,7 +141,7 @@ impl WireGateway for MagnetApi { self.taler_out_channel.subscribe() }) .await?, - debit_account: self.payto.clone(), + debit_account: self.payto.as_uri(), }) } @@ -151,7 +151,7 @@ impl WireGateway for MagnetApi { self.taler_in_channel.subscribe() }) .await?, - credit_account: self.payto.clone(), + credit_account: self.payto.as_uri(), }) } @@ -258,7 +258,7 @@ impl Revenue for MagnetApi { self.in_channel.subscribe() }) .await?, - credit_account: self.payto.clone(), + credit_account: self.payto.as_uri(), }) } } @@ -269,6 +269,10 @@ impl PreparedTransfer for MagnetApi { } async fn registration(&self, req: RegistrationRequest) -> ApiResult<RegistrationResponse> { + let creditor = FullHuPayto::try_from(&req.credit_account)?; + if *creditor != *self.payto { + return Err(failure_code(ErrorCode::BANK_UNKNOWN_CREDITOR)); + } match db::transfer_register(&self.pool, &req).await? { db::RegistrationResult::Success => { let simple = TransferSubject::Simple { @@ -345,13 +349,15 @@ mod test { }; static PAYTO: LazyLock<FullHuPayto> = LazyLock::new(|| { - magnet_payto("payto://iban/HU02162000031000164800000000?receiver-name=name") + magnet_payto("payto://iban/HU02162000031000164800000000?receiver-name=Smith") }); - static ACCOUNT: LazyLock<PaytoURI> = LazyLock::new(|| PAYTO.as_uri()); + static EXCHANGE: LazyLock<PaytoURI> = LazyLock::new(|| PAYTO.as_uri()); + static UNKNOWN: LazyLock<PaytoURI> = + LazyLock::new(|| payto("payto://iban/HU60162006491000639900000000?receiver-name=Unknown")); async fn setup() -> (Router, PgPool) { let (_, pool) = db_test_setup(CONFIG_SOURCE).await; - let api = Arc::new(MagnetApi::start(pool.clone(), ACCOUNT.clone()).await); + let api = Arc::new(MagnetApi::start(pool.clone(), PAYTO.clone()).await); let server = Router::new() .wire_gateway(api.clone(), AuthMethod::None) .prepared_transfer(api.clone()) @@ -469,7 +475,7 @@ mod test { #[tokio::test] async fn admin_add_incoming() { let (server, _) = setup().await; - admin_add_incoming_routine(&server, &ACCOUNT, true).await; + admin_add_incoming_routine(&server, &EXCHANGE, &EXCHANGE, true).await; } #[tokio::test] @@ -477,7 +483,8 @@ mod test { let (server, db) = &setup().await; in_history_routine( server, - &ACCOUNT, + &EXCHANGE, + &EXCHANGE, true, tasks!({ in_talerable(db).await }), tasks!( @@ -495,7 +502,7 @@ mod test { let (server, db) = &setup().await; revenue_routine( server, - &ACCOUNT, + &EXCHANGE, true, tasks!({ in_malformed(db).await }, { in_talerable(db).await },), tasks!({ out_malformed(db).await }, { out_talerable(db).await }, { @@ -540,6 +547,6 @@ mod test { #[tokio::test] async fn registration() { let (server, pool) = setup().await; - registration_routine(&server, &ACCOUNT, || check_in(&pool)).await; + registration_routine(&server, &EXCHANGE, &EXCHANGE, &UNKNOWN, || check_in(&pool)).await; } } diff --git a/taler-magnet-bank/src/config.rs b/taler-magnet-bank/src/config.rs @@ -24,7 +24,6 @@ use taler_api::{ use taler_common::{ config::{Config, ValueErr}, map_config, - types::payto::PaytoURI, }; use url::Url; @@ -44,7 +43,7 @@ pub fn parse_account_payto(cfg: &Config) -> Result<FullHuPayto, ValueErr> { /// taler-magnet-bank httpd config pub struct ServeCfg { - pub payto: PaytoURI, + pub payto: FullHuPayto, pub serve: Serve, pub wire_gateway: Option<ApiCfg>, pub revenue: Option<ApiCfg>, @@ -62,7 +61,7 @@ impl ServeCfg { let revenue = ApiCfg::parse(cfg.section("magnet-bank-httpd-revenue-api"))?; Ok(Self { - payto: payto.as_uri(), + payto, serve, wire_gateway, revenue,