taler-rust

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

commit a9326b1c867940da4dba77c81ff81ba52630a113
parent 2c6788738c5f4784874e62c91e32dab4550c4626
Author: Antoine A <>
Date:   Tue, 29 Sep 2026 17:55:27 +0200

common: stricter prepared transfer tests and fixes

Diffstat:
Madapters/taler-cyclos/db/cyclos-procedures.sql | 4++--
Madapters/taler-cyclos/src/api.rs | 7++-----
Madapters/taler-magnet-bank/db/magnet-bank-procedures.sql | 4++--
Madapters/taler-magnet-bank/src/api.rs | 7++-----
Madapters/taler-wise/db/wise-procedures.sql | 4++--
Mcommon/taler-api/db/taler-api-procedures.sql | 4++--
Mcommon/taler-test-utils/src/routine.rs | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 96 insertions(+), 18 deletions(-)

diff --git a/adapters/taler-cyclos/db/cyclos-procedures.sql b/adapters/taler-cyclos/db/cyclos-procedures.sql @@ -129,7 +129,7 @@ ELSIF in_type IS NOT NULL THEN UPDATE prepared_in SET tx_in_id = out_tx_row_id WHERE ( - tx_in_id IS NULL AND account_pub = in_metadata AND type='reserve' + tx_in_id IS NULL AND account_pub = in_metadata AND in_type=type AND type='reserve' ) OR authorization_pub = local_authorization_pub; -- Insert new incoming talerable transaction INSERT INTO taler_in ( @@ -439,7 +439,7 @@ WHERE authorization_pub = in_authorization_pub; -- Check idempotency and delay garbage collection IF FOUND AND idempotent THEN UPDATE prepared_in - SET registered_at=in_timestamp + SET registered_at=in_timestamp,authorization_sig=in_authorization_sig WHERE authorization_pub=in_authorization_pub; RETURN; END IF; diff --git a/adapters/taler-cyclos/src/api.rs b/adapters/taler-cyclos/src/api.rs @@ -459,13 +459,10 @@ mod test { .await .assert_ok_json::<RevenueConfig>(); server - .get(format!("/taler-observability/config")) + .get("/taler-observability/config") .await .assert_ok_json::<Config>(); - server - .get(format!("/taler-observability/metrics")) - .await - .assert_ok(); + server.get("/taler-observability/metrics").await.assert_ok(); } #[tokio::test] diff --git a/adapters/taler-magnet-bank/db/magnet-bank-procedures.sql b/adapters/taler-magnet-bank/db/magnet-bank-procedures.sql @@ -125,7 +125,7 @@ ELSIF in_type IS NOT NULL THEN UPDATE prepared_in SET tx_in_id = out_tx_row_id WHERE ( - tx_in_id IS NULL AND account_pub = in_metadata AND type='reserve' + tx_in_id IS NULL AND account_pub = in_metadata AND in_type=type AND type='reserve' ) OR authorization_pub = local_authorization_pub; -- Insert new incoming talerable transaction INSERT INTO taler_in ( @@ -491,7 +491,7 @@ WHERE authorization_pub = in_authorization_pub; -- Check idempotency and delay garbage collection IF FOUND AND idempotent THEN UPDATE prepared_in - SET registered_at=in_timestamp + SET registered_at=in_timestamp,authorization_sig=in_authorization_sig WHERE authorization_pub=in_authorization_pub; RETURN; END IF; diff --git a/adapters/taler-magnet-bank/src/api.rs b/adapters/taler-magnet-bank/src/api.rs @@ -427,13 +427,10 @@ mod test { .await .assert_ok_json::<RevenueConfig>(); server - .get(format!("/taler-observability/config")) + .get("/taler-observability/config") .await .assert_ok_json::<Config>(); - server - .get(format!("/taler-observability/metrics")) - .await - .assert_ok(); + server.get("/taler-observability/metrics").await.assert_ok(); } #[tokio::test] diff --git a/adapters/taler-wise/db/wise-procedures.sql b/adapters/taler-wise/db/wise-procedures.sql @@ -130,7 +130,7 @@ ELSIF in_type IS NOT NULL AND in_debit_payto IS NOT NULL THEN UPDATE prepared_in SET tx_in_id = out_tx_row_id WHERE ( - tx_in_id IS NULL AND account_pub = in_metadata AND type='reserve' + tx_in_id IS NULL AND account_pub = in_metadata AND in_type=type AND type='reserve' ) OR authorization_pub = local_authorization_pub; -- Insert new incoming talerable transaction INSERT INTO taler_in ( @@ -181,7 +181,7 @@ WHERE authorization_pub = in_authorization_pub; -- Check idempotency and delay garbage collection IF FOUND AND idempotent THEN UPDATE prepared_in - SET registered_at=in_timestamp + SET registered_at=in_timestamp,authorization_sig=in_authorization_sig WHERE authorization_pub=in_authorization_pub; RETURN; END IF; diff --git a/common/taler-api/db/taler-api-procedures.sql b/common/taler-api/db/taler-api-procedures.sql @@ -176,7 +176,7 @@ ELSE UPDATE prepared_in SET tx_in_id = out_tx_row_id WHERE ( - tx_in_id IS NULL AND account_pub = in_account_pub AND type='reserve' + tx_in_id IS NULL AND account_pub = in_account_pub AND in_type=type AND type='reserve' ) OR authorization_pub = local_authorization_pub; INSERT INTO taler_in ( tx_in_id, @@ -225,7 +225,7 @@ WHERE authorization_pub = in_authorization_pub; -- Check idempotency and delay garbage collection IF FOUND AND idempotent THEN UPDATE prepared_in - SET registered_at=in_timestamp + SET registered_at=in_timestamp,authorization_sig=in_authorization_sig WHERE authorization_pub=in_authorization_pub; RETURN; END IF; diff --git a/common/taler-test-utils/src/routine.rs b/common/taler-test-utils/src/routine.rs @@ -43,6 +43,7 @@ use taler_common::{ }, db::IncomingType, error_code::ErrorCode, + signature::Signature as _, types::{ amount::{Amount, Currency, amount}, base32::Base32, @@ -1621,6 +1622,86 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( .await .assert_error(ErrorCode::BANK_OLD_TIMESTAMP); + // A KYC payment with a reserve's account key must not consume that + // reserve's prepared registration. The mapped reserve is still valid. + let reserve_pair = Ed25519KeyPair::generate().unwrap(); + let authorization_pub = EddsaPublicKey::try_from(reserve_pair.public_key().as_ref()).unwrap(); + let reserve_pub = EddsaPublicKey::rand(); + prepared_transfer + .post("/registration") + .json( + RegistrationRequest { + account_pub: reserve_pub, + authorization_pub, + ..req.clone() + } + .signed(&reserve_pair), + ) + .await + .assert_ok_json::<RegistrationResponse>(); + wire_gateway + .post("/admin/add-kycauth") + .json(json!({ + "amount": amount, + "account_pub": reserve_pub, + "debit_account": debit_acount, + })) + .await + .assert_ok_json::<TransferResponse>(); + register(&authorization_pub) + .await + .assert_ok_json::<TransferResponse>(); + + // Re-registering the same key with changed signed information must + // replace the signature retained for the next mapped transfer. + let replacement_pair = Ed25519KeyPair::generate().unwrap(); + let replacement_auth_pub = + EddsaPublicKey::try_from(replacement_pair.public_key().as_ref()).unwrap(); + let replacement_reserve_pub = EddsaPublicKey::rand(); + let original_registration = RegistrationRequest { + account_pub: replacement_reserve_pub, + authorization_pub: replacement_auth_pub, + ..req.clone() + } + .signed(&replacement_pair); + prepared_transfer + .post("/registration") + .json(&original_registration) + .await + .assert_ok_json::<RegistrationResponse>(); + let replacement_registration = RegistrationRequest { + credit_amount: taler_common::types::amount::amount(format!("{currency}:43")), + ..original_registration + } + .signed(&replacement_pair); + prepared_transfer + .post("/registration") + .json(&replacement_registration) + .await + .assert_ok_json::<RegistrationResponse>(); + let before_replacement_transfer = + latest_id::<IncomingHistory>(&wire_gateway.suffix("/history/incoming")).await; + register(&replacement_auth_pub) + .await + .assert_ok_json::<TransferResponse>(); + let latest = wire_gateway + .get(format!( + "/history/incoming?limit=1&offset={before_replacement_transfer}" + )) + .await + .assert_ok_json::<IncomingHistory>(); + assert!(matches!( + &latest.incoming_transactions[0], + IncomingBankTransaction::Reserve { + reserve_pub, + authorization_pub: Some(auth_pub), + authorization_sig: Some(auth_sig), + .. + } if *reserve_pub == replacement_reserve_pub + && *auth_pub == replacement_auth_pub + && replacement_registration.verify(auth_pub, auth_sig) + )); + /* ----- API ----- */ let history: Vec<_> = wire_gateway @@ -1662,6 +1743,9 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( (acc_pub5, None), (acc_pub5, None), (acc_pub5, Some(auth_pub2)), + (reserve_pub, None), + (reserve_pub, Some(authorization_pub)), + (replacement_reserve_pub, Some(replacement_auth_pub)), ] ) }