commit bf97302f72d421d47874387ad9c5f2fff2cf2270
parent 88509824a6a6ae25be548da609b7f82583cd58ac
Author: Antoine A <>
Date: Thu, 12 Feb 2026 10:42:42 +0100
cyclos: more payto fixes and tests
Diffstat:
7 files changed, 145 insertions(+), 95 deletions(-)
diff --git a/taler-cyclos/src/api.rs b/taler-cyclos/src/api.rs
@@ -36,8 +36,8 @@ use taler_common::{
use tokio::sync::watch::Sender;
use crate::{
- FullCyclosPayto,
db::{self, AddIncomingResult, Transfer, TxInAdmin},
+ payto::FullCyclosPayto,
};
pub struct CyclosApi {
diff --git a/taler-cyclos/src/bin/cyclos-harness.rs b/taler-cyclos/src/bin/cyclos-harness.rs
@@ -37,7 +37,6 @@ use taler_common::{
},
};
use taler_cyclos::{
- FullCyclosPayto,
config::{AccountType, HarnessCfg},
constants::CONFIG_SOURCE,
cyclos_api::{
@@ -46,6 +45,7 @@ use taler_cyclos::{
types::{HistoryItem, OrderBy},
},
db::{self, TransferResult, dbinit},
+ payto::FullCyclosPayto,
setup,
worker::{Worker, WorkerError, WorkerResult, run_worker},
};
diff --git a/taler-cyclos/src/config.rs b/taler-cyclos/src/config.rs
@@ -28,7 +28,7 @@ use taler_common::{
};
use url::Url;
-use crate::{CyclosAccount, CyclosId, FullCyclosPayto};
+use crate::payto::{CyclosAccount, CyclosId, FullCyclosPayto};
#[derive(Debug, Clone, Copy)]
pub enum AccountType {
@@ -65,7 +65,11 @@ impl MainCfg {
pub fn parse(cfg: &Config) -> Result<Self, ValueErr> {
let sect = cfg.section("cyclos");
let url = sect.base_url("CYCLOS_URL").require()?;
- let root = format_compact!("{}{}", url.host_str().unwrap_or_default(), url.path());
+ let root = format_compact!(
+ "{}{}",
+ url.host_str().unwrap_or_default(),
+ url.path().trim_end_matches('/')
+ );
Ok(Self {
currency: sect.parse("currency", "CURRENCY").require()?,
url,
diff --git a/taler-cyclos/src/cyclos_api/types.rs b/taler-cyclos/src/cyclos_api/types.rs
@@ -22,7 +22,7 @@ use serde::{Deserialize, Serialize};
use taler_common::types::{amount::Decimal, payto::FullPayto};
use url::Url;
-use crate::{CyclosAccount, CyclosId, FullCyclosPayto};
+use crate::payto::{CyclosAccount, CyclosId, FullCyclosPayto};
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
diff --git a/taler-cyclos/src/db.rs b/taler-cyclos/src/db.rs
@@ -41,7 +41,10 @@ use taler_common::{
use tokio::sync::watch::{Receiver, Sender};
use url::Url;
-use crate::{CyclosAccount, CyclosId, FullCyclosPayto, config::parse_db_cfg};
+use crate::{
+ config::parse_db_cfg,
+ payto::{CyclosAccount, CyclosId, FullCyclosPayto},
+};
const SCHEMA: &str = "cyclos";
@@ -303,7 +306,7 @@ pub async fn register_tx_out(
) -> sqlx::Result<AddOutgoingResult> {
let query = sqlx::query(
"
- SELECT out_result, out_tx_row_id
+ SELECT out_result, out_tx_row_id
FROM register_tx_out($1, $2, ($3, $4)::taler_amount, $5, $6, $7, $8, $9, $10, $11, $12)
",
)
@@ -454,7 +457,7 @@ pub async fn transfer_page<'a>(
credit_account,
credit_name,
initiated_at
- FROM transfer
+ FROM transfer
JOIN initiated USING (initiated_id)
WHERE
",
@@ -636,8 +639,8 @@ pub async fn transfer_by_id<'a>(
credit_account,
credit_name,
initiated_at
- FROM transfer
- JOIN initiated USING (initiated_id)
+ FROM transfer
+ JOIN initiated USING (initiated_id)
WHERE initiated_id = $1
",
)
@@ -665,7 +668,7 @@ pub async fn pending_batch<'a>(
sqlx::query(
"
SELECT initiated_id, (amount).val, (amount).frac, subject, credit_account, credit_name
- FROM initiated
+ FROM initiated
WHERE tx_id IS NULL
AND status='pending'
AND (last_submitted IS NULL OR last_submitted < $1)
diff --git a/taler-cyclos/src/lib.rs b/taler-cyclos/src/lib.rs
@@ -14,15 +14,11 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-use std::{fmt::Display, num::ParseIntError, ops::Deref, str::FromStr, sync::Arc};
+use std::sync::Arc;
-use compact_str::CompactString;
use sqlx::PgPool;
use taler_api::api::{Router, TalerRouter as _};
-use taler_common::{
- config::Config,
- types::payto::{FullPayto, Payto, PaytoErr, PaytoImpl, PaytoURI, TransferPayto},
-};
+use taler_common::config::Config;
use crate::{api::CyclosApi, config::ServeCfg};
@@ -33,6 +29,7 @@ pub mod cyclos_api;
pub mod db;
pub mod dev;
pub mod notification;
+pub mod payto;
pub mod setup;
pub mod worker;
@@ -49,81 +46,3 @@ pub async fn run_serve(cfg: &Config, pool: PgPool) -> anyhow::Result<()> {
router.serve(cfg.serve, None).await?;
Ok(())
}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct CyclosAccount {
- pub id: CyclosId,
- pub root: CompactString,
-}
-
-#[derive(
- Debug, Clone, Copy, PartialEq, Eq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay,
-)]
-pub struct CyclosId(pub u64);
-
-impl Deref for CyclosId {
- type Target = u64;
-
- fn deref(&self) -> &Self::Target {
- &self.0
- }
-}
-
-impl Display for CyclosId {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- self.0.fmt(f)
- }
-}
-
-#[derive(Debug, thiserror::Error)]
-#[error("malformed cyclos id: {0}")]
-pub struct CyclosIdError(ParseIntError);
-
-impl FromStr for CyclosId {
- type Err = CyclosIdError;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- Ok(Self(u64::from_str(s).map_err(CyclosIdError)?))
- }
-}
-
-const CYCLOS: &str = "cyclos";
-
-#[derive(Debug, thiserror::Error)]
-#[error("missing cyclos root and account id in path")]
-pub struct MissingParts;
-
-impl PaytoImpl for CyclosAccount {
- fn as_payto(&self) -> PaytoURI {
- PaytoURI::from_parts(CYCLOS, format_args!("/{}/{}", self.root, self.id))
- }
-
- fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> {
- let url = raw.as_ref();
- if url.domain() != Some(CYCLOS) {
- return Err(PaytoErr::UnsupportedKind(
- CYCLOS,
- url.domain().unwrap_or_default().to_owned(),
- ));
- }
- let Some((root, id)) = url.path().trim_start_matches("/").rsplit_once('/') else {
- return Err(PaytoErr::custom(MissingParts));
- };
-
- Ok(CyclosAccount {
- id: CyclosId::from_str(id).map_err(PaytoErr::custom)?,
- root: CompactString::new(root),
- })
- }
-}
-
-/// Parse a cyclos payto URI, panic if malformed
-pub fn cyclos_payto(url: impl AsRef<str>) -> FullCyclosPayto {
- url.as_ref().parse().expect("invalid cyclos payto")
-}
-
-// TODO should we check the root url ?
-
-pub type CyclosPayto = Payto<CyclosAccount>;
-pub type FullCyclosPayto = FullPayto<CyclosAccount>;
-pub type TransferCyclosPayto = TransferPayto<CyclosAccount>;
diff --git a/taler-cyclos/src/payto.rs b/taler-cyclos/src/payto.rs
@@ -0,0 +1,124 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2026 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+
+use std::{fmt::Display, num::ParseIntError, ops::Deref, str::FromStr};
+
+use compact_str::CompactString;
+use taler_common::types::payto::{FullPayto, Payto, PaytoErr, PaytoImpl, PaytoURI, TransferPayto};
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct CyclosAccount {
+ pub id: CyclosId,
+ pub root: CompactString,
+}
+
+#[derive(
+ Debug, Clone, Copy, PartialEq, Eq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay,
+)]
+pub struct CyclosId(pub u64);
+
+impl Deref for CyclosId {
+ type Target = u64;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl Display for CyclosId {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+#[error("malformed cyclos id: {0}")]
+pub struct CyclosIdError(ParseIntError);
+
+impl FromStr for CyclosId {
+ type Err = CyclosIdError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ Ok(Self(u64::from_str(s).map_err(CyclosIdError)?))
+ }
+}
+
+const CYCLOS: &str = "cyclos";
+
+#[derive(Debug, thiserror::Error)]
+#[error("missing cyclos root and account id in path")]
+pub struct MissingParts;
+
+impl PaytoImpl for CyclosAccount {
+ fn as_payto(&self) -> PaytoURI {
+ PaytoURI::from_parts(CYCLOS, format_args!("/{}/{}", self.root, self.id))
+ }
+
+ fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> {
+ let url = raw.as_ref();
+ if url.domain() != Some(CYCLOS) {
+ return Err(PaytoErr::UnsupportedKind(
+ CYCLOS,
+ url.domain().unwrap_or_default().to_owned(),
+ ));
+ }
+ let Some((root, id)) = url.path().trim_start_matches("/").rsplit_once('/') else {
+ return Err(PaytoErr::custom(MissingParts));
+ };
+
+ Ok(CyclosAccount {
+ id: CyclosId::from_str(id).map_err(PaytoErr::custom)?,
+ root: CompactString::new(root),
+ })
+ }
+}
+
+/// Parse a cyclos payto URI, panic if malformed
+pub fn cyclos_payto(url: impl AsRef<str>) -> FullCyclosPayto {
+ url.as_ref().parse().expect("invalid cyclos payto")
+}
+
+// TODO should we check the root url ?
+
+pub type CyclosPayto = Payto<CyclosAccount>;
+pub type FullCyclosPayto = FullPayto<CyclosAccount>;
+pub type TransferCyclosPayto = TransferPayto<CyclosAccount>;
+
+#[cfg(test)]
+mod test {
+ use crate::payto::cyclos_payto;
+
+ #[test]
+ pub fn parse() {
+ let simple = "payto://cyclos/demo.cyclos.org/7762070814194619199?receiver-name=John+Smith";
+ let payto = cyclos_payto(simple);
+
+ assert_eq!(*payto.id, 7762070814194619199);
+ assert_eq!(payto.name, "John Smith");
+ assert_eq!(payto.root, "demo.cyclos.org");
+
+ assert_eq!(payto.to_string(), simple);
+
+ let complex = "payto://cyclos/communities.cyclos.org/utrecht/7762070814194619199?receiver-name=John+Smith";
+ let payto = cyclos_payto(complex);
+
+ assert_eq!(*payto.id, 7762070814194619199);
+ assert_eq!(payto.name, "John Smith");
+ assert_eq!(payto.root, "communities.cyclos.org/utrecht");
+
+ assert_eq!(payto.to_string(), complex);
+ }
+}