taler-rust

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

routine.rs (58510B)


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