taler-rust

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

routine.rs (38166B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2024, 2025, 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     time::{Duration, Instant},
     21 };
     22 
     23 use aws_lc_rs::signature::{Ed25519KeyPair, KeyPair as _};
     24 use axum::{Router, http::StatusCode};
     25 use jiff::{SignedDuration, Timestamp};
     26 use serde::de::DeserializeOwned;
     27 use taler_api::{
     28     crypto::{check_eddsa_signature, eddsa_sign},
     29     subject::fmt_in_subject,
     30 };
     31 use taler_common::{
     32     api_common::{EddsaPublicKey, HashCode, ShortHashCode},
     33     api_params::PageParams,
     34     api_revenue::RevenueIncomingHistory,
     35     api_transfer::{RegistrationResponse, TransferSubject, TransferType},
     36     api_wire::{
     37         IncomingBankTransaction, IncomingHistory, OutgoingHistory, TransferList, TransferRequest,
     38         TransferResponse, TransferState, TransferStatus,
     39     },
     40     db::IncomingType,
     41     error_code::ErrorCode,
     42     types::{amount::amount, base32::Base32, payto::PaytoURI, url},
     43 };
     44 use tokio::time::sleep;
     45 
     46 use crate::{
     47     json,
     48     server::{TestResponse, TestServer as _},
     49 };
     50 
     51 pub trait Page: DeserializeOwned {
     52     fn ids(&self) -> Vec<i64>;
     53 }
     54 
     55 impl Page for IncomingHistory {
     56     fn ids(&self) -> Vec<i64> {
     57         self.incoming_transactions
     58             .iter()
     59             .map(|it| match it {
     60                 IncomingBankTransaction::Reserve { row_id, .. }
     61                 | IncomingBankTransaction::Wad { row_id, .. }
     62                 | IncomingBankTransaction::Kyc { row_id, .. } => **row_id as i64,
     63             })
     64             .collect()
     65     }
     66 }
     67 
     68 impl Page for OutgoingHistory {
     69     fn ids(&self) -> Vec<i64> {
     70         self.outgoing_transactions
     71             .iter()
     72             .map(|it| *it.row_id as i64)
     73             .collect()
     74     }
     75 }
     76 
     77 impl Page for RevenueIncomingHistory {
     78     fn ids(&self) -> Vec<i64> {
     79         self.incoming_transactions
     80             .iter()
     81             .map(|it| *it.row_id as i64)
     82             .collect()
     83     }
     84 }
     85 
     86 impl Page for TransferList {
     87     fn ids(&self) -> Vec<i64> {
     88         self.transfers.iter().map(|it| *it.row_id as i64).collect()
     89     }
     90 }
     91 
     92 pub async fn latest_id<T: Page>(router: &Router, url: &str) -> i64 {
     93     let res = router.get(&format!("{url}?limit=-1")).await;
     94     if res.status == StatusCode::NO_CONTENT {
     95         0
     96     } else {
     97         res.assert_ids::<T>(1)[0]
     98     }
     99 }
    100 
    101 pub async fn routine_pagination<T: Page, F: Future<Output = ()>>(
    102     server: &Router,
    103     url: &str,
    104     mut register: impl FnMut(usize) -> F,
    105 ) {
    106     // Check supported
    107     if !server.get(url).await.is_implemented() {
    108         return;
    109     }
    110 
    111     // Check history is following specs
    112     let assert_history = async |args: &str, size: usize| {
    113         server
    114             .get(&format!("{url}?{args}"))
    115             .await
    116             .assert_ids::<T>(size)
    117     };
    118     // Get latest registered id
    119     let latest_id = async || latest_id::<T>(server, url).await;
    120 
    121     for i in 0..20 {
    122         register(i).await;
    123     }
    124 
    125     let id = latest_id().await;
    126 
    127     // default
    128     assert_history("", 20).await;
    129 
    130     // forward range
    131     assert_history("limit=10", 10).await;
    132     assert_history("limit=10&offset=4", 10).await;
    133 
    134     // backward range
    135     assert_history("limit=-10", 10).await;
    136     assert_history(&format!("limit=-10&{}", id - 4), 10).await;
    137 }
    138 
    139 async fn assert_time<R: Debug>(range: std::ops::Range<u128>, task: impl Future<Output = R>) {
    140     let start = Instant::now();
    141     task.await;
    142     let elapsed = start.elapsed().as_millis();
    143     if !range.contains(&elapsed) {
    144         panic!("Expected to last {range:?} got {elapsed:?}")
    145     }
    146 }
    147 
    148 async fn check_history_trigger<T: Page, F: Future<Output = ()>>(
    149     server: &Router,
    150     url: &str,
    151     lambda: impl FnOnce() -> F,
    152 ) {
    153     // Check history is following specs
    154     macro_rules! assert_history {
    155         ($args:expr, $size:expr) => {
    156             async {
    157                 server
    158                     .get(&format!("{url}?{}", $args))
    159                     .await
    160                     .assert_ids::<T>($size)
    161             }
    162         };
    163     }
    164     // Get latest registered id
    165     let latest_id = latest_id::<T>(server, url).await;
    166     tokio::join!(
    167         // Check polling succeed
    168         assert_time(
    169             100..400,
    170             assert_history!(
    171                 format_args!("limit=2&offset={latest_id}&timeout_ms=1000"),
    172                 1
    173             )
    174         ),
    175         assert_time(
    176             200..500,
    177             assert_history!(
    178                 format_args!("limit=1&offset={}&timeout_ms=200", latest_id + 3),
    179                 0
    180             )
    181         ),
    182         async {
    183             sleep(Duration::from_millis(100)).await;
    184             lambda().await
    185         }
    186     );
    187 }
    188 
    189 async fn check_history_in_trigger<F: Future<Output = ()>>(
    190     server: &Router,
    191     lambda: impl FnOnce() -> F,
    192 ) {
    193     check_history_trigger::<IncomingHistory, _>(
    194         server,
    195         "/taler-wire-gateway/history/incoming",
    196         lambda,
    197     )
    198     .await;
    199 }
    200 
    201 pub async fn routine_history<T: Page, FR: Future<Output = ()>, FI: Future<Output = ()>>(
    202     server: &Router,
    203     url: &str,
    204     nb_register: usize,
    205     mut register: impl FnMut(usize) -> FR,
    206     nb_ignore: usize,
    207     mut ignore: impl FnMut(usize) -> FI,
    208 ) {
    209     // Check history is following specs
    210     macro_rules! assert_history {
    211         ($args:expr, $size:expr) => {
    212             async {
    213                 server
    214                     .get(&format!("{url}?{}", $args))
    215                     .await
    216                     .assert_ids::<T>($size)
    217             }
    218         };
    219     }
    220     // Get latest registered id
    221     let latest_id = async || assert_history!("limit=-1", 1).await[0];
    222 
    223     // Check error when no transactions
    224     assert_history!("limit=7".to_owned(), 0).await;
    225 
    226     let mut register_iter = (0..nb_register).peekable();
    227     let mut ignore_iter = (0..nb_ignore).peekable();
    228     while register_iter.peek().is_some() || ignore_iter.peek().is_some() {
    229         if let Some(idx) = register_iter.next() {
    230             register(idx).await
    231         }
    232         if let Some(idx) = ignore_iter.next() {
    233             ignore(idx).await
    234         }
    235     }
    236     let nb_total = nb_register + nb_ignore;
    237 
    238     // Check ignored
    239     assert_history!(format_args!("limit={nb_total}"), nb_register).await;
    240     // Check skip ignored
    241     assert_history!(format_args!("limit={nb_register}"), nb_register).await;
    242 
    243     // Check no polling when we cannot have more transactions
    244     assert_time(
    245         0..200,
    246         assert_history!(
    247             format_args!("limit=-{}&timeout_ms=1000", nb_register + 1),
    248             nb_register
    249         ),
    250     )
    251     .await;
    252     // Check no polling when already find transactions even if less than delta
    253     assert_time(
    254         0..200,
    255         assert_history!(
    256             format_args!("limit={}&timeout_ms=1000", nb_register + 1),
    257             nb_register
    258         ),
    259     )
    260     .await;
    261 
    262     // Check polling
    263     let id = latest_id().await;
    264     tokio::join!(
    265         // Check polling succeed
    266         assert_time(
    267             100..400,
    268             assert_history!(format_args!("limit=2&offset={id}&timeout_ms=1000"), 1)
    269         ),
    270         assert_time(
    271             200..500,
    272             assert_history!(
    273                 format_args!(
    274                     "limit=1&offset={}&timeout_ms=200",
    275                     id as usize + nb_total * 3
    276                 ),
    277                 0
    278             )
    279         ),
    280         async {
    281             sleep(Duration::from_millis(100)).await;
    282             register(0).await
    283         }
    284     );
    285 
    286     // Test triggers
    287     for i in 0..nb_register {
    288         let id = latest_id().await;
    289         tokio::join!(
    290             // Check polling succeed
    291             assert_time(
    292                 100..400,
    293                 assert_history!(format_args!("limit=7&offset={id}&timeout_ms=1000"), 1)
    294             ),
    295             async {
    296                 sleep(Duration::from_millis(100)).await;
    297                 register(i).await
    298             }
    299         );
    300     }
    301 
    302     // Test doesn't trigger
    303     let id = latest_id().await;
    304     tokio::join!(
    305         // Check polling succeed
    306         assert_time(
    307             200..500,
    308             assert_history!(format_args!("limit=7&offset={id}&timeout_ms=200"), 0)
    309         ),
    310         async {
    311             sleep(Duration::from_millis(100)).await;
    312             for i in 0..nb_ignore {
    313                 ignore(i).await
    314             }
    315         }
    316     );
    317 
    318     routine_pagination::<T, _>(server, url, register).await;
    319 }
    320 
    321 impl TestResponse {
    322     #[track_caller]
    323     fn assert_ids<T: Page>(&self, size: usize) -> Vec<i64> {
    324         if size == 0 {
    325             self.assert_no_content();
    326             return vec![];
    327         }
    328         let body = self.assert_ok_json::<T>();
    329         let page = body.ids();
    330         let params = self.query::<PageParams>().check(1024).unwrap();
    331 
    332         // testing the size is like expected
    333         assert_eq!(size, page.len(), "bad page length: {page:?}");
    334         if params.limit < 0 {
    335             // testing that the first id is at most the 'offset' query param.
    336             assert!(
    337                 params
    338                     .offset
    339                     .map(|offset| page[0] <= offset)
    340                     .unwrap_or(true),
    341                 "bad page offset: {params:?} {page:?}"
    342             );
    343             // testing that the id decreases.
    344             assert!(
    345                 page.as_slice().is_sorted_by(|a, b| a > b),
    346                 "bad page order: {page:?}"
    347             )
    348         } else {
    349             // testing that the first id is at least the 'offset' query param.
    350             assert!(
    351                 params
    352                     .offset
    353                     .map(|offset| page[0] >= offset)
    354                     .unwrap_or(true),
    355                 "bad page offset: {params:?} {page:?}"
    356             );
    357             // testing that the id increases.
    358             assert!(page.as_slice().is_sorted(), "bad page order: {page:?}")
    359         }
    360         page
    361     }
    362 }
    363 
    364 // Get currency from config
    365 async fn get_wire_currency(server: &Router) -> String {
    366     let config = server
    367         .get("/taler-wire-gateway/config")
    368         .await
    369         .assert_ok_json::<serde_json::Value>();
    370     let currency = config["currency"].as_str().unwrap();
    371     currency.to_owned()
    372 }
    373 
    374 /// Test standard behavior of the transfer endpoints
    375 pub async fn transfer_routine(
    376     server: &Router,
    377     default_status: TransferState,
    378     credit_account: &PaytoURI,
    379 ) {
    380     let currency = &get_wire_currency(server).await;
    381     let default_amount = amount(format!("{currency}:42"));
    382     let request_uid = HashCode::rand();
    383     let wtid = ShortHashCode::rand();
    384     let transfer_request = json!({
    385         "request_uid": request_uid,
    386         "amount": default_amount,
    387         "exchange_base_url": "http://exchange.taler/",
    388         "wtid": wtid,
    389         "credit_account": credit_account,
    390     });
    391 
    392     // Check empty db
    393     {
    394         server
    395             .get("/taler-wire-gateway/transfers")
    396             .await
    397             .assert_no_content();
    398         server
    399             .get(&format!(
    400                 "/taler-wire-gateway/transfers?status={}",
    401                 default_status.as_ref()
    402             ))
    403             .await
    404             .assert_no_content();
    405     }
    406 
    407     // TODO check subject formatting
    408 
    409     let routine = async |req: &TransferRequest| {
    410         // Check OK
    411         let first = server
    412             .post("/taler-wire-gateway/transfer")
    413             .json(&req)
    414             .await
    415             .assert_ok_json::<TransferResponse>();
    416         // Check idempotent
    417         let second = server
    418             .post("/taler-wire-gateway/transfer")
    419             .json(&req)
    420             .await
    421             .assert_ok_json::<TransferResponse>();
    422         assert_eq!(first.row_id, second.row_id);
    423         assert_eq!(first.timestamp, second.timestamp);
    424 
    425         // Check by id
    426         let tx = server
    427             .get(&format!("/taler-wire-gateway/transfers/{}", first.row_id))
    428             .await
    429             .assert_ok_json::<TransferStatus>();
    430         assert_eq!(default_status, tx.status);
    431         assert_eq!(default_amount, tx.amount);
    432         assert_eq!("http://exchange.taler/", tx.origin_exchange_url);
    433         assert_eq!(req.wtid, tx.wtid);
    434         assert_eq!(first.timestamp, tx.timestamp);
    435         assert_eq!(req.metadata, tx.metadata);
    436         assert_eq!(credit_account, &tx.credit_account);
    437 
    438         // Check page
    439         let list = server
    440             .get("/taler-wire-gateway/transfers?limit=-1")
    441             .await
    442             .assert_ok_json::<TransferList>();
    443         let tx = &list.transfers[0];
    444         assert_eq!(first.row_id, tx.row_id);
    445         assert_eq!(default_status, tx.status);
    446         assert_eq!(default_amount, tx.amount);
    447         assert_eq!(first.timestamp, tx.timestamp);
    448         assert_eq!(credit_account, &tx.credit_account);
    449     };
    450 
    451     let req = TransferRequest {
    452         request_uid,
    453         amount: default_amount.clone(),
    454         exchange_base_url: url("http://exchange.taler/"),
    455         metadata: None,
    456         wtid,
    457         credit_account: credit_account.clone(),
    458     };
    459     // Simple
    460     routine(&req).await;
    461     // With metadata
    462     routine(&TransferRequest {
    463         request_uid: HashCode::rand(),
    464         wtid: ShortHashCode::rand(),
    465         metadata: Some("test:medatata".into()),
    466         ..req
    467     })
    468     .await;
    469 
    470     // Check create transfer errors
    471     {
    472         // Check request uid reuse
    473         server
    474             .post("/taler-wire-gateway/transfer")
    475             .json(&json!(transfer_request + {
    476                 "wtid": ShortHashCode::rand()
    477             }))
    478             .await
    479             .assert_error(ErrorCode::BANK_TRANSFER_REQUEST_UID_REUSED);
    480         // Check wtid reuse
    481         server
    482             .post("/taler-wire-gateway/transfer")
    483             .json(&json!(transfer_request + {
    484                 "request_uid": HashCode::rand(),
    485             }))
    486             .await
    487             .assert_error(ErrorCode::BANK_TRANSFER_WTID_REUSED);
    488 
    489         // Check currency mismatch
    490         server
    491             .post("/taler-wire-gateway/transfer")
    492             .json(&json!(transfer_request + {
    493                 "amount": "BAD:42"
    494             }))
    495             .await
    496             .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH);
    497 
    498         // Malformed metadata
    499         for metadata in ["bad_id", "bad id", "bad@id.com", &"A".repeat(41)] {
    500             server
    501                 .post("/taler-wire-gateway/transfer")
    502                 .json(&json!(transfer_request + {
    503                     "metadata": metadata
    504                 }))
    505                 .await
    506                 .assert_error(ErrorCode::GENERIC_JSON_INVALID);
    507         }
    508     }
    509 
    510     // Check transfer by id errors
    511     {
    512         // Check unknown transaction
    513         server
    514             .get("/taler-wire-gateway/transfers/42")
    515             .await
    516             .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND);
    517     }
    518 
    519     // Check transfer page
    520     {
    521         for _ in 0..4 {
    522             server
    523                 .post("/taler-wire-gateway/transfer")
    524                 .json(&json!(transfer_request + {
    525                     "request_uid": HashCode::rand(),
    526                     "wtid": ShortHashCode::rand(),
    527                 }))
    528                 .await
    529                 .assert_ok_json::<TransferResponse>();
    530         }
    531         {
    532             let list = server
    533                 .get("/taler-wire-gateway/transfers")
    534                 .await
    535                 .assert_ok_json::<TransferList>();
    536             assert_eq!(list.transfers.len(), 6);
    537             assert_eq!(
    538                 list,
    539                 server
    540                     .get(&format!(
    541                         "/taler-wire-gateway/transfers?status={}",
    542                         default_status.as_ref()
    543                     ))
    544                     .await
    545                     .assert_ok_json::<TransferList>()
    546             )
    547         }
    548 
    549         // Pagination test
    550         routine_pagination::<TransferList, _>(server, "/taler-wire-gateway/transfers", async |i| {
    551             server
    552                 .post("/taler-wire-gateway/transfer")
    553                 .json(&json!({
    554                     "request_uid": HashCode::rand(),
    555                     "amount": amount(format!("{currency}:0.0{i}")),
    556                     "exchange_base_url": url("http://exchange.taler"),
    557                     "wtid": ShortHashCode::rand(),
    558                     "credit_account": credit_account,
    559                 }))
    560                 .await
    561                 .assert_ok_json::<TransferResponse>();
    562         })
    563         .await;
    564     }
    565 }
    566 
    567 async fn add_incoming_routine(
    568     server: &Router,
    569     currency: &str,
    570     kind: IncomingType,
    571     debit_acount: &PaytoURI,
    572 ) {
    573     let (path, key) = match kind {
    574         IncomingType::reserve => ("/taler-wire-gateway/admin/add-incoming", "reserve_pub"),
    575         IncomingType::kyc => ("/taler-wire-gateway/admin/add-kycauth", "account_pub"),
    576         IncomingType::map => unreachable!(),
    577     };
    578     let valid_req = json!({
    579         "amount": format!("{currency}:44"),
    580         key: EddsaPublicKey::rand(),
    581         "debit_account": debit_acount,
    582     });
    583 
    584     // Check OK
    585     server.post(path).json(&valid_req).await.assert_ok();
    586 
    587     match kind {
    588         IncomingType::reserve => {
    589             // Trigger conflict due to reused reserve_pub
    590             server
    591                 .post(path)
    592                 .json(&json!(valid_req + {
    593                     "amount": format!("{currency}:44.1"),
    594                 }))
    595                 .await
    596                 .assert_error(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT)
    597         }
    598         IncomingType::kyc => {
    599             // Non conflict on reuse
    600             server.post(path).json(&valid_req).await.assert_ok();
    601         }
    602         IncomingType::map => unreachable!(),
    603     }
    604 
    605     // Currency mismatch
    606     server
    607         .post(path)
    608         .json(&json!(valid_req + {
    609             "amount": "BAD:33"
    610         }))
    611         .await
    612         .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH);
    613 
    614     // Bad BASE32 reserve_pub
    615     server
    616         .post(path)
    617         .json(&json!(valid_req + {
    618             key: "I love chocolate"
    619         }))
    620         .await
    621         .assert_error(ErrorCode::GENERIC_JSON_INVALID);
    622 
    623     server
    624         .post(path)
    625         .json(&json!(valid_req + {
    626             key: Base32::<31>::rand()
    627         }))
    628         .await
    629         .assert_error(ErrorCode::GENERIC_JSON_INVALID);
    630 
    631     // Bad payto kind
    632     server
    633         .post(path)
    634         .json(&json!(valid_req + {
    635             "debit_account": "http://email@test.com"
    636         }))
    637         .await
    638         .assert_error(ErrorCode::GENERIC_JSON_INVALID);
    639 }
    640 
    641 /// Test standard behavior of the revenue endpoints
    642 pub async fn revenue_routine(server: &Router, debit_acount: &PaytoURI, kyc: bool) {
    643     let currency = &get_wire_currency(server).await;
    644 
    645     routine_history::<RevenueIncomingHistory, _, _>(
    646         server,
    647         "/taler-revenue/history",
    648         2,
    649         async |i| {
    650             if i % 2 == 0 || !kyc {
    651                 server
    652                     .post("/taler-wire-gateway/admin/add-incoming")
    653                     .json(&json!({
    654                         "amount": format!("{currency}:0.0{i}"),
    655                         "reserve_pub": EddsaPublicKey::rand(),
    656                         "debit_account": debit_acount,
    657                     }))
    658                     .await
    659                     .assert_ok_json::<TransferResponse>();
    660             } else {
    661                 server
    662                     .post("/taler-wire-gateway/admin/add-kycauth")
    663                     .json(&json!({
    664                         "amount": format!("{currency}:0.0{i}"),
    665                         "account_pub": EddsaPublicKey::rand(),
    666                         "debit_account": debit_acount,
    667                     }))
    668                     .await
    669                     .assert_ok_json::<TransferResponse>();
    670             }
    671         },
    672         0,
    673         async |_| {},
    674     )
    675     .await;
    676 }
    677 
    678 /// Test standard behavior of the admin add incoming endpoints
    679 pub async fn admin_add_incoming_routine(server: &Router, debit_acount: &PaytoURI, kyc: bool) {
    680     let currency = &get_wire_currency(server).await;
    681 
    682     // History
    683     // TODO check non taler some are ignored
    684     routine_history::<IncomingHistory, _, _>(
    685         server,
    686         "/taler-wire-gateway/history/incoming",
    687         2,
    688         async |i| {
    689             if i % 2 == 0 || !kyc {
    690                 server
    691                     .post("/taler-wire-gateway/admin/add-incoming")
    692                     .json(&json!({
    693                         "amount": format!("{currency}:0.0{i}"),
    694                         "reserve_pub": EddsaPublicKey::rand(),
    695                         "debit_account": debit_acount,
    696                     }))
    697                     .await
    698                     .assert_ok_json::<TransferResponse>();
    699             } else {
    700                 server
    701                     .post("/taler-wire-gateway/admin/add-kycauth")
    702                     .json(&json!({
    703                         "amount": format!("{currency}:0.0{i}"),
    704                         "account_pub": EddsaPublicKey::rand(),
    705                         "debit_account": debit_acount,
    706                     }))
    707                     .await
    708                     .assert_ok_json::<TransferResponse>();
    709             }
    710         },
    711         0,
    712         async |_| {},
    713     )
    714     .await;
    715     // Add incoming reserve
    716     add_incoming_routine(server, currency, IncomingType::reserve, debit_acount).await;
    717     if kyc {
    718         // Add incoming kyc
    719         add_incoming_routine(server, currency, IncomingType::kyc, debit_acount).await;
    720     }
    721 }
    722 
    723 // Get currency from config
    724 async fn get_transfer_currency(server: &Router) -> String {
    725     let config = server
    726         .get("/taler-wire-transfer-gateway/config")
    727         .await
    728         .assert_ok_json::<serde_json::Value>();
    729     let currency = config["currency"].as_str().unwrap();
    730     currency.to_owned()
    731 }
    732 
    733 #[derive(Debug, PartialEq, Eq)]
    734 pub enum Status {
    735     Simple,
    736     Pending,
    737     Bounced,
    738     Incomplete,
    739     Reserve(EddsaPublicKey),
    740     Kyc(EddsaPublicKey),
    741 }
    742 
    743 /// Test standard registration behavior of the registration endpoints
    744 pub async fn registration_routine<F1: Future<Output = Vec<Status>>, F2: Future<Output = ()>>(
    745     server: &Router,
    746     account: &PaytoURI,
    747     mut in_status: impl FnMut() -> F1,
    748     mut register: impl FnMut(&EddsaPublicKey) -> F2,
    749 ) {
    750     pub use Status::*;
    751     let mut check_in = async |state: &[Status]| {
    752         let current = in_status().await;
    753         assert_eq!(state, current);
    754     };
    755 
    756     let currency = &get_transfer_currency(server).await;
    757     let amount = amount(format!("{currency}:42"));
    758     let key_pair1 = Ed25519KeyPair::generate().unwrap();
    759     let auth_pub1 = EddsaPublicKey::try_from(key_pair1.public_key().as_ref()).unwrap();
    760     let req = json!({
    761         "credit_amount": amount,
    762         "type": "reserve",
    763         "alg": "EdDSA",
    764         "account_pub": auth_pub1,
    765         "authorization_pub": auth_pub1,
    766         "authorization_sig": eddsa_sign(&key_pair1, auth_pub1.as_ref()),
    767         "recurrent": false
    768     });
    769 
    770     /* ----- Registration ----- */
    771     let routine = async |ty: TransferType,
    772                          account_pub: &EddsaPublicKey,
    773                          recurrent: bool,
    774                          fmt: IncomingType| {
    775         let req = json!(req + {
    776             "type": ty,
    777             "account_pub": account_pub,
    778             "authorization_pub": auth_pub1,
    779             "authorization_sig": eddsa_sign(&key_pair1, account_pub.as_ref()),
    780             "recurrent": recurrent
    781         });
    782         // Valid
    783         let res = server
    784             .post("/taler-wire-transfer-gateway/registration")
    785             .json(&req)
    786             .await
    787             .assert_ok_json::<RegistrationResponse>();
    788 
    789         // Idempotent
    790         assert_eq!(
    791             res,
    792             server
    793                 .post("/taler-wire-transfer-gateway/registration")
    794                 .json(&req)
    795                 .await
    796                 .assert_ok_json::<RegistrationResponse>()
    797         );
    798 
    799         assert!(!res.subjects.is_empty());
    800 
    801         for sub in res.subjects {
    802             if let TransferSubject::Simple { subject, .. } = sub {
    803                 assert_eq!(subject, fmt_in_subject(fmt, &auth_pub1));
    804             };
    805         }
    806     };
    807     for ty in [TransferType::reserve, TransferType::kyc] {
    808         routine(ty, &auth_pub1, false, ty.into()).await;
    809         routine(ty, &auth_pub1, true, IncomingType::map).await;
    810     }
    811 
    812     let acc_pub1 = EddsaPublicKey::rand();
    813     for ty in [TransferType::reserve, TransferType::kyc] {
    814         routine(ty, &acc_pub1, false, IncomingType::map).await;
    815         routine(ty, &acc_pub1, true, IncomingType::map).await;
    816     }
    817 
    818     // Bad signature
    819     server
    820         .post("/taler-wire-transfer-gateway/registration")
    821         .json(&json!(req + {
    822             "authorization_sig": eddsa_sign(&key_pair1, "lol".as_bytes()),
    823         }))
    824         .await
    825         .assert_error(ErrorCode::BANK_BAD_SIGNATURE);
    826 
    827     // Reserve pub reuse
    828     server
    829         .post("/taler-wire-transfer-gateway/registration")
    830         .json(&json!(req + {
    831            "account_pub": acc_pub1,
    832             "authorization_sig": eddsa_sign(&key_pair1, acc_pub1.as_ref()),
    833         }))
    834         .await
    835         .assert_ok_json::<RegistrationResponse>();
    836     {
    837         let key_pair = Ed25519KeyPair::generate().unwrap();
    838         let auth_pub = EddsaPublicKey::try_from(key_pair.public_key().as_ref()).unwrap();
    839         server
    840             .post("/taler-wire-transfer-gateway/registration")
    841             .json(&json!(req + {
    842                 "account_pub": acc_pub1,
    843                 "authorization_pub": auth_pub,
    844                 "authorization_sig": eddsa_sign(&key_pair, acc_pub1.as_ref()),
    845             }))
    846             .await
    847             .assert_error(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT);
    848     }
    849 
    850     // Non recurrent accept one then bounce
    851     server
    852         .post("/taler-wire-transfer-gateway/registration")
    853         .json(&json!(req + {
    854             "account_pub": acc_pub1,
    855             "authorization_sig": eddsa_sign(&key_pair1, acc_pub1.as_ref()),
    856         }))
    857         .await
    858         .assert_ok_json::<RegistrationResponse>();
    859     check_history_in_trigger(server, async || register(&auth_pub1).await).await;
    860     check_in(&[Reserve(acc_pub1.clone())]).await;
    861     register(&auth_pub1).await;
    862     check_in(&[Reserve(acc_pub1.clone()), Bounced]).await;
    863 
    864     // Again without using mapping
    865     let acc_pub2 = EddsaPublicKey::rand();
    866     server
    867         .post("/taler-wire-transfer-gateway/registration")
    868         .json(&json!(req + {
    869             "account_pub": acc_pub2,
    870             "authorization_sig": eddsa_sign(&key_pair1, acc_pub2.as_ref()),
    871         }))
    872         .await
    873         .assert_ok_json::<RegistrationResponse>();
    874     server
    875         .post("/taler-wire-gateway/admin/add-incoming")
    876         .json(&json!({
    877             "amount": amount,
    878             "reserve_pub": acc_pub2,
    879             "debit_account": account,
    880         }))
    881         .await
    882         .assert_ok();
    883     register(&auth_pub1).await;
    884     check_in(&[
    885         Reserve(acc_pub1.clone()),
    886         Bounced,
    887         Reserve(acc_pub2.clone()),
    888         Bounced,
    889     ])
    890     .await;
    891 
    892     // Recurrent accept one and delay others
    893     let acc_pub3 = EddsaPublicKey::rand();
    894     server
    895         .post("/taler-wire-transfer-gateway/registration")
    896         .json(&json!(req + {
    897             "account_pub": acc_pub3,
    898             "authorization_sig": eddsa_sign(&key_pair1, acc_pub3.as_ref()),
    899             "recurrent": true
    900         }))
    901         .await
    902         .assert_ok_json::<RegistrationResponse>();
    903     for _ in 0..5 {
    904         register(&auth_pub1).await;
    905     }
    906     check_in(&[
    907         Reserve(acc_pub1.clone()),
    908         Bounced,
    909         Reserve(acc_pub2.clone()),
    910         Bounced,
    911         Reserve(acc_pub3.clone()),
    912         Pending,
    913         Pending,
    914         Pending,
    915         Pending,
    916     ])
    917     .await;
    918 
    919     // Complete pending on recurrent update
    920     let acc_pub4 = EddsaPublicKey::rand();
    921     server
    922         .post("/taler-wire-transfer-gateway/registration")
    923         .json(&json!(req + {
    924             "type": "kyc",
    925             "account_pub": acc_pub4,
    926             "authorization_sig": eddsa_sign(&key_pair1, acc_pub4.as_ref()),
    927             "recurrent": true
    928         }))
    929         .await
    930         .assert_ok_json::<RegistrationResponse>();
    931     check_history_in_trigger(server, async || {
    932         server
    933             .post("/taler-wire-transfer-gateway/registration")
    934             .json(&json!(req + {
    935                 "account_pub": acc_pub4,
    936                 "authorization_sig": eddsa_sign(&key_pair1, acc_pub4.as_ref()),
    937                 "recurrent": true
    938             }))
    939             .await
    940             .assert_ok_json::<RegistrationResponse>();
    941     })
    942     .await;
    943     check_in(&[
    944         Reserve(acc_pub1.clone()),
    945         Bounced,
    946         Reserve(acc_pub2.clone()),
    947         Bounced,
    948         Reserve(acc_pub3.clone()),
    949         Kyc(acc_pub4.clone()),
    950         Reserve(acc_pub4.clone()),
    951         Pending,
    952         Pending,
    953     ])
    954     .await;
    955 
    956     // Kyc key reuse keep pending ones
    957     server
    958         .post("/taler-wire-gateway/admin/add-kycauth")
    959         .json(&json!({
    960             "amount": amount,
    961             "account_pub": acc_pub4,
    962             "debit_account": account,
    963         }))
    964         .await
    965         .assert_ok_json::<TransferResponse>();
    966     check_in(&[
    967         Reserve(acc_pub1.clone()),
    968         Bounced,
    969         Reserve(acc_pub2.clone()),
    970         Bounced,
    971         Reserve(acc_pub3.clone()),
    972         Kyc(acc_pub4.clone()),
    973         Reserve(acc_pub4.clone()),
    974         Pending,
    975         Pending,
    976         Kyc(acc_pub4.clone()),
    977     ])
    978     .await;
    979 
    980     // Switching to non recurrent cancel pending
    981     let auth_pair = Ed25519KeyPair::generate().unwrap();
    982     let auth_pub2 = EddsaPublicKey::try_from(auth_pair.public_key().as_ref()).unwrap();
    983     server
    984         .post("/taler-wire-transfer-gateway/registration")
    985         .json(&json!(req + {
    986             "account_pub": auth_pub2,
    987             "authorization_pub": auth_pub2,
    988             "authorization_sig": eddsa_sign(&auth_pair, auth_pub2.as_ref()),
    989             "recurrent": true
    990         }))
    991         .await
    992         .assert_ok_json::<RegistrationResponse>();
    993     for _ in 0..3 {
    994         register(&auth_pub2).await;
    995     }
    996     check_in(&[
    997         Reserve(acc_pub1.clone()),
    998         Bounced,
    999         Reserve(acc_pub2.clone()),
   1000         Bounced,
   1001         Reserve(acc_pub3.clone()),
   1002         Kyc(acc_pub4.clone()),
   1003         Reserve(acc_pub4.clone()),
   1004         Pending,
   1005         Pending,
   1006         Kyc(acc_pub4.clone()),
   1007         Reserve(auth_pub2.clone()),
   1008         Pending,
   1009         Pending,
   1010     ])
   1011     .await;
   1012     server
   1013         .post("/taler-wire-transfer-gateway/registration")
   1014         .json(&json!(req + {
   1015             "type": "kyc",
   1016             "account_pub": auth_pub2,
   1017             "authorization_pub": auth_pub2,
   1018             "authorization_sig": eddsa_sign(&auth_pair, auth_pub2.as_ref()),
   1019             "recurrent": false
   1020         }))
   1021         .await
   1022         .assert_ok_json::<RegistrationResponse>();
   1023     check_in(&[
   1024         Reserve(acc_pub1.clone()),
   1025         Bounced,
   1026         Reserve(acc_pub2.clone()),
   1027         Bounced,
   1028         Reserve(acc_pub3.clone()),
   1029         Kyc(acc_pub4.clone()),
   1030         Reserve(acc_pub4.clone()),
   1031         Pending,
   1032         Pending,
   1033         Kyc(acc_pub4.clone()),
   1034         Reserve(auth_pub2.clone()),
   1035         Bounced,
   1036         Bounced,
   1037     ])
   1038     .await;
   1039 
   1040     // Recurrent reserve simple subject
   1041     let acc_pub5 = EddsaPublicKey::rand();
   1042     server
   1043         .post("/taler-wire-transfer-gateway/registration")
   1044         .json(&json!(req + {
   1045             "type": "reserve",
   1046             "account_pub": acc_pub5,
   1047             "authorization_pub": auth_pub2,
   1048             "authorization_sig": eddsa_sign(&auth_pair, acc_pub5.as_ref()),
   1049             "recurrent": true
   1050         }))
   1051         .await
   1052         .assert_ok_json::<RegistrationResponse>();
   1053     server
   1054         .post("/taler-wire-gateway/admin/add-incoming")
   1055         .json(&json!({
   1056             "amount": amount,
   1057             "reserve_pub": acc_pub5,
   1058             "debit_account": account,
   1059         }))
   1060         .await
   1061         .assert_ok();
   1062     register(&auth_pub2).await;
   1063     check_in(&[
   1064         Reserve(acc_pub1.clone()),
   1065         Bounced,
   1066         Reserve(acc_pub2.clone()),
   1067         Bounced,
   1068         Reserve(acc_pub3.clone()),
   1069         Kyc(acc_pub4.clone()),
   1070         Reserve(acc_pub4.clone()),
   1071         Pending,
   1072         Pending,
   1073         Kyc(acc_pub4.clone()),
   1074         Reserve(auth_pub2.clone()),
   1075         Bounced,
   1076         Bounced,
   1077         Reserve(acc_pub5.clone()),
   1078         Pending,
   1079     ])
   1080     .await;
   1081 
   1082     // Recurrent kyc simple subject
   1083     server
   1084         .post("/taler-wire-transfer-gateway/registration")
   1085         .json(&json!(req + {
   1086             "type": "kyc",
   1087             "account_pub": acc_pub5,
   1088             "authorization_pub": auth_pub2,
   1089             "authorization_sig": eddsa_sign(&auth_pair, acc_pub5.as_ref()),
   1090             "recurrent": false
   1091         }))
   1092         .await
   1093         .assert_ok_json::<RegistrationResponse>();
   1094     server
   1095         .post("/taler-wire-transfer-gateway/registration")
   1096         .json(&json!(req + {
   1097             "type": "kyc",
   1098             "account_pub": acc_pub5,
   1099             "authorization_pub": auth_pub2,
   1100             "authorization_sig": eddsa_sign(&auth_pair, acc_pub5.as_ref()),
   1101             "recurrent": true
   1102         }))
   1103         .await
   1104         .assert_ok_json::<RegistrationResponse>();
   1105     server
   1106         .post("/taler-wire-gateway/admin/add-kycauth")
   1107         .json(&json!({
   1108             "amount": amount,
   1109             "account_pub": acc_pub5,
   1110             "debit_account": account,
   1111         }))
   1112         .await
   1113         .assert_ok();
   1114     register(&auth_pub2).await;
   1115     register(&auth_pub2).await;
   1116     check_in(&[
   1117         Reserve(acc_pub1.clone()),
   1118         Bounced,
   1119         Reserve(acc_pub2.clone()),
   1120         Bounced,
   1121         Reserve(acc_pub3.clone()),
   1122         Kyc(acc_pub4.clone()),
   1123         Reserve(acc_pub4.clone()),
   1124         Pending,
   1125         Pending,
   1126         Kyc(acc_pub4.clone()),
   1127         Reserve(auth_pub2.clone()),
   1128         Bounced,
   1129         Bounced,
   1130         Reserve(acc_pub5.clone()),
   1131         Bounced,
   1132         Kyc(acc_pub5.clone()),
   1133         Pending,
   1134         Pending,
   1135     ])
   1136     .await;
   1137 
   1138     /* ----- Unregistration ----- */
   1139     let now = Timestamp::now().to_string();
   1140     let un_req = json!({
   1141         "timestamp": now,
   1142         "authorization_pub": auth_pub2,
   1143         "authorization_sig": eddsa_sign(&auth_pair, now.as_bytes()),
   1144     });
   1145 
   1146     // Delete
   1147     server
   1148         .delete("/taler-wire-transfer-gateway/registration")
   1149         .json(&un_req)
   1150         .await
   1151         .assert_no_content();
   1152 
   1153     // Check bounce pending on deletion
   1154 
   1155     check_in(&[
   1156         Reserve(acc_pub1.clone()),
   1157         Bounced,
   1158         Reserve(acc_pub2.clone()),
   1159         Bounced,
   1160         Reserve(acc_pub3.clone()),
   1161         Kyc(acc_pub4.clone()),
   1162         Reserve(acc_pub4.clone()),
   1163         Pending,
   1164         Pending,
   1165         Kyc(acc_pub4.clone()),
   1166         Reserve(auth_pub2.clone()),
   1167         Bounced,
   1168         Bounced,
   1169         Reserve(acc_pub5.clone()),
   1170         Bounced,
   1171         Kyc(acc_pub5.clone()),
   1172         Bounced,
   1173         Bounced,
   1174     ])
   1175     .await;
   1176 
   1177     // Idempotent
   1178     server
   1179         .delete("/taler-wire-transfer-gateway/registration")
   1180         .json(&un_req)
   1181         .await
   1182         .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND);
   1183 
   1184     // Bad signature
   1185     server
   1186         .delete("/taler-wire-transfer-gateway/registration")
   1187         .json(&json!(un_req + {
   1188             "authorization_sig": eddsa_sign(&auth_pair, "lol".as_bytes()),
   1189         }))
   1190         .await
   1191         .assert_error(ErrorCode::BANK_BAD_SIGNATURE);
   1192 
   1193     // Old timestamp
   1194     let now = (Timestamp::now() - SignedDuration::from_mins(10)).to_string();
   1195     server
   1196         .delete("/taler-wire-transfer-gateway/registration")
   1197         .json(&json!({
   1198             "timestamp": now,
   1199             "authorization_pub": auth_pub2,
   1200             "authorization_sig": eddsa_sign(&auth_pair, now.as_bytes()),
   1201         }))
   1202         .await
   1203         .assert_error(ErrorCode::BANK_OLD_TIMESTAMP);
   1204 
   1205     /* ----- API ----- */
   1206 
   1207     let history: Vec<_> = server
   1208         .get("/taler-wire-gateway/history/incoming?limit=20")
   1209         .await
   1210         .assert_ok_json::<IncomingHistory>()
   1211         .incoming_transactions
   1212         .into_iter()
   1213         .map(|tx| {
   1214             let (acc_pub, auth_pub, auth_sig) = match tx {
   1215                 IncomingBankTransaction::Reserve {
   1216                     reserve_pub,
   1217                     authorization_pub,
   1218                     authorization_sig,
   1219                     ..
   1220                 } => (reserve_pub, authorization_pub, authorization_sig),
   1221                 IncomingBankTransaction::Wad { .. } => unreachable!(),
   1222                 IncomingBankTransaction::Kyc {
   1223                     account_pub,
   1224                     authorization_pub,
   1225                     authorization_sig,
   1226                     ..
   1227                 } => (account_pub, authorization_pub, authorization_sig),
   1228             };
   1229             if let Some(auth_pub) = &auth_pub {
   1230                 assert!(check_eddsa_signature(
   1231                     auth_pub,
   1232                     acc_pub.as_ref(),
   1233                     &auth_sig.unwrap()
   1234                 ));
   1235             } else {
   1236                 assert!(auth_sig.is_none())
   1237             }
   1238             (acc_pub, auth_pub)
   1239         })
   1240         .collect();
   1241     dbg!(&history);
   1242     assert_eq!(
   1243         history,
   1244         [
   1245             (acc_pub1.clone(), Some(auth_pub1.clone())),
   1246             (acc_pub2.clone(), None),
   1247             (acc_pub3.clone(), Some(auth_pub1.clone())),
   1248             (acc_pub4.clone(), Some(auth_pub1.clone())),
   1249             (acc_pub4.clone(), Some(auth_pub1.clone())),
   1250             (acc_pub4.clone(), None),
   1251             (auth_pub2.clone(), Some(auth_pub2.clone())),
   1252             (acc_pub5.clone(), None),
   1253             (acc_pub5.clone(), None),
   1254         ]
   1255     )
   1256 }