taler-rust

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

commit db6b9cbe3c8276cad9a477419cb1fd4321541a9b
parent 3da77e2db1959363a8251859b31c0c4cde7a1cbd
Author: Antoine A <>
Date:   Wed, 11 Mar 2026 12:00:37 +0100

common: support new outgoing subject format

Diffstat:
Mcommon/taler-api/Cargo.toml | 1+
Mcommon/taler-api/src/db.rs | 17+++++++++++++++++
Mcommon/taler-api/src/subject.rs | 61++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mtaler-cyclos/src/db.rs | 14++++----------
Mtaler-cyclos/tests/api.rs | 7+------
Mtaler-magnet-bank/src/db.rs | 14++++----------
Mtaler-magnet-bank/tests/api.rs | 8++------
7 files changed, 79 insertions(+), 43 deletions(-)

diff --git a/common/taler-api/Cargo.toml b/common/taler-api/Cargo.toml @@ -25,6 +25,7 @@ taler-common.workspace = true sqlx.workspace = true jiff.workspace = true aws-lc-rs.workspace = true +compact_str.workspace = true [dev-dependencies] taler-test-utils.workspace = true diff --git a/common/taler-api/src/db.rs b/common/taler-api/src/db.rs @@ -156,6 +156,10 @@ pub trait TypeHelper { &self, index: I, ) -> sqlx::Result<T>; + fn try_get_opt_parse<I: sqlx::ColumnIndex<Self>, E: Into<BoxDynError>, T: FromStr<Err = E>>( + &self, + index: I, + ) -> sqlx::Result<Option<T>>; fn try_get_timestamp<I: sqlx::ColumnIndex<Self>>(&self, index: I) -> sqlx::Result<Timestamp> { self.try_get_map(index, |micros| { jiff::Timestamp::from_microsecond(micros) @@ -183,6 +187,12 @@ pub trait TypeHelper { fn try_get_payto<I: sqlx::ColumnIndex<Self>>(&self, index: I) -> sqlx::Result<PaytoURI> { self.try_get_parse(index) } + fn try_get_opt_payto<I: sqlx::ColumnIndex<Self>>( + &self, + index: I, + ) -> sqlx::Result<Option<PaytoURI>> { + self.try_get_opt_parse(index) + } fn try_get_iban<I: sqlx::ColumnIndex<Self>>(&self, index: I) -> sqlx::Result<IBAN> { self.try_get_parse(index) } @@ -223,6 +233,13 @@ impl TypeHelper for PgRow { self.try_get_map(index, |s: &str| s.parse()) } + fn try_get_opt_parse<I: sqlx::ColumnIndex<Self>, E: Into<BoxDynError>, T: FromStr<Err = E>>( + &self, + index: I, + ) -> sqlx::Result<Option<T>> { + self.try_get_map(index, |s: Option<&str>| s.map(|s| s.parse()).transpose()) + } + fn try_get_amount<I: sqlx::ColumnIndex<Self>>( &self, index: I, diff --git a/common/taler-api/src/subject.rs b/common/taler-api/src/subject.rs @@ -14,11 +14,16 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -use std::{fmt::Debug, str::FromStr}; +use std::{ + fmt::{Debug, Write as _}, + str::FromStr, +}; +use compact_str::CompactString; use taler_common::{ api_common::{EddsaPublicKey, ShortHashCode}, types::base32::{Base32Error, CROCKFORD_ALPHABET}, + types::url, }; use url::Url; @@ -46,7 +51,22 @@ impl IncomingSubject { } #[derive(Debug, PartialEq, Eq)] -pub struct OutgoingSubject(pub ShortHashCode, pub Url); +pub struct OutgoingSubject { + pub wtid: ShortHashCode, + pub exchange_base_url: Url, + pub metadata: Option<CompactString>, +} + +impl OutgoingSubject { + /// Generate a random outgoing subject for https://exchange.test.com + pub fn rand() -> Self { + Self { + wtid: ShortHashCode::rand(), + exchange_base_url: url("https://exchange.test.com"), + metadata: None, + } + } +} /** Base32 quality by proximity to spec and error probability */ #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] @@ -106,16 +126,35 @@ pub enum OutgoingSubjectErr { Url(#[from] url::ParseError), } -/** - * Extract the wtid and exchange url from an outgoing transfer subject. - */ +/// Parse a talerable outgoing tranfer subject pub fn parse_outgoing(subject: &str) -> Result<OutgoingSubject, OutgoingSubjectErr> { - let (wtid, base_url) = subject - .split_once(" ") - .ok_or(OutgoingSubjectErr::MissingParts)?; - let wtid = wtid.parse()?; - let base_url = base_url.parse()?; - Ok(OutgoingSubject(wtid, base_url)) + let mut parts = subject.split(' '); + let first = parts.next().ok_or(OutgoingSubjectErr::MissingParts)?; + let second = parts.next().ok_or(OutgoingSubjectErr::MissingParts)?; + Ok(if let Some(third) = parts.next() { + OutgoingSubject { + wtid: second.parse()?, + exchange_base_url: third.parse()?, + metadata: Some(first.into()), + } + } else { + OutgoingSubject { + wtid: first.parse()?, + exchange_base_url: second.parse()?, + metadata: None, + } + }) +} + +/// Format an outgoing subject +pub fn fmt_outgoing_subject(wtid: &ShortHashCode, url: &Url, metadata: Option<&str>) -> String { + let mut buf = String::new(); + if let Some(metadata) = metadata { + buf.push_str(metadata); + buf.push(' '); + } + write!(&mut buf, "{wtid} {url}").unwrap(); + buf } /** diff --git a/taler-cyclos/src/db.rs b/taler-cyclos/src/db.rs @@ -324,8 +324,8 @@ pub async fn register_tx_out( .bind(None::<i64>), TxOutKind::Bounce(bounced) => query.bind(None::<&[u8]>).bind(None::<&str>).bind(*bounced), TxOutKind::Talerable(subject) => query - .bind(subject.0.as_ref()) - .bind(subject.1.as_ref()) + .bind(subject.wtid.as_ref()) + .bind(subject.exchange_base_url.as_ref()) .bind(None::<i64>), }; query @@ -1205,14 +1205,8 @@ mod test { // Talerable transaction routine( &mut db, - &TxOutKind::Talerable(OutgoingSubject( - ShortHashCode::rand(), - url("https://exchange.com"), - )), - &TxOutKind::Talerable(OutgoingSubject( - ShortHashCode::rand(), - url("https://exchange.com"), - )), + &TxOutKind::Talerable(OutgoingSubject::rand()), + &TxOutKind::Talerable(OutgoingSubject::rand()), ) .await; diff --git a/taler-cyclos/tests/api.rs b/taler-cyclos/tests/api.rs @@ -21,13 +21,11 @@ use jiff::Timestamp; use sqlx::PgPool; use taler_api::{api::TalerRouter as _, auth::AuthMethod, subject::OutgoingSubject}; use taler_common::{ - api_common::ShortHashCode, api_revenue::RevenueConfig, api_wire::{OutgoingHistory, TransferState, WireConfig}, types::{ amount::{Currency, decimal}, payto::payto, - url, }, }; use taler_cyclos::{ @@ -116,10 +114,7 @@ async fn outgoing_history() { creditor_name: "Name".to_string(), valued_at: Timestamp::now(), }, - &TxOutKind::Talerable(OutgoingSubject( - ShortHashCode::rand(), - url("https://exchange.test"), - )), + &TxOutKind::Talerable(OutgoingSubject::rand()), &Timestamp::now(), ) .await diff --git a/taler-magnet-bank/src/db.rs b/taler-magnet-bank/src/db.rs @@ -312,8 +312,8 @@ pub async fn register_tx_out( .bind(None::<&str>) .bind(*bounced as i64), TxOutKind::Talerable(subject) => query - .bind(subject.0.as_ref()) - .bind(subject.1.as_ref()) + .bind(subject.wtid.as_ref()) + .bind(subject.exchange_base_url.as_ref()) .bind(None::<i64>), }; query @@ -1173,14 +1173,8 @@ mod test { // Talerable transaction routine( &mut db, - &TxOutKind::Talerable(OutgoingSubject( - ShortHashCode::rand(), - url("https://exchange.com"), - )), - &TxOutKind::Talerable(OutgoingSubject( - ShortHashCode::rand(), - url("https://exchange.com"), - )), + &TxOutKind::Talerable(OutgoingSubject::rand()), + &TxOutKind::Talerable(OutgoingSubject::rand()), ) .await; diff --git a/taler-magnet-bank/tests/api.rs b/taler-magnet-bank/tests/api.rs @@ -20,10 +20,9 @@ use jiff::{Timestamp, Zoned}; use sqlx::PgPool; use taler_api::{api::TalerRouter as _, auth::AuthMethod, subject::OutgoingSubject}; use taler_common::{ - api_common::ShortHashCode, api_revenue::RevenueConfig, api_wire::{OutgoingHistory, TransferState, WireConfig}, - types::{amount::amount, payto::payto, url}, + types::{amount::amount, payto::payto}, }; use taler_magnet_bank::{ api::MagnetApi, @@ -109,10 +108,7 @@ async fn outgoing_history() { value_date: now, status: TxStatus::Completed, }, - &TxOutKind::Talerable(OutgoingSubject( - ShortHashCode::rand(), - url("https://exchange.test"), - )), + &TxOutKind::Talerable(OutgoingSubject::rand()), &Timestamp::now(), ) .await