taler-rust

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

commit 8aa7ebfd6b4880a01af57b1dbdfb9a18e86744f3
parent 392dde21040646a83433ddd99c22fae3c2e13d11
Author: Antoine A <>
Date:   Tue,  4 Nov 2025 11:00:37 +0100

magnet-bank: use correct date type

Diffstat:
MCargo.lock | 120++++++++++++++++++++++++++++++++++++++-----------------------------------------
Mcommon/taler-api/src/db.rs | 16++++++++++++++++
Mcommon/taler-common/src/types/timestamp.rs | 2+-
Mcommon/taler-common/src/types/utils.rs | 10++++++++++
Mtaler-magnet-bank/src/api.rs | 6+++---
Mtaler-magnet-bank/src/bin/magnet-bank-harness.rs | 19++++++++++---------
Mtaler-magnet-bank/src/config.rs | 16+++++-----------
Mtaler-magnet-bank/src/db.rs | 77+++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mtaler-magnet-bank/src/dev.rs | 8++++----
Mtaler-magnet-bank/src/magnet.rs | 6+++---
Mtaler-magnet-bank/src/worker.rs | 68++++++++++++++++++++++++++++++++++++++++----------------------------
Mtaler-magnet-bank/tests/api.rs | 8++++++--
12 files changed, 204 insertions(+), 152 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -211,9 +211,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.43" +version = "1.2.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" +checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" dependencies = [ "find-msvc-tools", "shlex", @@ -260,9 +260,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.50" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" dependencies = [ "clap_builder", "clap_derive", @@ -270,9 +270,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.50" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" dependencies = [ "anstream", "anstyle", @@ -964,7 +964,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.3", + "webpki-roots 1.0.4", ] [[package]] @@ -993,9 +993,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -1006,9 +1006,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -1019,11 +1019,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -1034,42 +1033,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -1200,18 +1195,18 @@ checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libdeflate-sys" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "805824325366c44599dfeb62850fe3c7d7b3e3d75f9ab46785bc7dba3676815c" +checksum = "23bd6304ebf75390d8a99b88bdf2a266f62647838140cb64af8e6702f6e3fddc" dependencies = [ "cc", ] [[package]] name = "libdeflater" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b270bcc7e9d6dce967a504a55b1b0444f966aa9184e8605b531bc0492abb30bb" +checksum = "d5d4880e6d634d3d029d65fa016038e788cc728a17b782684726fb34ee140caf" dependencies = [ "libdeflate-sys", ] @@ -1246,9 +1241,9 @@ dependencies = [ [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" @@ -1456,9 +1451,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -1692,7 +1687,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.3", + "webpki-roots 1.0.4", ] [[package]] @@ -1773,9 +1768,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.7" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "ring", "rustls-pki-types", @@ -2337,9 +2332,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -2542,24 +2537,24 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-normalization" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "untrusted" @@ -2737,14 +2732,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.3", + "webpki-roots 1.0.4", ] [[package]] name = "webpki-roots" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" dependencies = [ "rustls-pki-types", ] @@ -3035,17 +3030,16 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -3053,9 +3047,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -3112,9 +3106,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -3123,9 +3117,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -3134,9 +3128,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", diff --git a/common/taler-api/src/db.rs b/common/taler-api/src/db.rs @@ -16,6 +16,10 @@ use std::{str::FromStr, time::Duration}; +use jiff::{ + civil::{Date, Time}, + tz::TimeZone, +}; use sqlx::{ Decode, Error, PgExecutor, PgPool, QueryBuilder, Type, error::BoxDynError, postgres::PgRow, query::Query, @@ -30,6 +34,7 @@ use taler_common::{ iban::IBAN, payto::PaytoURI, timestamp::Timestamp, + utils::date_to_utc_timestamp, }, }; use tokio::sync::watch::Receiver; @@ -122,6 +127,7 @@ pub trait BindHelper { fn bind_amount(self, amount: &Amount) -> Self; fn bind_decimal(self, decimal: &Decimal) -> Self; fn bind_timestamp(self, timestamp: &Timestamp) -> Self; + fn bind_date(self, date: &Date) -> Self; } impl<'q> BindHelper for Query<'q, Postgres, <Postgres as sqlx::Database>::Arguments<'q>> { @@ -136,6 +142,10 @@ impl<'q> BindHelper for Query<'q, Postgres, <Postgres as sqlx::Database>::Argume fn bind_timestamp(self, timestamp: &Timestamp) -> Self { self.bind(timestamp.as_sql_micros()) } + + fn bind_date(self, date: &Date) -> Self { + self.bind_timestamp(&date_to_utc_timestamp(date)) + } } /* ----- Get ----- */ @@ -160,6 +170,12 @@ pub trait TypeHelper { fn try_get_timestamp<I: sqlx::ColumnIndex<Self>>(&self, index: I) -> sqlx::Result<Timestamp> { self.try_get_map(index, Timestamp::from_sql_micros) } + fn try_get_date<I: sqlx::ColumnIndex<Self>>(&self, index: I) -> sqlx::Result<Date> { + let timestamp = self.try_get_timestamp(index)?; + let zoned = timestamp.0.to_zoned(TimeZone::UTC); + assert_eq!(zoned.time(), Time::midnight()); + Ok(zoned.date()) + } fn try_get_u32<I: sqlx::ColumnIndex<Self>>(&self, index: I) -> sqlx::Result<u32> { self.try_get_map(index, |signed: i32| signed.try_into()) } diff --git a/common/taler-common/src/types/timestamp.rs b/common/taler-common/src/types/timestamp.rs @@ -22,7 +22,7 @@ use serde_json::Value; /// <https://docs.taler.net/core/api-common.html#tsref-type-Timestamp> #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct Timestamp(jiff::Timestamp); +pub struct Timestamp(pub jiff::Timestamp); #[derive(Serialize, Deserialize)] struct TimestampImpl { diff --git a/common/taler-common/src/types/utils.rs b/common/taler-common/src/types/utils.rs @@ -16,6 +16,10 @@ use std::{fmt::Debug, ops::Deref}; +use jiff::{civil::Date, tz::TimeZone}; + +use crate::types::timestamp::Timestamp; + #[derive(Clone, PartialEq, Eq)] pub struct InlineStr<const LEN: usize> { /// Len of ascii string in buf @@ -88,3 +92,9 @@ impl<const LEN: usize> Deref for InlineStr<LEN> { unsafe { self.buf.get_unchecked(..self.len as usize) } } } + +/** Convert a date to a UTC timestamp */ +pub fn date_to_utc_timestamp(date: &Date) -> Timestamp { + let zoned = date.to_zoned(TimeZone::UTC).unwrap(); + Timestamp(zoned.timestamp()) +} diff --git a/taler-magnet-bank/src/api.rs b/taler-magnet-bank/src/api.rs @@ -29,7 +29,7 @@ use taler_common::{ TransferState, TransferStatus, }, error_code::ErrorCode, - types::{payto::PaytoURI, timestamp::Timestamp}, + types::{payto::PaytoURI, timestamp::Timestamp, utils::date_to_utc_timestamp}, }; use tokio::sync::watch::Sender; @@ -159,7 +159,7 @@ impl WireGateway for MagnetApi { row_id, valued_at, .. } => Ok(AddIncomingResponse { row_id: safe_u64(row_id), - timestamp: valued_at, + timestamp: date_to_utc_timestamp(&valued_at), }), AddIncomingResult::ReservePubReuse => Err(failure( ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT, @@ -186,7 +186,7 @@ impl WireGateway for MagnetApi { row_id, valued_at, .. } => Ok(AddKycauthResponse { row_id: safe_u64(row_id), - timestamp: valued_at, + timestamp: date_to_utc_timestamp(&valued_at), }), AddIncomingResult::ReservePubReuse => Err(failure( ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT, diff --git a/taler-magnet-bank/src/bin/magnet-bank-harness.rs b/taler-magnet-bank/src/bin/magnet-bank-harness.rs @@ -156,7 +156,7 @@ impl HarnessClient<'_> { ) .await?; self.api - .sign_tx( + .submit_tx( self.signing_key, &from.number, info.code, @@ -330,28 +330,29 @@ fn main() { .assert_transfer_status(transfer_id, TransferState::success, None) .await?; - step("Test transfer failure create-tx"); + step("Test transfer failure init-tx"); harness.transfer(10).await?; - set_failure_logic(FailureLogic::History(vec!["create-tx"])); + set_failure_logic(FailureLogic::History(vec!["init-tx"])); assert!(matches!( worker.run().await, - Err(WorkerError::Injected(InjectedErr("create-tx"))) + Err(WorkerError::Injected(InjectedErr("init-tx"))) )); balance.expect(0).await?; worker.run().await?; balance.expect(-10).await?; + worker.run().await?; - step("Test transfer failure sign-tx"); + step("Test transfer failure submit-tx"); harness.transfer(11).await?; - set_failure_logic(FailureLogic::History(vec!["sign-tx"])); + set_failure_logic(FailureLogic::History(vec!["submit-tx"])); assert!(matches!( worker.run().await, - Err(WorkerError::Injected(InjectedErr("sign-tx"))) + Err(WorkerError::Injected(InjectedErr("submit-tx"))) )); balance.expect(0).await?; worker.run().await?; - // TODO both transactions came through which is VERY WRONG waiting on Magnet Bank on the matter - balance.expect(-22).await?; + balance.expect(-11).await?; + worker.run().await?; Ok(()) }); } diff --git a/taler-magnet-bank/src/config.rs b/taler-magnet-bank/src/config.rs @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -use jiff::tz::TimeZone; +use jiff::civil::Date; use reqwest::Url; use taler_api::{ Serve, @@ -81,8 +81,8 @@ pub struct WorkerCfg { pub consumer: Token, pub keys_path: String, pub account_type: AccountType, - pub ignore_tx_before: Option<jiff::Timestamp>, - pub ignore_bounces_before: Option<jiff::Timestamp>, + pub ignore_tx_before: Option<Date>, + pub ignore_bounces_before: Option<Date>, } impl WorkerCfg { @@ -102,14 +102,8 @@ impl WorkerCfg { secret: sect.str("CONSUMER_SECRET").require()?, }, keys_path: sect.path("KEYS_FILE").require()?, - ignore_tx_before: sect - .date("IGNORE_TRANSACTIONS_BEFORE") - .opt()? - .map(|d| d.to_zoned(TimeZone::system()).unwrap().timestamp()), - ignore_bounces_before: sect - .date("IGNORE_BOUNCES_BEFORE") - .opt()? - .map(|d| d.to_zoned(TimeZone::system()).unwrap().timestamp()), + ignore_tx_before: sect.date("IGNORE_TRANSACTIONS_BEFORE").opt()?, + ignore_bounces_before: sect.date("IGNORE_BOUNCES_BEFORE").opt()?, }) } } diff --git a/taler-magnet-bank/src/db.rs b/taler-magnet-bank/src/db.rs @@ -16,6 +16,7 @@ use std::fmt::Display; +use jiff::{civil::Date, tz::TimeZone}; use sqlx::{PgConnection, PgExecutor, PgPool, QueryBuilder, Row, postgres::PgRow}; use taler_api::{ db::{BindHelper, IncomingType, TypeHelper, history, page}, @@ -32,7 +33,7 @@ use taler_common::{ }; use tokio::sync::watch::{Receiver, Sender}; -use crate::{FullHuPayto, constant::CURRENCY}; +use crate::{FullHuPayto, constant::CURRENCY, magnet::TxStatus}; pub async fn notification_listener( pool: PgPool, @@ -63,7 +64,7 @@ pub struct TxIn { pub amount: Amount, pub subject: String, pub debtor: FullHuPayto, - pub value_date: Timestamp, + pub value_date: Date, } impl Display for TxIn { @@ -90,7 +91,8 @@ pub struct TxOut { pub amount: Amount, pub subject: String, pub creditor: FullHuPayto, - pub value_date: Timestamp, + pub value_date: Date, + pub status: TxStatus, } impl Display for TxOut { @@ -101,10 +103,11 @@ impl Display for TxOut { subject, creditor, value_date, + status, } = self; write!( f, - "{value_date} {amount} {code} ({} {}) '{subject}'", + "{value_date} {amount} {code} ({} {}) {status:?} '{subject}'", creditor.bban(), &creditor.name ) @@ -155,7 +158,7 @@ pub enum AddIncomingResult { Success { new: bool, row_id: u64, - valued_at: Timestamp, + valued_at: Date, }, ReservePubReuse, } @@ -175,7 +178,7 @@ pub async fn register_tx_in_admin( .bind(&tx.subject) .bind(tx.debtor.iban()) .bind(&tx.debtor.name) - .bind_timestamp(now) + .bind_date(&now.0.to_zoned(TimeZone::UTC).date()) .bind(tx.metadata.ty()) .bind(tx.metadata.key()) .try_map(|r: PgRow| { @@ -184,7 +187,7 @@ pub async fn register_tx_in_admin( } else { AddIncomingResult::Success { row_id: r.try_get_u64(1)?, - valued_at: r.try_get_timestamp(2)?, + valued_at: r.try_get_date(2)?, new: r.try_get(3)?, } }) @@ -210,7 +213,7 @@ pub async fn register_tx_in( .bind(&tx.subject) .bind(tx.debtor.iban()) .bind(&tx.debtor.name) - .bind_timestamp(&tx.value_date) + .bind_date(&tx.value_date) .bind(subject.as_ref().map(|it| it.ty())) .bind(subject.as_ref().map(|it| it.key())) .bind_timestamp(now) @@ -220,7 +223,7 @@ pub async fn register_tx_in( } else { AddIncomingResult::Success { row_id: r.try_get_u64(1)?, - valued_at: r.try_get_timestamp(2)?, + valued_at: r.try_get_date(2)?, new: r.try_get(3)?, } }) @@ -253,7 +256,7 @@ pub async fn register_tx_out( .bind(&tx.subject) .bind(tx.creditor.iban()) .bind(&tx.creditor.name) - .bind_timestamp(&tx.value_date); + .bind_date(&tx.value_date); let query = match kind { TxOutKind::Simple => query .bind(None::<&[u8]>) @@ -350,7 +353,7 @@ pub async fn register_bounce_tx_in( .bind(&tx.subject) .bind(tx.debtor.iban()) .bind(&tx.debtor.name) - .bind_timestamp(&tx.value_date) + .bind_date(&tx.value_date) .bind_amount(amount) .bind(reason) .bind_timestamp(now) @@ -676,9 +679,21 @@ pub async fn initiated_submit_permanent_failure<'a>( Ok(()) } +/** Check if an initiated transaction exist for a magnet code */ +pub async fn initiated_exists_for_code<'a>( + db: impl PgExecutor<'a>, + code: u64, +) -> sqlx::Result<Option<u64>> { + sqlx::query("SELECT initiated_id FROM initiated WHERE magnet_code=$1") + .bind(code as i64) + .try_map(|r| Ok(r.try_get::<i64, _>(0)? as u64)) + .fetch_optional(db) + .await +} + #[cfg(test)] mod test { - use jiff::Span; + use jiff::{Span, Zoned}; use sqlx::{PgConnection, PgPool, postgres::PgRow}; use taler_api::{ db::TypeHelper, @@ -700,6 +715,7 @@ mod test { TxIn, TxOut, TxOutKind, make_transfer, register_bounce_tx_in, register_tx_in, register_tx_in_admin, register_tx_out, }, + magnet::TxStatus, magnet_payto, }; @@ -731,7 +747,8 @@ mod test { .await .unwrap(); let now = Timestamp::now_stable(); - let later = now + Span::new().hours(4); + let date = Zoned::now().date(); + let later = date.tomorrow().unwrap(); let tx = TxIn { code: code, amount: amount("EUR:10"), @@ -739,7 +756,7 @@ mod test { debtor: magnet_payto( "payto://iban/HU30162000031000163100000000?receiver-name=name", ), - value_date: now, + value_date: date, }; // Insert assert_eq!( @@ -749,7 +766,7 @@ mod test { AddIncomingResult::Success { new: true, row_id: id, - valued_at: now + valued_at: date } ); // Idempotent @@ -768,7 +785,7 @@ mod test { AddIncomingResult::Success { new: false, row_id: id, - valued_at: now + valued_at: date } ); // Many @@ -856,7 +873,8 @@ mod test { ); let now = Timestamp::now_stable(); - let later = now + Span::new().hours(3); + let later = now + Span::new().hours(2); + let date = Zoned::now().date(); let tx = TxInAdmin { amount: amount("EUR:10"), subject: "subject".to_owned(), @@ -871,7 +889,7 @@ mod test { AddIncomingResult::Success { new: true, row_id: 1, - valued_at: now + valued_at: date } ); // Idempotent @@ -882,7 +900,7 @@ mod test { AddIncomingResult::Success { new: false, row_id: 1, - valued_at: now + valued_at: date } ); // Many @@ -901,7 +919,7 @@ mod test { AddIncomingResult::Success { new: true, row_id: 2, - valued_at: later + valued_at: date } ); @@ -927,7 +945,8 @@ mod test { .await .unwrap(); let now = Timestamp::now_stable(); - let later = now + Span::new().hours(4); + let date = Zoned::now().date(); + let later = date.tomorrow().unwrap(); let tx = TxOut { code, amount: amount("EUR:10"), @@ -935,7 +954,8 @@ mod test { creditor: magnet_payto( "payto://iban/HU30162000031000163100000000?receiver-name=name", ), - value_date: now, + value_date: date, + status: TxStatus::Completed, }; // Insert assert_eq!( @@ -1137,6 +1157,7 @@ mod test { let amount = amount("HUF:10"); let payto = magnet_payto("payto://iban/HU30162000031000163100000000?receiver-name=name"); let now = Timestamp::now_stable(); + let date = Zoned::now().date(); // Empty db assert!(db::pending_batch(&mut db, &now).await.unwrap().is_empty()); @@ -1150,7 +1171,7 @@ mod test { amount: amount.clone(), subject: "subject".to_owned(), debtor: payto.clone(), - value_date: now + value_date: date }, &None, &now @@ -1160,7 +1181,7 @@ mod test { AddIncomingResult::Success { new: true, row_id: 1, - valued_at: now + valued_at: date } ); @@ -1173,7 +1194,7 @@ mod test { amount: amount.clone(), subject: "subject".to_owned(), debtor: payto.clone(), - value_date: now + value_date: date }, &amount, "good reason", @@ -1197,7 +1218,7 @@ mod test { amount: amount.clone(), subject: "subject".to_owned(), debtor: payto.clone(), - value_date: now + value_date: date }, &amount, "good reason", @@ -1222,7 +1243,7 @@ mod test { amount: amount.clone(), subject: "subject".to_owned(), debtor: payto.clone(), - value_date: now + value_date: date }, &amount, "good reason", @@ -1246,7 +1267,7 @@ mod test { amount: amount.clone(), subject: "subject".to_owned(), debtor: payto.clone(), - value_date: now + value_date: date }, &amount, "good reason", diff --git a/taler-magnet-bank/src/dev.rs b/taler-magnet-bank/src/dev.rs @@ -72,7 +72,7 @@ pub async fn dev(cfg: &Config, cmd: DevCmd) -> anyhow::Result<()> { for partner in res.partners { for account in partner.bank_accounts { let payto = account.iban.as_full_payto(&partner.partner.name); - info!("{} {} {}", account.code, account.currency.symbol, payto); + info!(target: "dev", "{} {} {}", account.code, account.currency.symbol, payto); } } } @@ -92,8 +92,8 @@ pub async fn dev(cfg: &Config, cmd: DevCmd) -> anyhow::Result<()> { for item in page.list { let tx = extract_tx_info(item.tx); match tx { - Tx::In(tx_in) => info!("in {tx_in}"), - Tx::Out(tx_out) => info!("out {tx_out}"), + Tx::In(tx_in) => info!(target: "dev", "in {tx_in}"), + Tx::Out(tx_out) => info!(target: "dev", "out {tx_out}"), } } if next.is_none() { @@ -133,7 +133,7 @@ pub async fn dev(cfg: &Config, cmd: DevCmd) -> anyhow::Result<()> { ) .await?; client - .sign_tx( + .submit_tx( &keys.signing_key, &account.number, init.code, diff --git a/taler-magnet-bank/src/magnet.rs b/taler-magnet-bank/src/magnet.rs @@ -269,9 +269,9 @@ pub struct Transaction { #[serde(rename = "tranzakcioAltipus")] pub kind: Option<String>, #[serde(rename = "eredetiErteknap")] - pub tx_date: jiff::Timestamp, + pub tx_date: jiff::civil::Date, #[serde(rename = "erteknap")] - pub value_date: jiff::Timestamp, + pub value_date: jiff::civil::Date, #[serde(rename = "eszamla")] pub counter_account: String, #[serde(rename = "epartner")] @@ -518,7 +518,7 @@ impl ApiClient<'_> { .info) } - pub async fn sign_tx( + pub async fn submit_tx( &self, signing_key: &SigningKey, bban: &str, diff --git a/taler-magnet-bank/src/worker.rs b/taler-magnet-bank/src/worker.rs @@ -33,7 +33,7 @@ use crate::{ db::{self, AddIncomingResult, Initiated, TxIn, TxOut, TxOutKind}, failure_injection::{InjectedErr, fail_point}, magnet::{ - ApiClient, Direction, Transaction, + ApiClient, Direction, Transaction, TxStatus, error::{ApiError, MagnetError}, }, }; @@ -57,8 +57,8 @@ pub struct Worker<'a> { pub account_code: u64, pub key: &'a SigningKey, pub account_type: AccountType, - pub ignore_tx_before: Option<jiff::Timestamp>, - pub ignore_bounces_before: Option<jiff::Timestamp>, + pub ignore_tx_before: Option<Date>, + pub ignore_bounces_before: Option<Date>, } impl Worker<'_> { @@ -167,6 +167,12 @@ impl Worker<'_> { } } Tx::Out(tx_out) => { + if tx_out.status == TxStatus::ToBeRecorded { + self.recover_tx(&tx_out).await?; + continue; + } else if tx_out.status != TxStatus::Completed { + continue; + } match self.account_type { AccountType::Exchange => { // TODO log status (known | recovered | founded) @@ -237,8 +243,6 @@ impl Worker<'_> { } } - // Recover pending transaction - // Send transactions let start = Timestamp::now(); let now = Zoned::now(); @@ -249,29 +253,36 @@ impl Worker<'_> { } for tx in batch { debug!(target: "worker", "send tx {tx}"); - self.create_tx(&tx, &now).await?; + self.init_tx(&tx, &now).await?; } } Ok(()) } /// Try to sign an unsigned initiated transaction - pub async fn recover_tx(&mut self, tx: &Transaction) -> WorkerResult { - // This transaction have not been signed yet, something went wrong - // if in db - // Then try to sign it -> we completed the transaction - // else - // The transaction is unknowned (we failed after creating it and before storing it in the db) - // we delete it - // TODO - self.client.delete_tx(tx.code).await?; - debug!(target: "worker", "out {}: delete uncompleted orphan", tx.code); + pub async fn recover_tx(&mut self, tx: &TxOut) -> WorkerResult { + if let Some(_) = db::initiated_exists_for_code(&mut *self.db, tx.code).await? { + // Known initiated we submit it + assert_eq!(tx.amount.frac, 0); + self.submit_tx( + tx.code, + -(tx.amount.val as f64), + &tx.value_date, + tx.creditor.bban(), + ) + .await?; + } else { + // The transaction is unknown (we failed after creating it and before storing it in the db) + // we delete it + self.client.delete_tx(tx.code).await?; + debug!(target: "worker", "out {}: delete uncompleted orphan", tx.code); + } Ok(()) } /// Create and sign a forint transfer - pub async fn create_tx(&mut self, tx: &Initiated, now: &Zoned) -> WorkerResult { + pub async fn init_tx(&mut self, tx: &Initiated, now: &Zoned) -> WorkerResult { trace!(target: "worker", "create tx {tx}"); assert_eq!(tx.amount.frac, 0); let date = now.date(); @@ -287,8 +298,7 @@ impl Worker<'_> { tx.creditor.bban(), ) .await; - debug!("{res:?}"); - fail_point("create-tx")?; + fail_point("init-tx")?; let info = match res { // Check if succeeded Ok(info) => { @@ -325,27 +335,27 @@ impl Worker<'_> { } Err(e) => return WorkerResult::Err(WorkerError::Api(e)), }; - trace!(target: "worker", "created tx {}", info.code); + trace!(target: "worker", "init tx {}", info.code); // Sign transaction - self.sign_tx(info.code, info.amount, &date, tx.creditor.bban()) + self.submit_tx(info.code, info.amount, &date, tx.creditor.bban()) .await?; Ok(()) } - /** Sign an initiated forint transfer */ - pub async fn sign_tx( + /** Submit an initiated forint transfer */ + pub async fn submit_tx( &mut self, tx_code: u64, amount: f64, date: &Date, creditor: &str, ) -> WorkerResult { - debug!(target: "worker", "sign tx {tx_code}"); - fail_point("sign-tx")?; + debug!(target: "worker", "submit tx {tx_code}"); + fail_point("submit-tx")?; // Sign initiated transaction, on failure we will retry self.client - .sign_tx( + .submit_tx( self.key, self.account_number, tx_code, @@ -375,12 +385,13 @@ pub fn extract_tx_info(tx: Transaction) -> Tx { }; let counter_account = FullHuPayto::new(iban, tx.counter_name); if tx.amount.is_sign_positive() { + assert_eq!(tx.status, TxStatus::Completed, "Can this happen ?"); Tx::In(TxIn { code: tx.code, amount, subject: tx.subject, debtor: counter_account, - value_date: Timestamp::from(tx.value_date), + value_date: tx.value_date, }) } else { Tx::Out(TxOut { @@ -388,7 +399,8 @@ pub fn extract_tx_info(tx: Transaction) -> Tx { amount, subject: tx.subject, creditor: counter_account, - value_date: Timestamp::from(tx.value_date), + value_date: tx.value_date, + status: tx.status, }) } } diff --git a/taler-magnet-bank/tests/api.rs b/taler-magnet-bank/tests/api.rs @@ -16,6 +16,7 @@ use std::sync::Arc; +use jiff::Zoned; use sqlx::PgPool; use taler_api::{api::TalerRouter as _, auth::AuthMethod, subject::OutgoingSubject}; use taler_common::{ @@ -28,6 +29,7 @@ use taler_magnet_bank::{ CONFIG_SOURCE, api::MagnetApi, db::{self, TxOutKind}, + magnet::TxStatus, magnet_payto, }; use taler_test_utils::{ @@ -91,9 +93,10 @@ async fn outgoing_history() { }, |_, i| { let acquire = pool.acquire(); + Timestamp::now().to_string(); async move { let mut conn = acquire.await.unwrap(); - let now = Timestamp::now(); + let now = Zoned::now().date(); db::register_tx_out( &mut *conn, &db::TxOut { @@ -104,12 +107,13 @@ async fn outgoing_history() { "payto://iban/HU30162000031000163100000000?receiver-name=name", ), value_date: now, + status: TxStatus::Completed, }, &TxOutKind::Talerable(OutgoingSubject( ShortHashCode::rand(), url("https://exchange.test"), )), - &now, + &Timestamp::now(), ) .await .unwrap();