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:
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,