commit db6b9cbe3c8276cad9a477419cb1fd4321541a9b
parent 3da77e2db1959363a8251859b31c0c4cde7a1cbd
Author: Antoine A <>
Date: Wed, 11 Mar 2026 12:00:37 +0100
common: support new outgoing subject format
Diffstat:
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