taler-rust

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

commit bf97302f72d421d47874387ad9c5f2fff2cf2270
parent 88509824a6a6ae25be548da609b7f82583cd58ac
Author: Antoine A <>
Date:   Thu, 12 Feb 2026 10:42:42 +0100

cyclos: more payto fixes and tests

Diffstat:
Mtaler-cyclos/src/api.rs | 2+-
Mtaler-cyclos/src/bin/cyclos-harness.rs | 2+-
Mtaler-cyclos/src/config.rs | 8++++++--
Mtaler-cyclos/src/cyclos_api/types.rs | 2+-
Mtaler-cyclos/src/db.rs | 15+++++++++------
Mtaler-cyclos/src/lib.rs | 87+++----------------------------------------------------------------------------
Ataler-cyclos/src/payto.rs | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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); + } +}