taler-rust

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

routine.rs (60506B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2024-2026 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU Affero General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
     12 
     13   You should have received a copy of the GNU Affero General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 
     17 use std::{
     18     fmt::Debug,
     19     future::Future,
     20     str::FromStr,
     21     sync::LazyLock,
     22     time::{Duration, Instant},
     23 };
     24 
     25 use aws_lc_rs::signature::{Ed25519KeyPair, KeyPair as _};
     26 use axum::{Router, http::StatusCode};
     27 use jiff::{SignedDuration, Timestamp};
     28 use serde::de::DeserializeOwned;
     29 use taler_api::subject::fmt_in_subject;
     30 use taler_common::{
     31     api::{
     32         EddsaPublicKey, EddsaSignature, HashCode, ShortHashCode,
     33         params::PageParams,
     34         prepared::{
     35             PublicKeyAlg, RegistrationRequest, RegistrationResponse, TransferSubject, TransferType,
     36             Unregistration,
     37         },
     38         revenue::RevenueIncomingHistory,
     39         wire::{
     40             IncomingBankTransaction, IncomingHistory, OutgoingHistory, TransferList,
     41             TransferRequest, TransferResponse, TransferState, TransferStatus,
     42         },
     43     },
     44     db::IncomingType,
     45     error_code::ErrorCode,
     46     signature::Signature as _,
     47     types::{
     48         amount::{Amount, Currency, amount},
     49         base32::Base32,
     50         payto::PaytoURI,
     51         time::TalerTimestamp,
     52         url,
     53     },
     54 };
     55 use tokio::time::sleep;
     56 
     57 use crate::{
     58     json,
     59     server::{TestResponse, TestServer as _},
     60 };
     61 
     62 static UNKNOWN: LazyLock<PaytoURI> = LazyLock::new(|| {
     63     PaytoURI::from_str("payto://malformed/unused?receiver-name=Malformed").unwrap()
     64 });
     65 
     66 pub trait Page: DeserializeOwned + Debug {
     67     fn ids(&self) -> Vec<i64>;
     68 }
     69 
     70 impl Page for IncomingHistory {
     71     fn ids(&self) -> Vec<i64> {
     72         self.incoming_transactions
     73             .iter()
     74             .map(|it| match it {
     75                 IncomingBankTransaction::Reserve { row_id, .. }
     76                 | IncomingBankTransaction::Wad { row_id, .. }
     77                 | IncomingBankTransaction::Kyc { row_id, .. } => *row_id as i64,
     78             })
     79             .collect()
     80     }
     81 }
     82 
     83 impl Page for OutgoingHistory {
     84     fn ids(&self) -> Vec<i64> {
     85         self.outgoing_transactions
     86             .iter()
     87             .map(|it| it.row_id as i64)
     88             .collect()
     89     }
     90 }
     91 
     92 impl Page for RevenueIncomingHistory {
     93     fn ids(&self) -> Vec<i64> {
     94         self.incoming_transactions
     95             .iter()
     96             .map(|it| it.row_id as i64)
     97             .collect()
     98     }
     99 }
    100 
    101 impl Page for TransferList {
    102     fn ids(&self) -> Vec<i64> {
    103         self.transfers.iter().map(|it| it.row_id as i64).collect()
    104     }
    105 }
    106 
    107 pub async fn latest_id<T: Page>(router: &Router) -> i64 {
    108     let res = router.get("?limit=-1").await;
    109     if res.status == StatusCode::NO_CONTENT {
    110         0
    111     } else {
    112         res.assert_ids::<T>(1)[0]
    113     }
    114 }
    115 
    116 pub async fn routine_pagination<T: Page>(
    117     server: &Router,
    118     mut register: Tasks<impl AsyncFnMut(usize)>,
    119 ) {
    120     // Check supported
    121     if !server.get("").await.is_implemented() {
    122         return;
    123     }
    124 
    125     let assert_history =
    126         async |args: &str, size: usize| server.get(format!("?{args}")).await.assert_ids::<T>(size);
    127     let before = latest_id::<T>(server).await;
    128 
    129     for i in 0..20 {
    130         (register.lambda)(i).await;
    131     }
    132 
    133     // Use actual row IDs: earlier tests and ignored transactions can leave gaps
    134     let ids = assert_history(&format!("limit=20&offset={before}"), 20).await;
    135     let reverse: Vec<_> = ids.iter().copied().rev().collect();
    136     assert_eq!(assert_history("", 20).await, reverse);
    137     assert_eq!(assert_history("limit=-10", 10).await, reverse[..10]);
    138 
    139     // Cursors are exclusive in both directions, including single-row pages.
    140     for limit in [1, 10] {
    141         assert_eq!(
    142             assert_history(&format!("limit={limit}&offset={}", ids[4]), limit).await,
    143             ids[5..5 + limit]
    144         );
    145         assert_eq!(
    146             assert_history(&format!("limit=-{limit}&offset={}", ids[15]), limit).await,
    147             reverse[5..5 + limit]
    148         );
    149     }
    150 
    151     // Partial pages and exhausted cursors.
    152     assert_eq!(
    153         assert_history(&format!("limit=10&offset={}", ids[18]), 1).await,
    154         ids[19..]
    155     );
    156     assert_history(&format!("limit=1&offset={}", ids[19]), 0).await;
    157     let first = assert_history("limit=1", 1).await[0];
    158     assert_history(&format!("limit=-1&offset={first}"), 0).await;
    159     assert_history("limit=-1&offset=0", 0).await;
    160     assert_history(&format!("limit=1&offset={}", i64::MAX), 0).await;
    161 
    162     for args in [
    163         "limit=0",
    164         "limit=1025",
    165         "limit=-1025",
    166         "limit=-9223372036854775808",
    167         "limit=invalid",
    168         "offset=-1",
    169         "offset=invalid",
    170     ] {
    171         server
    172             .get(format!("?{args}"))
    173             .await
    174             .assert_error(ErrorCode::GENERIC_PARAMETER_MALFORMED);
    175     }
    176 }
    177 
    178 pub async fn assert_time<R>(range: std::ops::Range<u128>, task: impl Future<Output = R>) -> R {
    179     let start = Instant::now();
    180     let limit = Duration::from_millis(range.end.try_into().expect("timing bound exceeds u64"));
    181     let Ok(res) = tokio::time::timeout(limit, task).await else {
    182         panic!(
    183             "Expected to last {range:?} ms, timed out after {} ms",
    184             start.elapsed().as_millis()
    185         );
    186     };
    187     let elapsed = start.elapsed().as_millis();
    188     if !range.contains(&elapsed) {
    189         panic!("Expected to last {range:?} got {elapsed:?}")
    190     }
    191     res
    192 }
    193 
    194 pub async fn routine_history<T: Page>(
    195     server: &Router,
    196     mut register: Tasks<impl AsyncFnMut(usize)>,
    197     mut ignore: Tasks<impl AsyncFnMut(usize)>,
    198 ) {
    199     let assert_history = async |args: String, size: usize| {
    200         server.get(format!("?{args}")).await.assert_ids::<T>(size)
    201     };
    202 
    203     // Check error when no transactions
    204     assert_history("limit=7".to_owned(), 0).await;
    205 
    206     // Interleave visible and ignored transactions to exercise gaps in row IDs.
    207     for idx in 0..register.len.max(ignore.len) {
    208         if idx < register.len {
    209             (register.lambda)(idx).await;
    210         }
    211         if idx < ignore.len {
    212             (ignore.lambda)(idx).await;
    213         }
    214     }
    215     let nb_register = register.len;
    216     let nb_ignore = ignore.len;
    217     let nb_total = nb_register + nb_ignore;
    218 
    219     // Check ignored
    220     assert_history(format!("limit={nb_total}"), nb_register).await;
    221     // Check skip ignored
    222     assert_history(format!("limit={nb_register}"), nb_register).await;
    223 
    224     // Allow CI scheduling and database delays, but require completion before
    225     // the long-poll timeout so broken notification delivery still fails.
    226     // Check no polling when we cannot have more transactions
    227     assert_time(
    228         0..5000,
    229         assert_history(
    230             format!("limit=-{}&timeout_ms=10000", nb_register + 1),
    231             nb_register,
    232         ),
    233     )
    234     .await;
    235     // Check no polling when already find transactions even if less than delta
    236     assert_time(
    237         0..5000,
    238         assert_history(
    239             format!("limit={}&timeout_ms=10000", nb_register + 1),
    240             nb_register,
    241         ),
    242     )
    243     .await;
    244 
    245     // Check polling
    246     let id = latest_id::<T>(server).await;
    247     tokio::join!(
    248         // Check polling succeed
    249         assert_time(
    250             100..5000,
    251             assert_history(format!("limit=2&offset={id}&timeout_ms=10000"), 1)
    252         ),
    253         assert_time(
    254             200..5000,
    255             assert_history(
    256                 format!(
    257                     "limit=1&offset={}&timeout_ms=200",
    258                     id as usize + nb_total * 3
    259                 ),
    260                 0
    261             )
    262         ),
    263         async {
    264             sleep(Duration::from_millis(100)).await;
    265             (register.lambda)(0).await
    266         }
    267     );
    268 
    269     // Test triggers
    270     for i in 0..register.len {
    271         let id = latest_id::<T>(server).await;
    272         tokio::join!(
    273             // Check polling succeed
    274             assert_time(
    275                 100..5000,
    276                 assert_history(format!("limit=7&offset={id}&timeout_ms=10000"), 1)
    277             ),
    278             async {
    279                 sleep(Duration::from_millis(100)).await;
    280                 (register.lambda)(i).await
    281             }
    282         );
    283     }
    284 
    285     // Test doesn't trigger
    286     let id = latest_id::<T>(server).await;
    287     tokio::join!(
    288         // Check polling succeed
    289         assert_time(
    290             200..5000,
    291             assert_history(format!("limit=7&offset={id}&timeout_ms=200"), 0)
    292         ),
    293         async {
    294             sleep(Duration::from_millis(100)).await;
    295             for i in 0..ignore.len {
    296                 (ignore.lambda)(i).await
    297             }
    298         }
    299     );
    300 
    301     routine_pagination::<T>(server, register).await;
    302 }
    303 
    304 impl TestResponse {
    305     #[track_caller]
    306     fn assert_ids<T: Page>(&self, size: usize) -> Vec<i64> {
    307         if size == 0 {
    308             self.assert_no_content();
    309             return vec![];
    310         }
    311         let body = self.assert_ok_json::<T>();
    312         let page = body.ids();
    313         let params = self.query::<PageParams>().check().unwrap();
    314 
    315         // testing the size is like expected
    316         assert_eq!(size, page.len(), "bad page length: {page:?}\n{body:?}");
    317         if params.limit < 0 {
    318             // The cursor row must not be repeated on the next page
    319             assert!(
    320                 params.offset.map(|offset| page[0] < offset).unwrap_or(true),
    321                 "bad page offset: {params:?} {page:?}"
    322             );
    323             // testing that the id decreases.
    324             assert!(
    325                 page.as_slice().is_sorted_by(|a, b| a > b),
    326                 "bad page order: {page:?}"
    327             )
    328         } else {
    329             // The cursor row must not be repeated on the next page.
    330             assert!(
    331                 params.offset.map(|offset| page[0] > offset).unwrap_or(true),
    332                 "bad page offset: {params:?} {page:?}"
    333             );
    334             // testing that the id increases.
    335             assert!(
    336                 page.as_slice().is_sorted_by(|a, b| a < b),
    337                 "bad page order: {page:?}"
    338             )
    339         }
    340         page
    341     }
    342 }
    343 
    344 // Get currency from config
    345 async fn get_currency(server: &Router) -> Currency {
    346     let config = server
    347         .get("/config")
    348         .await
    349         .assert_ok_json::<serde_json::Value>();
    350     let currency = config["currency"].as_str().unwrap();
    351     Currency::from_str(currency).unwrap()
    352 }
    353 
    354 /// Test standard behavior of the transfer endpoints
    355 pub async fn transfer_routine(
    356     wire_gateway: &Router,
    357     default_status: TransferState,
    358     credit_account: &PaytoURI,
    359 ) {
    360     let currency = &get_currency(wire_gateway).await;
    361     let default_amount = amount(format!("{currency}:42"));
    362     let request_uid = HashCode::rand();
    363     let wtid = ShortHashCode::rand();
    364     let valid_req = json!({
    365         "request_uid": request_uid,
    366         "amount": default_amount,
    367         "exchange_base_url": "http://exchange.taler/",
    368         "wtid": wtid,
    369         "credit_account": credit_account,
    370     });
    371 
    372     // Check empty db
    373     {
    374         wire_gateway.get("/transfers").await.assert_no_content();
    375         wire_gateway
    376             .get(format!("/transfers?status={}", default_status.as_ref()))
    377             .await
    378             .assert_no_content();
    379     }
    380 
    381     // TODO check subject formatting
    382 
    383     let routine = async |req: &TransferRequest| {
    384         // Check OK
    385         let first = wire_gateway
    386             .post("/transfer")
    387             .json(req)
    388             .await
    389             .assert_ok_json::<TransferResponse>();
    390         // Check idempotent
    391         let second = wire_gateway
    392             .post("/transfer")
    393             .json(req)
    394             .await
    395             .assert_ok_json::<TransferResponse>();
    396         assert_eq!(first.row_id, second.row_id);
    397         assert_eq!(first.timestamp, second.timestamp);
    398 
    399         // Check by id
    400         let tx = wire_gateway
    401             .get(format!("/transfers/{}", first.row_id))
    402             .await
    403             .assert_ok_json::<TransferStatus>();
    404         assert_eq!(default_status, tx.status);
    405         assert_eq!(default_amount, tx.amount);
    406         assert_eq!("http://exchange.taler/", tx.exchange_base_url);
    407         assert_eq!(req.wtid, tx.wtid);
    408         assert_eq!(first.timestamp, tx.timestamp);
    409         assert_eq!(req.metadata, tx.metadata);
    410         assert_eq!(credit_account, &tx.credit_account);
    411 
    412         // Check page
    413         let list = wire_gateway
    414             .get("/transfers?limit=-1")
    415             .await
    416             .assert_ok_json::<TransferList>();
    417         let [tx] = list.transfers.as_slice() else {
    418             panic!("expected one transfer, got {list:?}");
    419         };
    420         assert_eq!(first.row_id, tx.row_id);
    421         assert_eq!(default_status, tx.status);
    422         assert_eq!(default_amount, tx.amount);
    423         assert_eq!(first.timestamp, tx.timestamp);
    424         assert_eq!(credit_account, &tx.credit_account);
    425     };
    426 
    427     let req = TransferRequest {
    428         request_uid,
    429         amount: default_amount,
    430         exchange_base_url: url("http://exchange.taler/"),
    431         metadata: None,
    432         wtid,
    433         credit_account: credit_account.clone(),
    434     };
    435     // Simple
    436     routine(&req).await;
    437     // With metadata
    438     wire_gateway
    439         .post("/transfer")
    440         .json(&TransferRequest {
    441             metadata: Some("changed".into()),
    442             ..req.clone()
    443         })
    444         .await
    445         .assert_error(ErrorCode::BANK_TRANSFER_REQUEST_UID_REUSED);
    446 
    447     let metadata_req = TransferRequest {
    448         request_uid: HashCode::rand(),
    449         wtid: ShortHashCode::rand(),
    450         metadata: Some("test:metadata".into()),
    451         ..req.clone()
    452     };
    453     routine(&metadata_req).await;
    454     wire_gateway
    455         .post("/transfer")
    456         .json(&TransferRequest {
    457             metadata: None,
    458             ..metadata_req
    459         })
    460         .await
    461         .assert_error(ErrorCode::BANK_TRANSFER_REQUEST_UID_REUSED);
    462 
    463     // Check create transfer errors
    464     {
    465         // Changing any of these fields conflicts with the original request UID
    466         for conflicting_req in [
    467             TransferRequest {
    468                 wtid: ShortHashCode::rand(),
    469                 ..req.clone()
    470             },
    471             TransferRequest {
    472                 amount: amount(format!("{currency}:43")),
    473                 ..req.clone()
    474             },
    475             TransferRequest {
    476                 exchange_base_url: url("https://other-exchange.taler/"),
    477                 ..req.clone()
    478             },
    479         ] {
    480             wire_gateway
    481                 .post("/transfer")
    482                 .json(conflicting_req)
    483                 .await
    484                 .assert_error(ErrorCode::BANK_TRANSFER_REQUEST_UID_REUSED);
    485         }
    486         // Check wtid reuse
    487         wire_gateway
    488             .post("/transfer")
    489             .json(json!(valid_req + {
    490                 "request_uid": HashCode::rand(),
    491             }))
    492             .await
    493             .assert_error(ErrorCode::BANK_TRANSFER_WTID_REUSED);
    494 
    495         // Check currency mismatch
    496         wire_gateway
    497             .post("/transfer")
    498             .json(json!(valid_req + {
    499                 "amount": "BAD:42"
    500             }))
    501             .await
    502             .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH);
    503 
    504         // Invalid Base32
    505         wire_gateway
    506             .post("/transfer")
    507             .json(json!(valid_req + {
    508                 "wtid": "I love chocolate"
    509             }))
    510             .await
    511             .assert_error(ErrorCode::GENERIC_JSON_INVALID);
    512         wire_gateway
    513             .post("/transfer")
    514             .json(json!(valid_req + {
    515                 "wtid": Base32::<31>::rand()
    516             }))
    517             .await
    518             .assert_error(ErrorCode::GENERIC_JSON_INVALID);
    519         wire_gateway
    520             .post("/transfer")
    521             .json(json!(valid_req + {
    522                 "request_uid": "I love chocolate"
    523             }))
    524             .await
    525             .assert_error(ErrorCode::GENERIC_JSON_INVALID);
    526         wire_gateway
    527             .post("/transfer")
    528             .json(json!(valid_req + {
    529                 "request_uid": Base32::<65>::rand()
    530             }))
    531             .await
    532             .assert_error(ErrorCode::GENERIC_JSON_INVALID);
    533 
    534         // Missing receiver-name
    535         let res = wire_gateway
    536             .post("/transfer")
    537             .json(json!(valid_req + {
    538                 "credit_account": credit_account.as_ref().as_str().split('?').next().unwrap()
    539             }))
    540             .await;
    541         if !res.status.is_success() {
    542             res.assert_error(ErrorCode::GENERIC_PAYTO_URI_MALFORMED);
    543         }
    544 
    545         // Unsupported payto kind
    546         wire_gateway
    547             .post("/transfer")
    548             .json(json!(valid_req + { "credit_account": *UNKNOWN }))
    549             .await
    550             .assert_error(ErrorCode::GENERIC_PAYTO_URI_MALFORMED);
    551         // Malformed payto
    552         wire_gateway
    553             .post("/transfer")
    554             .json(json!(valid_req + { "credit_account": "http://email@test.com" }))
    555             .await
    556             .assert_error(ErrorCode::GENERIC_JSON_INVALID);
    557 
    558         // Bad base URL
    559         for base_url in [
    560             "not-a-url",
    561             "file://not.http.com/",
    562             "no.transport.com/",
    563             "https://not.a/base/url",
    564         ] {
    565             wire_gateway
    566                 .post("/transfer")
    567                 .json(&json!(valid_req + { "exchange_base_url": base_url }))
    568                 .await
    569                 .assert_error(ErrorCode::GENERIC_JSON_INVALID);
    570         }
    571 
    572         // Malformed metadata
    573         for metadata in ["", "bad_id", "bad id", "bad@id.com", &"A".repeat(41)] {
    574             wire_gateway
    575                 .post("/transfer")
    576                 .json(&json!(valid_req + { "metadata": metadata }))
    577                 .await
    578                 .assert_error(ErrorCode::GENERIC_JSON_INVALID);
    579         }
    580     }
    581 
    582     // Check transfer by id errors
    583     {
    584         // Check unknown transaction
    585         wire_gateway
    586             .get("/transfers/42")
    587             .await
    588             .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND);
    589     }
    590 
    591     // Check transfer page
    592     {
    593         // Valid metadata boundaries and all supported punctuation.
    594         for metadata in ["A", &"A".repeat(40), "test-1.2:metadata", "0"] {
    595             wire_gateway
    596                 .post("/transfer")
    597                 .json(&json!(valid_req + {
    598                     "request_uid": HashCode::rand(),
    599                     "wtid": ShortHashCode::rand(),
    600                     "metadata": metadata
    601                 }))
    602                 .await
    603                 .assert_ok_json::<TransferResponse>();
    604         }
    605         for status in [
    606             TransferState::pending,
    607             TransferState::transient_failure,
    608             TransferState::permanent_failure,
    609             TransferState::success,
    610         ] {
    611             if status != default_status {
    612                 wire_gateway
    613                     .get(format!("/transfers?status={}", status.as_ref()))
    614                     .await
    615                     .assert_no_content();
    616             }
    617         }
    618         wire_gateway
    619             .get("/transfers?status=invalid")
    620             .await
    621             .assert_error(ErrorCode::GENERIC_PARAMETER_MALFORMED);
    622         {
    623             let list = wire_gateway
    624                 .get("/transfers")
    625                 .await
    626                 .assert_ok_json::<TransferList>();
    627             assert_eq!(list.transfers.len(), 6);
    628             assert_eq!(
    629                 list,
    630                 wire_gateway
    631                     .get(format!("/transfers?status={}", default_status.as_ref()))
    632                     .await
    633                     .assert_ok_json::<TransferList>()
    634             )
    635         }
    636 
    637         // Pagination test
    638         routine_pagination::<TransferList>(
    639             &wire_gateway.suffix("/transfers"),
    640             crate::tasks!({
    641                 wire_gateway
    642                     .post("/transfer")
    643                     .json(json!({
    644                         "request_uid": HashCode::rand(),
    645                         "amount": amount(format!("{currency}:0.1")),
    646                         "exchange_base_url": url("http://exchange.taler"),
    647                         "wtid": ShortHashCode::rand(),
    648                         "credit_account": credit_account,
    649                     }))
    650                     .await
    651                     .assert_ok_json::<TransferResponse>();
    652             }),
    653         )
    654         .await;
    655     }
    656 }
    657 
    658 async fn add_incoming_routine(
    659     wire_gateway: &Router,
    660     prepared_transfer: &Router,
    661     currency: &Currency,
    662     kind: IncomingType,
    663     debit_acount: &PaytoURI,
    664     credit_account: &PaytoURI,
    665 ) {
    666     let (path, key) = match kind {
    667         IncomingType::reserve => ("/admin/add-incoming", "reserve_pub"),
    668         IncomingType::kyc => ("/admin/add-kycauth", "account_pub"),
    669         IncomingType::map => ("/admin/add-mapped", "authorization_pub"),
    670     };
    671     let key_pair = Ed25519KeyPair::generate().unwrap();
    672     let pub_key = EddsaPublicKey::try_from(key_pair.public_key().as_ref()).unwrap();
    673     // Valid
    674     let req = RegistrationRequest {
    675         credit_account: credit_account.clone(),
    676         r#type: TransferType::reserve,
    677         recurrent: false,
    678         credit_amount: Amount::new(currency, 44, 0),
    679         alg: PublicKeyAlg::EdDSA,
    680         account_pub: pub_key,
    681         authorization_pub: pub_key,
    682         authorization_sig: EddsaSignature::ZEROED,
    683     }
    684     .signed(&key_pair);
    685 
    686     prepared_transfer
    687         .post("/registration")
    688         .json(&req)
    689         .await
    690         .assert_ok_json::<RegistrationResponse>();
    691     let valid_req = json!({
    692         "amount": format!("{currency}:44"),
    693         key: pub_key,
    694         "debit_account": debit_acount,
    695     });
    696 
    697     // Check OK
    698     wire_gateway.post(path).json(&valid_req).await.assert_ok();
    699 
    700     match kind {
    701         IncomingType::reserve => {
    702             // Trigger conflict due to reused reserve_pub
    703             wire_gateway
    704                 .post(path)
    705                 .json(&json!(valid_req + {
    706                     "amount": format!("{currency}:44.1"),
    707                 }))
    708                 .await
    709                 .assert_error(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT)
    710         }
    711         IncomingType::kyc => {
    712             // Non conflict on reuse
    713             wire_gateway.post(path).json(&valid_req).await.assert_ok();
    714         }
    715         IncomingType::map => {
    716             // Trigger conflict due to reused authorization_pub
    717             wire_gateway
    718                 .post(path)
    719                 .json(&valid_req)
    720                 .await
    721                 .assert_error(ErrorCode::BANK_TRANSFER_MAPPING_REUSED);
    722             // Trigger conflict due to unknown authorization_pub
    723             wire_gateway
    724                 .post(path)
    725                 .json(&json!(valid_req + {
    726                    key: EddsaPublicKey::rand()
    727                 }))
    728                 .await
    729                 .assert_error(ErrorCode::BANK_TRANSFER_MAPPING_UNKNOWN);
    730         }
    731     }
    732 
    733     // Currency mismatch
    734     wire_gateway
    735         .post(path)
    736         .json(&json!(valid_req + { "amount": "BAD:33" }))
    737         .await
    738         .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH);
    739 
    740     // Bad BASE32 reserve_pub
    741     wire_gateway
    742         .post(path)
    743         .json(json!(valid_req + { key: "I love chocolate" }))
    744         .await
    745         .assert_error(ErrorCode::GENERIC_JSON_INVALID);
    746     wire_gateway
    747         .post(path)
    748         .json(json!(valid_req + { key: Base32::<31>::rand() }))
    749         .await
    750         .assert_error(ErrorCode::GENERIC_JSON_INVALID);
    751 
    752     // Unsupported payto kind
    753     wire_gateway
    754         .post(path)
    755         .json(json!(valid_req + { "debit_account": *UNKNOWN }))
    756         .await
    757         .assert_error(ErrorCode::GENERIC_PAYTO_URI_MALFORMED);
    758 
    759     // Malformed payto
    760     wire_gateway
    761         .post(path)
    762         .json(json!(valid_req + { "debit_account": "http://email@test.com" }))
    763         .await
    764         .assert_error(ErrorCode::GENERIC_JSON_INVALID);
    765 }
    766 
    767 pub struct Tasks<F: AsyncFnMut(usize)> {
    768     pub len: usize,
    769     pub lambda: F,
    770 }
    771 
    772 #[macro_export]
    773 macro_rules! tasks {
    774     // Create new
    775     ( $( $(if $cond:expr =>)? $body:block ),* $(,)? ) => {
    776         $crate::tasks!(@build 0usize;
    777             $( $(if $cond =>)? $body ),*
    778         )
    779     };
    780 
    781     // Append to existing
    782     ( $existing:expr ; $( $(if $cond:expr =>)? $body:block ),* $(,)? ) => {{
    783         let mut existing = $existing;
    784         let mut extra = $crate::tasks![
    785             $( $(if $cond =>)? $body ),*
    786         ];
    787 
    788         $crate::routine::Tasks {
    789             len: existing.len + extra.len,
    790             lambda: async move |i: usize| {
    791                 if existing.len == 0 && extra.len == 0 {
    792                     return;
    793                 }
    794 
    795                 let i = i % (existing.len + extra.len);
    796 
    797                 if i < existing.len {
    798                     (existing.lambda)(i).await;
    799                 } else {
    800                     (extra.lambda)(i - existing.len).await;
    801                 }
    802             }
    803         }
    804     }};
    805 
    806     // Internal builder
    807     (@build $idx:expr; $( $(if $cond:expr =>)? $body:block ),* ) => {{
    808         let conditions = [ $( true $( && $cond )? ),* ];
    809         let len = conditions.iter().filter(|&&x| x).count();
    810 
    811         $crate::routine::Tasks {
    812             len,
    813             lambda: async move |i: usize| {
    814                 if len == 0 {
    815                     return;
    816                 }
    817 
    818                 let i = i % len;
    819                 let mut current = 0usize;
    820 
    821                 $crate::tasks!(
    822                     @dispatch i, current, conditions, 0usize;
    823                     $( $(if $cond =>)? $body ),*
    824                 );
    825 
    826                 let _ = (i, &mut current); // suppress lints
    827 
    828                 unreachable!()
    829             }
    830         }
    831     }};
    832 
    833     // Recursive dispatcher
    834     (@dispatch $i:expr, $current:ident, $conditions:ident, $idx:expr;) => {};
    835 
    836     (@dispatch
    837         $i:expr,
    838         $current:ident,
    839         $conditions:ident,
    840         $idx:expr;
    841         $(if $cond:expr =>)? $body:block
    842         $(, $($rest:tt)*)?
    843     ) => {{
    844         if $conditions[$idx] {
    845             if $i == $current {
    846                 (async $body).await;
    847                 return;
    848             }
    849             $current += 1;
    850         }
    851 
    852         $crate::tasks!(
    853             @dispatch
    854             $i,
    855             $current,
    856             $conditions,
    857             $idx + 1usize;
    858             $($($rest)*)?
    859         );
    860     }};
    861 }
    862 
    863 /// Test standard behavior of the revenue endpoints
    864 pub async fn revenue_routine(
    865     wire_gateway: &Router,
    866     revenue_api: &Router,
    867     debit_acount: &PaytoURI,
    868     register: Tasks<impl AsyncFnMut(usize)>,
    869     ignore: Tasks<impl AsyncFnMut(usize)>,
    870 ) {
    871     let currency = &get_currency(revenue_api).await;
    872     routine_history::<RevenueIncomingHistory>(
    873         &revenue_api.suffix("/history"),
    874         tasks!(register;
    875             {
    876                 wire_gateway
    877                         .post("/admin/add-incoming")
    878                         .json(json!({
    879                             "amount": format!("{currency}:1"),
    880                             "reserve_pub": EddsaPublicKey::rand(),
    881                             "debit_account": debit_acount,
    882                         }))
    883                         .await
    884                         .assert_ok_json::<TransferResponse>();
    885             },
    886             {
    887                 wire_gateway
    888                         .post("/admin/add-kycauth")
    889                         .json(json!({
    890                             "amount": format!("{currency}:2"),
    891                             "account_pub": EddsaPublicKey::rand(),
    892                             "debit_account": debit_acount,
    893                         }))
    894                         .await
    895                         .assert_ok_json::<TransferResponse>();
    896             }
    897         ),
    898         ignore,
    899     )
    900     .await;
    901 }
    902 
    903 /// Test standard behavior of the outgoing history endpoint
    904 pub async fn out_history_routine(
    905     wire_gateway: &Router,
    906     register: Tasks<impl AsyncFnMut(usize)>,
    907     ignore: Tasks<impl AsyncFnMut(usize)>,
    908 ) {
    909     routine_history::<OutgoingHistory>(&wire_gateway.suffix("/history/outgoing"), register, ignore)
    910         .await;
    911 }
    912 
    913 /// Test standard behavior of the incoming history endpoint
    914 pub async fn in_history_routine(
    915     wire_gateway: &Router,
    916     prepared_transfer: &Router,
    917     debit_account: &PaytoURI,
    918     credit_account: &PaytoURI,
    919     register: Tasks<impl AsyncFnMut(usize)>,
    920     ignored: Tasks<impl AsyncFnMut(usize)>,
    921 ) {
    922     let currency = &get_currency(wire_gateway).await;
    923     let mut key = Ed25519KeyPair::generate().unwrap();
    924 
    925     routine_history::<IncomingHistory>(
    926         &wire_gateway.suffix("/history/incoming"),
    927         tasks!(register;
    928             {
    929                 wire_gateway
    930                     .post("/admin/add-incoming")
    931                     .json(json!({
    932                         "amount": format!("{currency}:1"),
    933                         "reserve_pub": EddsaPublicKey::rand(),
    934                         "debit_account": debit_account,
    935                     }))
    936                     .await
    937                     .assert_ok_json::<TransferResponse>();
    938             },
    939             {
    940                 key = Ed25519KeyPair::generate().unwrap();
    941                 let auth_pub = EddsaPublicKey::try_from(key.public_key().as_ref()).unwrap();
    942                 let reserve_pub = EddsaPublicKey::rand();
    943                 let amount = Amount::new(currency, 2, 0);
    944                 prepared_transfer
    945                     .post("/registration")
    946                     .json(
    947                         RegistrationRequest {
    948                             credit_account: credit_account.clone(),
    949                             r#type: TransferType::reserve,
    950                             recurrent: true,
    951                             credit_amount: amount,
    952                             alg: PublicKeyAlg::EdDSA,
    953                             account_pub: reserve_pub,
    954                             authorization_pub: auth_pub,
    955                             authorization_sig: EddsaSignature::ZEROED,
    956                         }
    957                         .signed(&key)
    958                     )
    959                     .await
    960                     .assert_ok_json::<RegistrationResponse>();
    961                 wire_gateway
    962                     .post("/admin/add-mapped")
    963                     .json(json!({
    964                         "amount": amount,
    965                         "authorization_pub": auth_pub,
    966                         "debit_account": debit_account,
    967                     }))
    968                     .await
    969                     .assert_ok_json::<TransferResponse>();
    970                 wire_gateway
    971                     .post("/admin/add-mapped")
    972                     .json(json!({
    973                         "amount": amount,
    974                         "authorization_pub": auth_pub,
    975                         "debit_account": debit_account,
    976                     }))
    977                     .await
    978                     .assert_ok_json::<TransferResponse>();
    979             },
    980             {
    981                 let auth_pub = EddsaPublicKey::try_from(key.public_key().as_ref()).unwrap();
    982                 let reserve_pub = EddsaPublicKey::rand();
    983                 prepared_transfer
    984                     .post("/registration")
    985                     .json(
    986                         RegistrationRequest {
    987                             credit_account: credit_account.clone(),
    988                             r#type: TransferType::reserve,
    989                             recurrent: true,
    990                             credit_amount: Amount::new(currency, 3, 0),
    991                             alg: PublicKeyAlg::EdDSA,
    992                             account_pub: reserve_pub,
    993                             authorization_pub: auth_pub,
    994                             authorization_sig: EddsaSignature::ZEROED,
    995                         }
    996                         .signed(&key)
    997                     )
    998                     .await
    999                     .assert_ok_json::<RegistrationResponse>();
   1000             },
   1001             {
   1002                 wire_gateway
   1003                     .post("/admin/add-kycauth")
   1004                     .json(json!({
   1005                         "amount": format!("{currency}:4"),
   1006                         "account_pub": EddsaPublicKey::rand(),
   1007                         "debit_account": debit_account,
   1008                     }))
   1009                     .await
   1010                     .assert_ok_json::<TransferResponse>();
   1011             },
   1012             {
   1013                 key = Ed25519KeyPair::generate().unwrap();
   1014                 let auth_pub = EddsaPublicKey::try_from(key.public_key().as_ref()).unwrap();
   1015                 let account_pub = EddsaPublicKey::rand();
   1016                 let amount = Amount::new(currency, 5, 0);
   1017                 prepared_transfer
   1018                     .post("/registration")
   1019                     .json(
   1020                         RegistrationRequest {
   1021                             credit_account: credit_account.clone(),
   1022                             r#type: TransferType::kyc,
   1023                             recurrent: true,
   1024                             credit_amount: amount,
   1025                             alg: PublicKeyAlg::EdDSA,
   1026                             account_pub,
   1027                             authorization_pub: auth_pub,
   1028                             authorization_sig: EddsaSignature::ZEROED,
   1029                         }
   1030                         .signed(&key)
   1031                     )
   1032                     .await
   1033                     .assert_ok_json::<RegistrationResponse>();
   1034                 wire_gateway
   1035                     .post("/admin/add-mapped")
   1036                     .json(json!({
   1037                         "amount": amount,
   1038                         "authorization_pub": auth_pub,
   1039                         "debit_account": debit_account,
   1040                     }))
   1041                     .await
   1042                     .assert_ok_json::<TransferResponse>();
   1043                 wire_gateway
   1044                     .post("/admin/add-mapped")
   1045                     .json(json!({
   1046                         "amount": amount,
   1047                         "authorization_pub": auth_pub,
   1048                         "debit_account": debit_account,
   1049                     }))
   1050                     .await
   1051                     .assert_ok_json::<TransferResponse>();
   1052             },
   1053             {
   1054                 let auth_pub = EddsaPublicKey::try_from(key.public_key().as_ref()).unwrap();
   1055                 let account_pub = EddsaPublicKey::rand();
   1056                 prepared_transfer
   1057                     .post("/registration")
   1058                     .json(
   1059                         RegistrationRequest {
   1060                             credit_account: credit_account.clone(),
   1061                             r#type: TransferType::kyc,
   1062                             recurrent: true,
   1063                             credit_amount: Amount::new(currency, 6, 0),
   1064                             alg: PublicKeyAlg::EdDSA,
   1065                             account_pub,
   1066                             authorization_pub: auth_pub,
   1067                             authorization_sig: EddsaSignature::ZEROED,
   1068                         }
   1069                         .signed(&key)
   1070                     )
   1071                     .await
   1072                     .assert_ok_json::<RegistrationResponse>();
   1073             }
   1074         ),
   1075         ignored,
   1076     )
   1077     .await;
   1078 
   1079     // Release an older bank payment after a consumer advances its cursor
   1080     let history = wire_gateway.suffix("/history/incoming");
   1081     let incoming = async |cursor| history.get(format!("?limit=10&offset={cursor}")).await;
   1082     let register = async |request: &RegistrationRequest| {
   1083         prepared_transfer
   1084             .post("/registration")
   1085             .json(request)
   1086             .await
   1087             .assert_ok_json::<RegistrationResponse>()
   1088     };
   1089     let initial_cursor = latest_id::<IncomingHistory>(&history).await;
   1090     let pair = Ed25519KeyPair::generate().unwrap();
   1091     let authorization_pub = EddsaPublicKey::try_from(pair.public_key().as_ref()).unwrap();
   1092     let first_reserve = EddsaPublicKey::rand();
   1093     let unrelated_reserve = EddsaPublicKey::rand();
   1094     let amount = Amount::new(currency, 42, 0);
   1095     let registration = RegistrationRequest {
   1096         credit_account: credit_account.clone(),
   1097         r#type: TransferType::reserve,
   1098         recurrent: true,
   1099         credit_amount: amount,
   1100         alg: PublicKeyAlg::EdDSA,
   1101         account_pub: first_reserve,
   1102         authorization_pub,
   1103         authorization_sig: EddsaSignature::ZEROED,
   1104     }
   1105     .signed(&pair);
   1106     register(&registration).await;
   1107     for _ in 0..2 {
   1108         wire_gateway
   1109             .post("/admin/add-mapped")
   1110             .json(json!({
   1111                 "amount": amount,
   1112                 "authorization_pub": authorization_pub,
   1113                 "debit_account": debit_account,
   1114             }))
   1115             .await
   1116             .assert_ok_json::<TransferResponse>();
   1117     }
   1118     wire_gateway
   1119         .post("/admin/add-incoming")
   1120         .json(json!({
   1121             "amount": amount,
   1122             "reserve_pub": unrelated_reserve,
   1123             "debit_account": debit_account,
   1124         }))
   1125         .await
   1126         .assert_ok_json::<TransferResponse>();
   1127     let before = incoming(initial_cursor)
   1128         .await
   1129         .assert_ok_json::<IncomingHistory>();
   1130     assert_eq!(before.incoming_transactions.len(), 2);
   1131     for (tx, expected) in before
   1132         .incoming_transactions
   1133         .iter()
   1134         .zip([first_reserve, unrelated_reserve])
   1135     {
   1136         assert!(
   1137             matches!(tx, IncomingBankTransaction::Reserve { reserve_pub, .. }
   1138             if *reserve_pub == expected)
   1139         );
   1140     }
   1141     let cursor = *before.ids().last().unwrap();
   1142     incoming(cursor).await.assert_no_content();
   1143     let release = RegistrationRequest {
   1144         account_pub: EddsaPublicKey::rand(),
   1145         ..registration
   1146     }
   1147     .signed(&pair);
   1148     let (response, ()) = tokio::join!(
   1149         assert_time(0..5000, async {
   1150             history
   1151                 .get(format!("?limit=10&offset={cursor}&timeout_ms=10000"))
   1152                 .await
   1153         }),
   1154         async {
   1155             sleep(Duration::from_millis(100)).await;
   1156             register(&release).await;
   1157         }
   1158     );
   1159     let resumed = response.assert_ok_json::<IncomingHistory>();
   1160     let [
   1161         IncomingBankTransaction::Reserve {
   1162             row_id,
   1163             reserve_pub,
   1164             amount: credited,
   1165             authorization_pub: Some(auth_pub),
   1166             authorization_sig: Some(auth_sig),
   1167             ..
   1168         },
   1169     ] = resumed.incoming_transactions.as_slice()
   1170     else {
   1171         panic!("expected exactly one released reserve payment");
   1172     };
   1173     assert_eq!(*reserve_pub, release.account_pub);
   1174     assert_eq!(*credited, amount);
   1175     assert_eq!(*auth_pub, authorization_pub);
   1176     assert!(release.verify(auth_pub, auth_sig));
   1177     let next_cursor = *row_id as i64;
   1178     assert!(
   1179         next_cursor > cursor,
   1180         "released payment must advance the history cursor"
   1181     );
   1182     assert_eq!(
   1183         incoming(cursor)
   1184             .await
   1185             .assert_ok_json::<IncomingHistory>()
   1186             .incoming_transactions,
   1187         resumed.incoming_transactions
   1188     );
   1189     incoming(next_cursor).await.assert_no_content();
   1190 }
   1191 
   1192 /// Test standard behavior of the admin add incoming endpoints
   1193 pub async fn admin_add_incoming_routine(
   1194     wire_gateway: &Router,
   1195     prepared_transfer: &Router,
   1196     debit_acount: &PaytoURI,
   1197     credit_account: &PaytoURI,
   1198 ) {
   1199     let currency = &get_currency(wire_gateway).await;
   1200     for kind in IncomingType::entries {
   1201         add_incoming_routine(
   1202             wire_gateway,
   1203             prepared_transfer,
   1204             currency,
   1205             *kind,
   1206             debit_acount,
   1207             credit_account,
   1208         )
   1209         .await;
   1210     }
   1211 }
   1212 
   1213 #[derive(Debug, PartialEq, Eq)]
   1214 pub enum Status {
   1215     Simple,
   1216     Pending,
   1217     Bounced,
   1218     Incomplete,
   1219     Reserve(EddsaPublicKey),
   1220     Kyc(EddsaPublicKey),
   1221 }
   1222 
   1223 /// Test standard registration behavior of the registration endpoints
   1224 pub async fn registration_routine<F1: Future<Output = Vec<Status>>>(
   1225     wire_gateway: &Router,
   1226     prepared_transfer: &Router,
   1227     debit_acount: &PaytoURI,
   1228     credit_account: &PaytoURI,
   1229     unknown_account: &PaytoURI,
   1230     mut in_status: impl FnMut() -> F1,
   1231 ) {
   1232     pub use Status::*;
   1233     let mut check_in = async |state: &[Status]| {
   1234         let current = in_status().await;
   1235         pretty_assertions::assert_eq!(state, current);
   1236     };
   1237 
   1238     let currency = &get_currency(wire_gateway).await;
   1239     let amount = amount(format!("{currency}:42"));
   1240     let key_pair1 = Ed25519KeyPair::generate().unwrap();
   1241     let auth_pub1 = EddsaPublicKey::try_from(key_pair1.public_key().as_ref()).unwrap();
   1242     let req = RegistrationRequest {
   1243         credit_account: credit_account.clone(),
   1244         r#type: TransferType::reserve,
   1245         recurrent: false,
   1246         credit_amount: amount,
   1247         alg: PublicKeyAlg::EdDSA,
   1248         account_pub: auth_pub1,
   1249         authorization_pub: auth_pub1,
   1250         authorization_sig: EddsaSignature::ZEROED,
   1251     };
   1252 
   1253     let register = async |auth_pub: &EddsaPublicKey| {
   1254         wire_gateway
   1255             .post("/admin/add-mapped")
   1256             .json(json!({
   1257                 "amount": format!("{currency}:42"),
   1258                 "authorization_pub": auth_pub,
   1259                 "debit_account": debit_acount,
   1260             }))
   1261             .await
   1262     };
   1263 
   1264     /* ----- Registration ----- */
   1265     let routine = async |ty: TransferType,
   1266                          account_pub: EddsaPublicKey,
   1267                          recurrent: bool,
   1268                          fmt: IncomingType| {
   1269         let req = RegistrationRequest {
   1270             r#type: ty,
   1271             account_pub,
   1272             recurrent,
   1273             ..req.clone()
   1274         }
   1275         .signed(&key_pair1);
   1276         // Valid
   1277         let res = prepared_transfer
   1278             .post("/registration")
   1279             .json(&req)
   1280             .await
   1281             .assert_ok_json::<RegistrationResponse>();
   1282 
   1283         // Idempotent
   1284         assert_eq!(
   1285             res,
   1286             prepared_transfer
   1287                 .post("/registration")
   1288                 .json(&req)
   1289                 .await
   1290                 .assert_ok_json::<RegistrationResponse>()
   1291         );
   1292 
   1293         assert!(!res.subjects.is_empty());
   1294 
   1295         for sub in res.subjects {
   1296             if let TransferSubject::Simple { subject, .. } = sub {
   1297                 assert_eq!(subject, fmt_in_subject(fmt, &auth_pub1).to_string());
   1298             };
   1299         }
   1300     };
   1301     for ty in [TransferType::reserve, TransferType::kyc] {
   1302         routine(ty, auth_pub1, false, ty.into()).await;
   1303         routine(ty, auth_pub1, true, IncomingType::map).await;
   1304     }
   1305 
   1306     let acc_pub1 = EddsaPublicKey::rand();
   1307     for ty in [TransferType::reserve, TransferType::kyc] {
   1308         routine(ty, acc_pub1, false, IncomingType::map).await;
   1309         routine(ty, acc_pub1, true, IncomingType::map).await;
   1310     }
   1311 
   1312     // Bad signature
   1313     prepared_transfer
   1314         .post("/registration")
   1315         .json(&req)
   1316         .await
   1317         .assert_error(ErrorCode::BANK_BAD_SIGNATURE);
   1318 
   1319     // Unknown account
   1320     prepared_transfer
   1321         .post("/registration")
   1322         .json(
   1323             RegistrationRequest {
   1324                 r#credit_account: unknown_account.clone(),
   1325                 ..req.clone()
   1326             }
   1327             .signed(&key_pair1),
   1328         )
   1329         .await
   1330         .assert_error(ErrorCode::BANK_UNKNOWN_CREDITOR);
   1331 
   1332     // Unsupported payto kind
   1333     prepared_transfer
   1334         .post("/registration")
   1335         .json(
   1336             RegistrationRequest {
   1337                 r#credit_account: UNKNOWN.clone(),
   1338                 ..req.clone()
   1339             }
   1340             .signed(&key_pair1),
   1341         )
   1342         .await
   1343         .assert_error(ErrorCode::GENERIC_PAYTO_URI_MALFORMED);
   1344 
   1345     // Malformed payto
   1346     prepared_transfer
   1347         .post("/registration")
   1348         .json(
   1349             RegistrationRequest {
   1350                 r#credit_account: unsafe { PaytoURI::from_raw("http://email@test.com") },
   1351                 ..req.clone()
   1352             }
   1353             .signed(&key_pair1),
   1354         )
   1355         .await
   1356         .assert_error(ErrorCode::GENERIC_JSON_INVALID);
   1357 
   1358     // Reserve pub reuse
   1359     prepared_transfer
   1360         .post("/registration")
   1361         .json(
   1362             RegistrationRequest {
   1363                 account_pub: acc_pub1,
   1364                 ..req.clone()
   1365             }
   1366             .signed(&key_pair1),
   1367         )
   1368         .await
   1369         .assert_ok_json::<RegistrationResponse>();
   1370     {
   1371         let key_pair = Ed25519KeyPair::generate().unwrap();
   1372         let auth_pub = EddsaPublicKey::try_from(key_pair.public_key().as_ref()).unwrap();
   1373         prepared_transfer
   1374             .post("/registration")
   1375             .json(
   1376                 RegistrationRequest {
   1377                     account_pub: acc_pub1,
   1378                     authorization_pub: auth_pub,
   1379                     ..req.clone()
   1380                 }
   1381                 .signed(&key_pair),
   1382             )
   1383             .await
   1384             .assert_error(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT);
   1385     }
   1386 
   1387     // Non recurrent accept one then bounce
   1388     prepared_transfer
   1389         .post("/registration")
   1390         .json(
   1391             RegistrationRequest {
   1392                 account_pub: acc_pub1,
   1393                 ..req.clone()
   1394             }
   1395             .signed(&key_pair1),
   1396         )
   1397         .await
   1398         .assert_ok_json::<RegistrationResponse>();
   1399     register(&auth_pub1)
   1400         .await
   1401         .assert_ok_json::<TransferResponse>();
   1402     check_in(&[Reserve(acc_pub1)]).await;
   1403     register(&auth_pub1)
   1404         .await
   1405         .assert_error(ErrorCode::BANK_TRANSFER_MAPPING_REUSED);
   1406 
   1407     // Again without using mapping
   1408     let acc_pub2 = EddsaPublicKey::rand();
   1409     prepared_transfer
   1410         .post("/registration")
   1411         .json(
   1412             RegistrationRequest {
   1413                 account_pub: acc_pub2,
   1414                 ..req.clone()
   1415             }
   1416             .signed(&key_pair1),
   1417         )
   1418         .await
   1419         .assert_ok_json::<RegistrationResponse>();
   1420     wire_gateway
   1421         .post("/admin/add-incoming")
   1422         .json(json!({
   1423             "amount": amount,
   1424             "reserve_pub": acc_pub2,
   1425             "debit_account": debit_acount,
   1426         }))
   1427         .await
   1428         .assert_ok();
   1429     register(&auth_pub1)
   1430         .await
   1431         .assert_error(ErrorCode::BANK_TRANSFER_MAPPING_REUSED);
   1432     check_in(&[Reserve(acc_pub1), Reserve(acc_pub2)]).await;
   1433 
   1434     // Recurrent accept one and delay others
   1435     let acc_pub3 = EddsaPublicKey::rand();
   1436     prepared_transfer
   1437         .post("/registration")
   1438         .json(
   1439             RegistrationRequest {
   1440                 account_pub: acc_pub3,
   1441                 recurrent: true,
   1442                 ..req.clone()
   1443             }
   1444             .signed(&key_pair1),
   1445         )
   1446         .await
   1447         .assert_ok_json::<RegistrationResponse>();
   1448     for _ in 0..5 {
   1449         register(&auth_pub1)
   1450             .await
   1451             .assert_ok_json::<TransferResponse>();
   1452     }
   1453     check_in(&[
   1454         Reserve(acc_pub1),
   1455         Reserve(acc_pub2),
   1456         Reserve(acc_pub3),
   1457         Pending,
   1458         Pending,
   1459         Pending,
   1460         Pending,
   1461     ])
   1462     .await;
   1463 
   1464     // Complete pending on recurrent update
   1465     let acc_pub4 = EddsaPublicKey::rand();
   1466     prepared_transfer
   1467         .post("/registration")
   1468         .json(
   1469             RegistrationRequest {
   1470                 r#type: TransferType::kyc,
   1471                 account_pub: acc_pub4,
   1472                 recurrent: true,
   1473                 ..req.clone()
   1474             }
   1475             .signed(&key_pair1),
   1476         )
   1477         .await
   1478         .assert_ok_json::<RegistrationResponse>();
   1479     prepared_transfer
   1480         .post("/registration")
   1481         .json(
   1482             RegistrationRequest {
   1483                 account_pub: acc_pub4,
   1484                 recurrent: true,
   1485                 ..req.clone()
   1486             }
   1487             .signed(&key_pair1),
   1488         )
   1489         .await
   1490         .assert_ok_json::<RegistrationResponse>();
   1491     check_in(&[
   1492         Reserve(acc_pub1),
   1493         Reserve(acc_pub2),
   1494         Reserve(acc_pub3),
   1495         Kyc(acc_pub4),
   1496         Reserve(acc_pub4),
   1497         Pending,
   1498         Pending,
   1499     ])
   1500     .await;
   1501 
   1502     // Kyc key reuse keep pending ones
   1503     wire_gateway
   1504         .post("/admin/add-kycauth")
   1505         .json(json!({
   1506             "amount": amount,
   1507             "account_pub": acc_pub4,
   1508             "debit_account": debit_acount,
   1509         }))
   1510         .await
   1511         .assert_ok_json::<TransferResponse>();
   1512     check_in(&[
   1513         Reserve(acc_pub1),
   1514         Reserve(acc_pub2),
   1515         Reserve(acc_pub3),
   1516         Kyc(acc_pub4),
   1517         Reserve(acc_pub4),
   1518         Pending,
   1519         Pending,
   1520         Kyc(acc_pub4),
   1521     ])
   1522     .await;
   1523 
   1524     // Switching to non recurrent cancel pending
   1525     let auth_pair = Ed25519KeyPair::generate().unwrap();
   1526     let auth_pub2 = EddsaPublicKey::try_from(auth_pair.public_key().as_ref()).unwrap();
   1527     prepared_transfer
   1528         .post("/registration")
   1529         .json(
   1530             RegistrationRequest {
   1531                 account_pub: auth_pub2,
   1532                 authorization_pub: auth_pub2,
   1533                 recurrent: true,
   1534                 ..req.clone()
   1535             }
   1536             .signed(&auth_pair),
   1537         )
   1538         .await
   1539         .assert_ok_json::<RegistrationResponse>();
   1540     for _ in 0..3 {
   1541         register(&auth_pub2)
   1542             .await
   1543             .assert_ok_json::<TransferResponse>();
   1544     }
   1545     check_in(&[
   1546         Reserve(acc_pub1),
   1547         Reserve(acc_pub2),
   1548         Reserve(acc_pub3),
   1549         Kyc(acc_pub4),
   1550         Reserve(acc_pub4),
   1551         Pending,
   1552         Pending,
   1553         Kyc(acc_pub4),
   1554         Reserve(auth_pub2),
   1555         Pending,
   1556         Pending,
   1557     ])
   1558     .await;
   1559     prepared_transfer
   1560         .post("/registration")
   1561         .json(
   1562             RegistrationRequest {
   1563                 r#type: TransferType::kyc,
   1564                 account_pub: auth_pub2,
   1565                 authorization_pub: auth_pub2,
   1566                 recurrent: false,
   1567                 ..req.clone()
   1568             }
   1569             .signed(&auth_pair),
   1570         )
   1571         .await
   1572         .assert_ok_json::<RegistrationResponse>();
   1573     check_in(&[
   1574         Reserve(acc_pub1),
   1575         Reserve(acc_pub2),
   1576         Reserve(acc_pub3),
   1577         Kyc(acc_pub4),
   1578         Reserve(acc_pub4),
   1579         Pending,
   1580         Pending,
   1581         Kyc(acc_pub4),
   1582         Reserve(auth_pub2),
   1583         Bounced,
   1584         Bounced,
   1585     ])
   1586     .await;
   1587 
   1588     // Recurrent reserve simple subject
   1589     let acc_pub5 = EddsaPublicKey::rand();
   1590     prepared_transfer
   1591         .post("/registration")
   1592         .json(
   1593             RegistrationRequest {
   1594                 account_pub: acc_pub5,
   1595                 authorization_pub: auth_pub2,
   1596                 recurrent: true,
   1597                 ..req.clone()
   1598             }
   1599             .signed(&auth_pair),
   1600         )
   1601         .await
   1602         .assert_ok_json::<RegistrationResponse>();
   1603     wire_gateway
   1604         .post("/admin/add-incoming")
   1605         .json(json!({
   1606             "amount": amount,
   1607             "reserve_pub": acc_pub5,
   1608             "debit_account": debit_acount,
   1609         }))
   1610         .await
   1611         .assert_ok();
   1612     register(&auth_pub2)
   1613         .await
   1614         .assert_ok_json::<TransferResponse>();
   1615     check_in(&[
   1616         Reserve(acc_pub1),
   1617         Reserve(acc_pub2),
   1618         Reserve(acc_pub3),
   1619         Kyc(acc_pub4),
   1620         Reserve(acc_pub4),
   1621         Pending,
   1622         Pending,
   1623         Kyc(acc_pub4),
   1624         Reserve(auth_pub2),
   1625         Bounced,
   1626         Bounced,
   1627         Reserve(acc_pub5),
   1628         Pending,
   1629     ])
   1630     .await;
   1631 
   1632     // Recurrent kyc simple subject
   1633     prepared_transfer
   1634         .post("/registration")
   1635         .json(
   1636             RegistrationRequest {
   1637                 r#type: TransferType::kyc,
   1638                 account_pub: acc_pub5,
   1639                 authorization_pub: auth_pub2,
   1640                 ..req.clone()
   1641             }
   1642             .signed(&auth_pair),
   1643         )
   1644         .await
   1645         .assert_ok_json::<RegistrationResponse>();
   1646     prepared_transfer
   1647         .post("/registration")
   1648         .json(
   1649             RegistrationRequest {
   1650                 r#type: TransferType::kyc,
   1651                 account_pub: acc_pub5,
   1652                 authorization_pub: auth_pub2,
   1653                 recurrent: true,
   1654                 ..req.clone()
   1655             }
   1656             .signed(&auth_pair),
   1657         )
   1658         .await
   1659         .assert_ok_json::<RegistrationResponse>();
   1660     let pair = Ed25519KeyPair::generate().unwrap();
   1661     prepared_transfer
   1662         .post("/registration")
   1663         .json(
   1664             RegistrationRequest {
   1665                 r#type: TransferType::kyc,
   1666                 account_pub: acc_pub5,
   1667                 authorization_pub: EddsaPublicKey::try_from(pair.public_key().as_ref()).unwrap(),
   1668                 recurrent: true,
   1669                 ..req.clone()
   1670             }
   1671             .signed(&pair),
   1672         )
   1673         .await
   1674         .assert_ok_json::<RegistrationResponse>();
   1675     wire_gateway
   1676         .post("/admin/add-kycauth")
   1677         .json(json!({
   1678             "amount": amount,
   1679             "account_pub": acc_pub5,
   1680             "debit_account": debit_acount,
   1681         }))
   1682         .await
   1683         .assert_ok();
   1684     for _ in 0..2 {
   1685         register(&auth_pub2)
   1686             .await
   1687             .assert_ok_json::<TransferResponse>();
   1688     }
   1689     check_in(&[
   1690         Reserve(acc_pub1),
   1691         Reserve(acc_pub2),
   1692         Reserve(acc_pub3),
   1693         Kyc(acc_pub4),
   1694         Reserve(acc_pub4),
   1695         Pending,
   1696         Pending,
   1697         Kyc(acc_pub4),
   1698         Reserve(auth_pub2),
   1699         Bounced,
   1700         Bounced,
   1701         Reserve(acc_pub5),
   1702         Bounced,
   1703         Kyc(acc_pub5),
   1704         Kyc(acc_pub5),
   1705         Pending,
   1706     ])
   1707     .await;
   1708 
   1709     // Kyc without using mapping
   1710 
   1711     /* ----- Unregistration ----- */
   1712     let un_req = Unregistration {
   1713         timestamp: TalerTimestamp::Timestamp(Timestamp::now()),
   1714         authorization_pub: auth_pub2,
   1715         authorization_sig: EddsaSignature::ZEROED,
   1716     };
   1717     let signed = un_req.clone().signed(&auth_pair);
   1718 
   1719     // Delete
   1720     prepared_transfer
   1721         .post("/unregistration")
   1722         .json(&signed)
   1723         .await
   1724         .assert_no_content();
   1725 
   1726     // Check bounce pending on deletion
   1727     check_in(&[
   1728         Reserve(acc_pub1),
   1729         Reserve(acc_pub2),
   1730         Reserve(acc_pub3),
   1731         Kyc(acc_pub4),
   1732         Reserve(acc_pub4),
   1733         Pending,
   1734         Pending,
   1735         Kyc(acc_pub4),
   1736         Reserve(auth_pub2),
   1737         Bounced,
   1738         Bounced,
   1739         Reserve(acc_pub5),
   1740         Bounced,
   1741         Kyc(acc_pub5),
   1742         Kyc(acc_pub5),
   1743         Bounced,
   1744     ])
   1745     .await;
   1746 
   1747     // Idempotent
   1748     prepared_transfer
   1749         .post("/unregistration")
   1750         .json(&signed)
   1751         .await
   1752         .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND);
   1753 
   1754     // Bad signature
   1755     prepared_transfer
   1756         .post("/unregistration")
   1757         .json(&un_req)
   1758         .await
   1759         .assert_error(ErrorCode::BANK_BAD_SIGNATURE);
   1760 
   1761     // Old timestamp
   1762     prepared_transfer
   1763         .post("/unregistration")
   1764         .json(
   1765             Unregistration {
   1766                 timestamp: TalerTimestamp::Timestamp(
   1767                     Timestamp::now() - SignedDuration::from_mins(10),
   1768                 ),
   1769                 ..un_req.clone()
   1770             }
   1771             .signed(&auth_pair),
   1772         )
   1773         .await
   1774         .assert_error(ErrorCode::BANK_OLD_TIMESTAMP);
   1775 
   1776     // Never timestamp
   1777     prepared_transfer
   1778         .post("/unregistration")
   1779         .json(
   1780             Unregistration {
   1781                 timestamp: TalerTimestamp::Never,
   1782                 ..un_req.clone()
   1783             }
   1784             .signed(&auth_pair),
   1785         )
   1786         .await
   1787         .assert_error(ErrorCode::BANK_OLD_TIMESTAMP);
   1788 
   1789     // Future timestamp
   1790     prepared_transfer
   1791         .post("/unregistration")
   1792         .json(
   1793             Unregistration {
   1794                 timestamp: TalerTimestamp::Timestamp(
   1795                     Timestamp::now() + SignedDuration::from_mins(1),
   1796                 ),
   1797                 ..un_req.clone()
   1798             }
   1799             .signed(&auth_pair),
   1800         )
   1801         .await
   1802         .assert_error(ErrorCode::BANK_OLD_TIMESTAMP);
   1803 
   1804     // A KYC payment with a reserve's account key must not consume that
   1805     // reserve's prepared registration. The mapped reserve is still valid.
   1806     let reserve_pair = Ed25519KeyPair::generate().unwrap();
   1807     let authorization_pub = EddsaPublicKey::try_from(reserve_pair.public_key().as_ref()).unwrap();
   1808     let reserve_pub = EddsaPublicKey::rand();
   1809     prepared_transfer
   1810         .post("/registration")
   1811         .json(
   1812             RegistrationRequest {
   1813                 account_pub: reserve_pub,
   1814                 authorization_pub,
   1815                 ..req.clone()
   1816             }
   1817             .signed(&reserve_pair),
   1818         )
   1819         .await
   1820         .assert_ok_json::<RegistrationResponse>();
   1821     wire_gateway
   1822         .post("/admin/add-kycauth")
   1823         .json(json!({
   1824             "amount": amount,
   1825             "account_pub": reserve_pub,
   1826             "debit_account": debit_acount,
   1827         }))
   1828         .await
   1829         .assert_ok_json::<TransferResponse>();
   1830     register(&authorization_pub)
   1831         .await
   1832         .assert_ok_json::<TransferResponse>();
   1833 
   1834     // Re-registering the same key with changed signed information must
   1835     // replace the signature retained for the next mapped transfer.
   1836     let replacement_pair = Ed25519KeyPair::generate().unwrap();
   1837     let replacement_auth_pub =
   1838         EddsaPublicKey::try_from(replacement_pair.public_key().as_ref()).unwrap();
   1839     let replacement_reserve_pub = EddsaPublicKey::rand();
   1840     let original_registration = RegistrationRequest {
   1841         account_pub: replacement_reserve_pub,
   1842         authorization_pub: replacement_auth_pub,
   1843         ..req.clone()
   1844     }
   1845     .signed(&replacement_pair);
   1846     prepared_transfer
   1847         .post("/registration")
   1848         .json(&original_registration)
   1849         .await
   1850         .assert_ok_json::<RegistrationResponse>();
   1851     let replacement_registration = RegistrationRequest {
   1852         credit_amount: taler_common::types::amount::amount(format!("{currency}:43")),
   1853         ..original_registration
   1854     }
   1855     .signed(&replacement_pair);
   1856     prepared_transfer
   1857         .post("/registration")
   1858         .json(&replacement_registration)
   1859         .await
   1860         .assert_ok_json::<RegistrationResponse>();
   1861     let before_replacement_transfer =
   1862         latest_id::<IncomingHistory>(&wire_gateway.suffix("/history/incoming")).await;
   1863     register(&replacement_auth_pub)
   1864         .await
   1865         .assert_ok_json::<TransferResponse>();
   1866     let latest = wire_gateway
   1867         .get(format!(
   1868             "/history/incoming?limit=1&offset={before_replacement_transfer}"
   1869         ))
   1870         .await
   1871         .assert_ok_json::<IncomingHistory>();
   1872     assert!(matches!(
   1873         &latest.incoming_transactions[0],
   1874         IncomingBankTransaction::Reserve {
   1875             reserve_pub,
   1876             authorization_pub: Some(auth_pub),
   1877             authorization_sig: Some(auth_sig),
   1878             ..
   1879         } if *reserve_pub == replacement_reserve_pub
   1880             && *auth_pub == replacement_auth_pub
   1881             && replacement_registration.verify(auth_pub, auth_sig)
   1882     ));
   1883 
   1884     /* ----- API ----- */
   1885 
   1886     let history: Vec<_> = wire_gateway
   1887         .get("/history/incoming?limit=20")
   1888         .await
   1889         .assert_ok_json::<IncomingHistory>()
   1890         .incoming_transactions
   1891         .into_iter()
   1892         .map(|tx| {
   1893             let (acc_pub, auth_pub, auth_sig) = match tx {
   1894                 IncomingBankTransaction::Reserve {
   1895                     reserve_pub,
   1896                     authorization_pub,
   1897                     authorization_sig,
   1898                     ..
   1899                 } => (reserve_pub, authorization_pub, authorization_sig),
   1900                 IncomingBankTransaction::Wad { .. } => unreachable!(),
   1901                 IncomingBankTransaction::Kyc {
   1902                     account_pub,
   1903                     authorization_pub,
   1904                     authorization_sig,
   1905                     ..
   1906                 } => (account_pub, authorization_pub, authorization_sig),
   1907             };
   1908             assert_eq!(auth_pub.is_some(), auth_sig.is_some());
   1909             (acc_pub, auth_pub)
   1910         })
   1911         .collect();
   1912     pretty_assertions::assert_eq!(
   1913         history,
   1914         [
   1915             (acc_pub1, Some(auth_pub1)),
   1916             (acc_pub2, None),
   1917             (acc_pub3, Some(auth_pub1)),
   1918             (acc_pub4, Some(auth_pub1)),
   1919             (acc_pub4, Some(auth_pub1)),
   1920             (acc_pub4, None),
   1921             (auth_pub2, Some(auth_pub2)),
   1922             (acc_pub5, None),
   1923             (acc_pub5, None),
   1924             (acc_pub5, Some(auth_pub2)),
   1925             (reserve_pub, None),
   1926             (reserve_pub, Some(authorization_pub)),
   1927             (replacement_reserve_pub, Some(replacement_auth_pub)),
   1928         ]
   1929     )
   1930 }