taler-rust

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

routine.rs (60541B)


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