commit 512a4592c5e721191356835fecd0317bb5a543d6
parent f5e3bb2d417428ccba9a23e3556d7ad11a0b3d4a
Author: Antoine A <>
Date: Fri, 4 Sep 2026 12:43:58 +0200
common: improve taler timestamp db compatibility with Kotlin implementation
Diffstat:
14 files changed, 254 insertions(+), 257 deletions(-)
diff --git a/adapters/taler-cyclos/src/api.rs b/adapters/taler-cyclos/src/api.rs
@@ -37,7 +37,7 @@ use taler_common::{
},
db::IncomingType,
error_code::ErrorCode,
- types::{amount::Currency, timestamp::TalerTimestamp},
+ types::{amount::Currency, time::TalerTimestamp},
};
use tokio::sync::watch::Sender;
diff --git a/adapters/taler-magnet-bank/src/api.rs b/adapters/taler-magnet-bank/src/api.rs
@@ -36,7 +36,7 @@ use taler_common::{
},
db::IncomingType,
error_code::ErrorCode,
- types::{amount::Currency, timestamp::TalerTimestamp, utils::date_to_utc_ts},
+ types::{amount::Currency, time::TalerTimestamp, utils::date_to_utc_ts},
};
use tokio::sync::watch::Sender;
diff --git a/adapters/taler-wise/src/api.rs b/adapters/taler-wise/src/api.rs
@@ -42,7 +42,7 @@ use taler_common::{
},
db::IncomingType,
error_code::ErrorCode,
- types::{amount::Currency, payto::PaytoImpl, timestamp::TalerTimestamp},
+ types::{amount::Currency, payto::PaytoImpl, time::TalerTimestamp},
};
use taler_test_utils::Router;
use tokio::sync::watch::Sender;
diff --git a/common/taler-api/src/db.rs b/common/taler-api/src/db.rs
@@ -33,7 +33,7 @@ use taler_common::{
amount::{Amount, Currency, Decimal},
iban::IBAN,
payto::PaytoURI,
- timestamp::TalerTimestamp,
+ time::TalerTimestamp,
utils::date_to_utc_ts,
},
};
diff --git a/common/taler-api/src/test/api.rs b/common/taler-api/src/test/api.rs
@@ -29,7 +29,7 @@ use taler_common::{
},
db::IncomingType,
error_code::ErrorCode::{self},
- types::{amount::Currency, payto::FullIbanPayto, timestamp::TalerTimestamp},
+ types::{amount::Currency, payto::FullIbanPayto, time::TalerTimestamp},
};
use tokio::sync::watch::Sender;
diff --git a/common/taler-common/benches/encoding.rs b/common/taler-common/benches/encoding.rs
@@ -14,14 +14,11 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-use std::hint::black_box;
-use std::str::FromStr;
+use std::{hint::black_box, str::FromStr};
use criterion::{BatchSize, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use taler_common::{
- encoding::base32,
- encoding::base64,
- encoding::hex,
+ encoding::{base32, base64, hex},
types::base32::Base32,
};
diff --git a/common/taler-common/src/api/prepared.rs b/common/taler-common/src/api/prepared.rs
@@ -29,7 +29,7 @@ use crate::{
types::{
amount::{Amount, Currency},
payto::PaytoURI,
- timestamp::TalerTimestamp,
+ time::TalerTimestamp,
},
};
diff --git a/common/taler-common/src/api/revenue.rs b/common/taler-common/src/api/revenue.rs
@@ -22,7 +22,7 @@ use taler_macros::api_config;
use crate::types::{
amount::{Amount, Currency},
payto::PaytoURI,
- timestamp::TalerTimestamp,
+ time::TalerTimestamp,
};
/// <https://docs.taler.net/core/api-bank-revenue.html#tsref-type-RevenueConfig>
diff --git a/common/taler-common/src/api/wire.rs b/common/taler-common/src/api/wire.rs
@@ -27,7 +27,7 @@ use crate::{
types::{
amount::{Amount, Currency},
payto::PaytoURI,
- timestamp::TalerTimestamp,
+ time::TalerTimestamp,
},
};
diff --git a/common/taler-common/src/signature.rs b/common/taler-common/src/signature.rs
@@ -21,7 +21,7 @@ use aws_lc_rs::{
use crate::{
api::{EddsaPublicKey, EddsaSignature},
- types::{amount::Amount, payto::PaytoURI, timestamp::TalerTimestamp},
+ types::{amount::Amount, payto::PaytoURI, time::TalerTimestamp},
};
pub trait Signature<const N: usize> {
diff --git a/common/taler-common/src/types.rs b/common/taler-common/src/types.rs
@@ -19,7 +19,7 @@ pub mod amount;
pub mod base32;
pub mod iban;
pub mod payto;
-pub mod timestamp;
+pub mod time;
pub mod utils;
use url::Url;
diff --git a/common/taler-common/src/types/time.rs b/common/taler-common/src/types/time.rs
@@ -0,0 +1,241 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2024, 2025, 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, ops::Add, str::FromStr, time::Duration};
+
+use jiff::{SignedDuration, Timestamp, civil::Time, tz::TimeZone};
+use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error, ser::SerializeStruct}; // codespell:ignore
+use serde_json::Value;
+
+/// <https://docs.taler.net/core/api-common.html#tsref-type-Timestamp>
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub enum TalerTimestamp {
+ Never,
+ Timestamp(Timestamp),
+}
+
+impl TalerTimestamp {
+ /** Encode timestamp for signature */
+ pub fn signature_bytes(self) -> [u8; 8] {
+ match self {
+ TalerTimestamp::Never => u64::MAX,
+ // Truncate to second and then encode into microseconds as JSON format only support second precision
+ TalerTimestamp::Timestamp(timestamp) => (timestamp.as_second() as u64) * 1000 * 1000,
+ }
+ .to_be_bytes()
+ }
+
+ /// Returns an absolute duration representing the elapsed time from this timestamp until the given other timestamp.
+ pub fn duration_until(self, other: Timestamp) -> SignedDuration {
+ match self {
+ TalerTimestamp::Never => SignedDuration::MAX,
+ TalerTimestamp::Timestamp(tm) => tm.duration_until(other),
+ }
+ }
+}
+
+impl FromStr for TalerTimestamp {
+ type Err = anyhow::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ if s == "never" {
+ return Ok(Self::Never);
+ }
+ let s: i64 = s.parse()?;
+
+ Ok(Self::Timestamp(jiff::Timestamp::from_second(s)?))
+ }
+}
+
+#[derive(Serialize, Deserialize)]
+struct TimestampImpl {
+ t_s: Value,
+}
+
+impl<'de> Deserialize<'de> for TalerTimestamp {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let tmp = TimestampImpl::deserialize(deserializer)?;
+ match tmp.t_s {
+ Value::Number(s) => {
+ if let Some(since_epoch_s) = s.as_u64() {
+ jiff::Timestamp::from_second(since_epoch_s as i64)
+ .map(Self::Timestamp)
+ .map_err(Error::custom)
+ } else {
+ Err(Error::custom("Expected epoch time"))
+ }
+ }
+ Value::String(str) if str == "never" => Ok(Self::Never),
+ _ => Err(Error::custom("Expected epoch time or 'never'")),
+ }
+ }
+}
+
+impl Serialize for TalerTimestamp {
+ fn serialize<S>(&self, se: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let mut se_struct = se.serialize_struct("Timestamp", 1)?;
+ match self {
+ TalerTimestamp::Never => se_struct.serialize_field("t_s", "never")?,
+ TalerTimestamp::Timestamp(timestamp) => {
+ se_struct.serialize_field("t_s", ×tamp.as_second())?
+ }
+ }
+ se_struct.end()
+ }
+}
+
+impl Display for TalerTimestamp {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ TalerTimestamp::Never => f.write_str("never"),
+ TalerTimestamp::Timestamp(timestamp) => timestamp.fmt(f),
+ }
+ }
+}
+
+impl From<jiff::Timestamp> for TalerTimestamp {
+ fn from(time: jiff::Timestamp) -> Self {
+ Self::Timestamp(time)
+ }
+}
+
+impl From<jiff::civil::Date> for TalerTimestamp {
+ fn from(date: jiff::civil::Date) -> Self {
+ date.to_datetime(Time::midnight())
+ .to_zoned(TimeZone::UTC)
+ .unwrap()
+ .timestamp()
+ .into()
+ }
+}
+
+impl Add<jiff::Span> for TalerTimestamp {
+ type Output = Self;
+
+ fn add(self, rhs: jiff::Span) -> Self::Output {
+ match self {
+ TalerTimestamp::Never => TalerTimestamp::Never,
+ TalerTimestamp::Timestamp(timestamp) => TalerTimestamp::Timestamp(timestamp + rhs),
+ }
+ }
+}
+
+impl sqlx::Type<sqlx::Postgres> for TalerTimestamp {
+ fn type_info() -> <sqlx::Postgres as sqlx::Database>::TypeInfo {
+ Option::<i64>::type_info()
+ }
+}
+
+impl<'q> sqlx::Encode<'q, sqlx::Postgres> for TalerTimestamp {
+ fn encode_by_ref(
+ &self,
+ buf: &mut <sqlx::Postgres as sqlx::Database>::ArgumentBuffer<'q>,
+ ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
+ match self {
+ TalerTimestamp::Never => None,
+ TalerTimestamp::Timestamp(timestamp) => Some(timestamp.as_microsecond()),
+ }
+ .encode_by_ref(buf)
+ }
+}
+
+impl<'r> sqlx::Decode<'r, sqlx::Postgres> for TalerTimestamp {
+ fn decode(
+ value: <sqlx::Postgres as sqlx::Database>::ValueRef<'r>,
+ ) -> Result<Self, sqlx::error::BoxDynError> {
+ let micros = Option::<i64>::decode(value)?;
+ Ok(match micros {
+ None | Some(i64::MAX) => Self::Never,
+ Some(micros) => Self::Timestamp(Timestamp::from_microsecond(micros)?),
+ })
+ }
+}
+
+/// <https://docs.taler.net/core/api-common.html#tsref-type-RelativeTime>
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub enum RelativeTime {
+ Forever,
+ Duration(Duration),
+}
+
+impl FromStr for RelativeTime {
+ type Err = anyhow::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ if s == "forever" {
+ return Ok(Self::Forever);
+ }
+ let micros: u64 = s.parse()?;
+
+ Ok(Self::Duration(Duration::from_micros(micros)))
+ }
+}
+
+#[derive(Serialize, Deserialize)]
+struct RelativeTimeImpl {
+ d_us: Value,
+}
+
+impl<'de> Deserialize<'de> for RelativeTime {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let tmp = RelativeTimeImpl::deserialize(deserializer)?;
+ match tmp.d_us {
+ Value::Number(s) => {
+ if let Some(micros) = s.as_u64() {
+ Ok(Self::Duration(Duration::from_micros(micros)))
+ } else {
+ Err(Error::custom("Expected microseconds"))
+ }
+ }
+ Value::String(str) if str == "forever" => Ok(Self::Forever),
+ _ => Err(Error::custom("Expected time or 'forever'")),
+ }
+ }
+}
+
+impl Serialize for RelativeTime {
+ fn serialize<S>(&self, se: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let mut se_struct = se.serialize_struct("RelativeTime", 1)?;
+ match self {
+ RelativeTime::Forever => se_struct.serialize_field("d_us", "forever")?,
+ RelativeTime::Duration(duration) => {
+ se_struct.serialize_field("d_us", &duration.as_micros())?
+ }
+ }
+ se_struct.end()
+ }
+}
+
+impl Display for RelativeTime {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ RelativeTime::Forever => f.write_str("forever"),
+ RelativeTime::Duration(duration) => write!(f, "{duration:?}"),
+ }
+ }
+}
diff --git a/common/taler-common/src/types/timestamp.rs b/common/taler-common/src/types/timestamp.rs
@@ -1,241 +0,0 @@
-/*
- This file is part of TALER
- Copyright (C) 2024, 2025, 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, ops::Add, str::FromStr, time::Duration};
-
-use jiff::{SignedDuration, Timestamp, civil::Time, tz::TimeZone};
-use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error, ser::SerializeStruct}; // codespell:ignore
-use serde_json::Value;
-
-/// <https://docs.taler.net/core/api-common.html#tsref-type-Timestamp>
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
-pub enum TalerTimestamp {
- Never,
- Timestamp(Timestamp),
-}
-
-impl TalerTimestamp {
- /** Encode timestamp for signature */
- pub fn signature_bytes(self) -> [u8; 8] {
- match self {
- TalerTimestamp::Never => u64::MAX,
- // Truncate to second and then encode into microseconds as JSON format only support second precision
- TalerTimestamp::Timestamp(timestamp) => (timestamp.as_second() as u64) * 1000 * 1000,
- }
- .to_be_bytes()
- }
-
- /// Returns an absolute duration representing the elapsed time from this timestamp until the given other timestamp.
- pub fn duration_until(self, other: Timestamp) -> SignedDuration {
- match self {
- TalerTimestamp::Never => SignedDuration::MAX,
- TalerTimestamp::Timestamp(tm) => tm.duration_until(other),
- }
- }
-}
-
-impl FromStr for TalerTimestamp {
- type Err = anyhow::Error;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- if s == "never" {
- return Ok(Self::Never);
- }
- let s: i64 = s.parse()?;
-
- Ok(Self::Timestamp(jiff::Timestamp::from_second(s)?))
- }
-}
-
-#[derive(Serialize, Deserialize)]
-struct TimestampImpl {
- t_s: Value,
-}
-
-impl<'de> Deserialize<'de> for TalerTimestamp {
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
- where
- D: Deserializer<'de>,
- {
- let tmp = TimestampImpl::deserialize(deserializer)?;
- match tmp.t_s {
- Value::Number(s) => {
- if let Some(since_epoch_s) = s.as_u64() {
- jiff::Timestamp::from_second(since_epoch_s as i64)
- .map(Self::Timestamp)
- .map_err(Error::custom)
- } else {
- Err(Error::custom("Expected epoch time"))
- }
- }
- Value::String(str) if str == "never" => Ok(Self::Never),
- _ => Err(Error::custom("Expected epoch time or 'never'")),
- }
- }
-}
-
-impl Serialize for TalerTimestamp {
- fn serialize<S>(&self, se: S) -> Result<S::Ok, S::Error>
- where
- S: Serializer,
- {
- let mut se_struct = se.serialize_struct("Timestamp", 1)?;
- match self {
- TalerTimestamp::Never => se_struct.serialize_field("t_s", "never")?,
- TalerTimestamp::Timestamp(timestamp) => {
- se_struct.serialize_field("t_s", ×tamp.as_second())?
- }
- }
- se_struct.end()
- }
-}
-
-impl Display for TalerTimestamp {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- TalerTimestamp::Never => f.write_str("never"),
- TalerTimestamp::Timestamp(timestamp) => timestamp.fmt(f),
- }
- }
-}
-
-impl From<jiff::Timestamp> for TalerTimestamp {
- fn from(time: jiff::Timestamp) -> Self {
- Self::Timestamp(time)
- }
-}
-
-impl From<jiff::civil::Date> for TalerTimestamp {
- fn from(date: jiff::civil::Date) -> Self {
- date.to_datetime(Time::midnight())
- .to_zoned(TimeZone::UTC)
- .unwrap()
- .timestamp()
- .into()
- }
-}
-
-impl Add<jiff::Span> for TalerTimestamp {
- type Output = Self;
-
- fn add(self, rhs: jiff::Span) -> Self::Output {
- match self {
- TalerTimestamp::Never => TalerTimestamp::Never,
- TalerTimestamp::Timestamp(timestamp) => TalerTimestamp::Timestamp(timestamp + rhs),
- }
- }
-}
-
-impl sqlx::Type<sqlx::Postgres> for TalerTimestamp {
- fn type_info() -> <sqlx::Postgres as sqlx::Database>::TypeInfo {
- Option::<i64>::type_info()
- }
-}
-
-impl<'q> sqlx::Encode<'q, sqlx::Postgres> for TalerTimestamp {
- fn encode_by_ref(
- &self,
- buf: &mut <sqlx::Postgres as sqlx::Database>::ArgumentBuffer<'q>,
- ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
- match self {
- TalerTimestamp::Never => None,
- TalerTimestamp::Timestamp(timestamp) => Some(timestamp.as_microsecond()),
- }
- .encode_by_ref(buf)
- }
-}
-
-impl<'r> sqlx::Decode<'r, sqlx::Postgres> for TalerTimestamp {
- fn decode(
- value: <sqlx::Postgres as sqlx::Database>::ValueRef<'r>,
- ) -> Result<Self, sqlx::error::BoxDynError> {
- let micros = Option::<i64>::decode(value)?;
- Ok(match micros {
- Some(micros) => Self::Timestamp(Timestamp::from_microsecond(micros)?),
- None => Self::Never,
- })
- }
-}
-
-/// <https://docs.taler.net/core/api-common.html#tsref-type-RelativeTime>
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
-pub enum RelativeTime {
- Forever,
- Duration(Duration),
-}
-
-impl FromStr for RelativeTime {
- type Err = anyhow::Error;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- if s == "forever" {
- return Ok(Self::Forever);
- }
- let micros: u64 = s.parse()?;
-
- Ok(Self::Duration(Duration::from_micros(micros)))
- }
-}
-
-#[derive(Serialize, Deserialize)]
-struct RelativeTimeImpl {
- d_us: Value,
-}
-
-impl<'de> Deserialize<'de> for RelativeTime {
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
- where
- D: Deserializer<'de>,
- {
- let tmp = RelativeTimeImpl::deserialize(deserializer)?;
- match tmp.d_us {
- Value::Number(s) => {
- if let Some(micros) = s.as_u64() {
- Ok(Self::Duration(Duration::from_micros(micros)))
- } else {
- Err(Error::custom("Expected microseconds"))
- }
- }
- Value::String(str) if str == "forever" => Ok(Self::Forever),
- _ => Err(Error::custom("Expected epoch time or 'forever'")),
- }
- }
-}
-
-impl Serialize for RelativeTime {
- fn serialize<S>(&self, se: S) -> Result<S::Ok, S::Error>
- where
- S: Serializer,
- {
- let mut se_struct = se.serialize_struct("RelativeTime", 1)?;
- match self {
- RelativeTime::Forever => se_struct.serialize_field("d_us", "forever")?,
- RelativeTime::Duration(duration) => {
- se_struct.serialize_field("d_us", &duration.as_micros())?
- }
- }
- se_struct.end()
- }
-}
-
-impl Display for RelativeTime {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- RelativeTime::Forever => f.write_str("forever"),
- RelativeTime::Duration(duration) => write!(f, "{duration:?}"),
- }
- }
-}
diff --git a/common/taler-test-utils/src/routine.rs b/common/taler-test-utils/src/routine.rs
@@ -47,7 +47,7 @@ use taler_common::{
amount::{Amount, Currency, amount},
base32::Base32,
payto::PaytoURI,
- timestamp::TalerTimestamp,
+ time::TalerTimestamp,
url,
},
};