libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

wire.rs (26202B)


      1 /*
      2 * This file is part of LibEuFin.
      3 * Copyright (C) 2026 Taler Systems S.A.
      4 
      5 * LibEuFin is free software; you can redistribute it and/or modify
      6 * it under the terms of the GNU Affero General Public License as
      7 * published by the Free Software Foundation; either version 3, or
      8 * (at your option) any later version.
      9 
     10 * LibEuFin is distributed in the hope that it will be useful, but
     11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     12 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
     13 * Public License for more details.
     14 
     15 * You should have received a copy of the GNU Affero General Public
     16 * License along with LibEuFin; see the file COPYING.  If not, see
     17 * <http://www.gnu.org/licenses/>
     18 */
     19 
     20 use std::sync::Arc;
     21 
     22 use axum::{
     23     Json, Router,
     24     extract::State,
     25     response::{IntoResponse, NoContent, Response},
     26     routing::{get, post},
     27 };
     28 use jiff::Timestamp;
     29 use serde::Deserialize;
     30 use sqlx::PgPool;
     31 use taler_api::{
     32     api::Validation,
     33     constants::WIRE_GATEWAY_API_VERSION,
     34     error::{ApiResult, failure_code},
     35     extract::{Path, Query, Req},
     36     subject::{IncomingKey, fmt_in_subject},
     37 };
     38 use taler_common::{
     39     api::{
     40         params::{HistoryParams, Page, PageParams},
     41         wire::{
     42             AddIncomingRequest, AddIncomingResponse, AddKycauthRequest, AddMappedRequest,
     43             IncomingHistory, OutgoingHistory, TransferList, TransferRequest, TransferResponse,
     44             TransferState, WireConfig,
     45         },
     46     },
     47     error_code::ErrorCode,
     48     types::{amount::Amount, payto::PaytoURI},
     49 };
     50 
     51 use crate::{
     52     api::{BankState, IMPLEMENTATION},
     53     auth::{UserAdminAuth, WireRAuth, WireRWAuth},
     54     db::{
     55         account::check_info,
     56         exchange::{
     57             AddIncomingResult, TransferResult, add_incoming, incoming_history, outgoing_history,
     58             page_transfer, transfer, transfer_by_id,
     59         },
     60     },
     61     payto::BankPayto,
     62 };
     63 
     64 #[derive(Debug, Clone, Deserialize)]
     65 pub struct TransferParams {
     66     #[serde(flatten)]
     67     pub page: PageParams,
     68     pub status: Option<TransferState>,
     69 }
     70 
     71 impl TransferParams {
     72     pub fn check(self) -> ApiResult<Transfer> {
     73         Ok(Transfer {
     74             page: self.page.check()?,
     75             status: self.status,
     76         })
     77     }
     78 }
     79 
     80 pub struct Transfer {
     81     pub page: Page,
     82     pub status: Option<TransferState>,
     83 }
     84 
     85 #[derive(Debug, Clone, Deserialize)]
     86 pub struct AccountCheckParams {
     87     pub account: BankPayto,
     88 }
     89 
     90 async fn admin_add_incoming(
     91     db: &PgPool,
     92     username: &str,
     93     amount: &Amount,
     94     debtor: &PaytoURI,
     95     metadata: &IncomingKey,
     96 ) -> ApiResult<Response> {
     97     let payto = BankPayto::try_from(debtor)?;
     98     match add_incoming(
     99         db,
    100         amount,
    101         &payto,
    102         &format!(
    103             "Admin incoming {}",
    104             fmt_in_subject(metadata.ty, &metadata.key)
    105         ),
    106         username,
    107         &Timestamp::now(),
    108         metadata,
    109     )
    110     .await?
    111     {
    112         AddIncomingResult::Success { id, timestamp, .. } => Ok(Json(AddIncomingResponse {
    113             row_id: id,
    114             timestamp: timestamp.into(),
    115         })
    116         .into_response()),
    117         AddIncomingResult::NotAnExchange => {
    118             Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE))
    119         }
    120         AddIncomingResult::UnknownExchange => Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT)),
    121         AddIncomingResult::UnknownDebtor => Err(failure_code(ErrorCode::BANK_UNKNOWN_DEBTOR)),
    122         AddIncomingResult::BothPartyAreExchange => {
    123             Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_EXCHANGE))
    124         }
    125         AddIncomingResult::ReservePubReuse => {
    126             Err(failure_code(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT))
    127         }
    128         AddIncomingResult::UnknownMapping => {
    129             Err(failure_code(ErrorCode::BANK_TRANSFER_MAPPING_UNKNOWN))
    130         }
    131         AddIncomingResult::MappingReuse => {
    132             Err(failure_code(ErrorCode::BANK_TRANSFER_MAPPING_REUSED))
    133         }
    134         AddIncomingResult::BalanceInsufficient => {
    135             Err(failure_code(ErrorCode::BANK_UNALLOWED_DEBIT))
    136         }
    137     }
    138 }
    139 
    140 pub fn wire_api() -> Router<Arc<BankState>> {
    141     Router::new()
    142         .route(
    143             "/accounts/{username}/taler-wire-gateway/config",
    144             get(async |State(state): State<Arc<BankState>>| {
    145                 Json(WireConfig {
    146                     name: (),
    147                     version: WIRE_GATEWAY_API_VERSION,
    148                     implementation: Some(IMPLEMENTATION),
    149                     currency: state.cfg.regional_currency,
    150                     support_account_check: true,
    151                 })
    152             }),
    153         )
    154         .route(
    155             "/accounts/{username}/taler-wire-gateway/transfer",
    156             post(
    157                 async |auth: WireRWAuth,
    158                        State(state): State<Arc<BankState>>,
    159                        Req(req): Req<TransferRequest>| {
    160                     req.check(&state.cfg.regional_currency)?;
    161                     let payto = BankPayto::try_from(&req.credit_account)?;
    162                     match transfer(
    163                         &state.db,
    164                         &auth.username,
    165                         &req,
    166                         &payto,
    167                         &Timestamp::now(),
    168                         state.cfg.fiat.is_some(),
    169                     )
    170                     .await?
    171                     {
    172                         TransferResult::Success { id, timestamp } => Ok(Json(TransferResponse {
    173                             timestamp: timestamp.into(),
    174                             row_id: id,
    175                         })),
    176                         TransferResult::NotAnExchange => {
    177                             Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE))
    178                         }
    179                         TransferResult::UnknownExchange => {
    180                             Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT))
    181                         }
    182                         TransferResult::BothPartyAreExchange => {
    183                             Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_EXCHANGE))
    184                         }
    185                         TransferResult::BalanceInsufficient => {
    186                             Err(failure_code(ErrorCode::BANK_UNALLOWED_DEBIT))
    187                         }
    188                         TransferResult::ReserveUidReuse => {
    189                             Err(failure_code(ErrorCode::BANK_TRANSFER_REQUEST_UID_REUSED))
    190                         }
    191                         TransferResult::WtidReuse => {
    192                             Err(failure_code(ErrorCode::BANK_TRANSFER_WTID_REUSED))
    193                         }
    194                         TransferResult::AdminCreditor => {
    195                             Err(failure_code(ErrorCode::BANK_ADMIN_CREDITOR))
    196                         }
    197                     }
    198                 },
    199             ),
    200         )
    201         .route(
    202             "/accounts/{username}/taler-wire-gateway/transfers",
    203             get(
    204                 async |Query(params): Query<TransferParams>,
    205                        mut auth: WireRAuth,
    206                        State(state): State<Arc<BankState>>| {
    207                     let params = params.check()?;
    208                     let info = auth.bank_info(&state.db, &state.cfg.ctx).await?;
    209                     if !info.is_exchange {
    210                         return Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE));
    211                     }
    212                     let transfers = page_transfer(
    213                         &state.db,
    214                         &state.cfg.ctx,
    215                         &state.cfg.regional_currency,
    216                         &params.page,
    217                         info.bank_account_id,
    218                         params.status,
    219                     )
    220                     .await?;
    221                     if transfers.is_empty() {
    222                         ApiResult::Ok(NoContent.into_response())
    223                     } else {
    224                         Ok(Json(TransferList {
    225                             transfers,
    226                             debit_account: info.payto.as_uri(),
    227                         })
    228                         .into_response())
    229                     }
    230                 },
    231             ),
    232         )
    233         .route(
    234             "/accounts/{username}/taler-wire-gateway/transfers/{id}",
    235             get(
    236                 async |Path((_, id)): Path<((), u64)>,
    237                        mut auth: WireRAuth,
    238                        State(state): State<Arc<BankState>>| {
    239                     let info = auth.bank_info(&state.db, &state.cfg.ctx).await?;
    240                     if !info.is_exchange {
    241                         return Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE));
    242                     }
    243                     match transfer_by_id(
    244                         &state.db,
    245                         &state.cfg.ctx,
    246                         &state.cfg.regional_currency,
    247                         info.bank_account_id,
    248                         id,
    249                     )
    250                     .await?
    251                     {
    252                         Some(t) => Ok(Json(t)),
    253                         None => Err(failure_code(ErrorCode::BANK_TRANSACTION_NOT_FOUND)),
    254                     }
    255                 },
    256             ),
    257         )
    258         .route(
    259             "/accounts/{username}/taler-wire-gateway/history/incoming",
    260             get(
    261                 async |Query(params): Query<HistoryParams>,
    262                        mut auth: WireRAuth,
    263                        State(state): State<Arc<BankState>>| {
    264                     let params = params.check()?;
    265                     let info = auth.bank_info(&state.db, &state.cfg.ctx).await?;
    266                     if !info.is_exchange {
    267                         return Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE));
    268                     }
    269                     let incoming_transactions = incoming_history(
    270                         &state.db,
    271                         &state.cfg.ctx,
    272                         &state.taler_in_channel,
    273                         &state.cfg.regional_currency,
    274                         &params,
    275                         info.bank_account_id,
    276                     )
    277                     .await?;
    278                     if incoming_transactions.is_empty() {
    279                         ApiResult::Ok(NoContent.into_response())
    280                     } else {
    281                         Ok(Json(IncomingHistory {
    282                             credit_account: info.payto.as_uri(),
    283                             incoming_transactions,
    284                         })
    285                         .into_response())
    286                     }
    287                 },
    288             ),
    289         )
    290         .route(
    291             "/accounts/{username}/taler-wire-gateway/history/outgoing",
    292             get(
    293                 async |Query(params): Query<HistoryParams>,
    294                        mut auth: WireRAuth,
    295                        State(state): State<Arc<BankState>>| {
    296                     let params = params.check()?;
    297                     let info = auth.bank_info(&state.db, &state.cfg.ctx).await?;
    298                     if !info.is_exchange {
    299                         return Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE));
    300                     }
    301                     let outgoing_transactions = outgoing_history(
    302                         &state.db,
    303                         &state.cfg.ctx,
    304                         &state.taler_out_channel,
    305                         &state.cfg.regional_currency,
    306                         &params,
    307                         info.bank_account_id,
    308                     )
    309                     .await?;
    310                     if outgoing_transactions.is_empty() {
    311                         ApiResult::Ok(NoContent.into_response())
    312                     } else {
    313                         Ok(Json(OutgoingHistory {
    314                             debit_account: info.payto.as_uri(),
    315                             outgoing_transactions,
    316                         })
    317                         .into_response())
    318                     }
    319                 },
    320             ),
    321         )
    322         .route(
    323             "/accounts/{username}/taler-wire-gateway/admin/add-incoming",
    324             post(
    325                 async |auth: UserAdminAuth,
    326                        State(state): State<Arc<BankState>>,
    327                        Req(req): Req<AddIncomingRequest>| {
    328                     req.check(&state.cfg.regional_currency)?;
    329                     admin_add_incoming(
    330                         &state.db,
    331                         &auth.username,
    332                         &req.amount,
    333                         &req.debit_account,
    334                         &IncomingKey::reserve(req.reserve_pub),
    335                     )
    336                     .await
    337                 },
    338             ),
    339         )
    340         .route(
    341             "/accounts/{username}/taler-wire-gateway/admin/add-kycauth",
    342             post(
    343                 async |auth: UserAdminAuth,
    344                        State(state): State<Arc<BankState>>,
    345                        Req(req): Req<AddKycauthRequest>| {
    346                     req.check(&state.cfg.regional_currency)?;
    347                     admin_add_incoming(
    348                         &state.db,
    349                         &auth.username,
    350                         &req.amount,
    351                         &req.debit_account,
    352                         &IncomingKey::kyc(req.account_pub),
    353                     )
    354                     .await
    355                 },
    356             ),
    357         )
    358         .route(
    359             "/accounts/{username}/taler-wire-gateway/admin/add-mapped",
    360             post(
    361                 async |auth: UserAdminAuth,
    362                        State(state): State<Arc<BankState>>,
    363                        Req(req): Req<AddMappedRequest>| {
    364                     req.check(&state.cfg.regional_currency)?;
    365                     admin_add_incoming(
    366                         &state.db,
    367                         &auth.username,
    368                         &req.amount,
    369                         &req.debit_account,
    370                         &IncomingKey::map(req.authorization_pub),
    371                     )
    372                     .await
    373                 },
    374             ),
    375         )
    376         .route(
    377             "/accounts/{username}/taler-wire-gateway/account/check",
    378             get(
    379                 async |Query(params): Query<AccountCheckParams>,
    380                        mut auth: WireRAuth,
    381                        State(state): State<Arc<BankState>>| {
    382                     let info = auth.bank_info(&state.db, &state.cfg.ctx).await?;
    383                     if !info.is_exchange {
    384                         return Err(failure_code(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE));
    385                     }
    386                     match check_info(&state.db, &params.account).await? {
    387                         Some(t) => Ok(Json(t)),
    388                         None => Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT)),
    389                     }
    390                 },
    391             ),
    392         )
    393 }
    394 
    395 #[cfg(test)]
    396 pub mod test {
    397     use axum::http::Method;
    398     use sqlx::postgres::PgConnectOptions;
    399     use taler_api::subject::fmt_out_subject;
    400     use taler_common::{
    401         api::{
    402             EddsaPublicKey, HashCode, ShortHashCode,
    403             wire::{AccountInfo, TransferResponse, TransferState, TransferStatus},
    404         },
    405         db::IncomingType,
    406         error_code::ErrorCode,
    407         types::amount::amount,
    408     };
    409     use taler_macros::db_test;
    410     use taler_test_utils::{
    411         json,
    412         routine::{
    413             admin_add_incoming_routine, in_history_routine, out_history_routine, transfer_routine,
    414         },
    415         server::TestServer as _,
    416         tasks,
    417     };
    418 
    419     use crate::api::test::{Auth, bank_setup};
    420 
    421     #[db_test(raw)]
    422     async fn transfer(db: PgConnectOptions) {
    423         let ctx = bank_setup(db).await;
    424 
    425         ctx.auth_routine(
    426             Method::POST,
    427             "/accounts/merchant/taler-wire-gateway/transfer",
    428             Auth::UserOnly,
    429         )
    430         .await;
    431         ctx.auth_routine(
    432             Method::GET,
    433             "/accounts/merchant/taler-wire-gateway/transfers/32",
    434             Auth::UserOnly,
    435         )
    436         .await;
    437         ctx.auth_routine(
    438             Method::GET,
    439             "/accounts/merchant/taler-wire-gateway/transfers",
    440             Auth::UserOnly,
    441         )
    442         .await;
    443 
    444         let req = json!({
    445             "request_uid": HashCode::rand(),
    446             "amount": "KUDOS:55",
    447             "exchange_base_url": "http://exchange.example.com/",
    448             "wtid": ShortHashCode::rand(),
    449             "credit_account": ctx.merchant_payto
    450         });
    451 
    452         ctx.posta("/accounts/exchange/taler-wire-gateway/transfer")
    453             .json(&req)
    454             .await
    455             .assert_error(ErrorCode::BANK_UNALLOWED_DEBIT);
    456 
    457         ctx.set_max_debt("exchange", "1000").await;
    458         transfer_routine(
    459             &ctx.admin_router()
    460                 .await
    461                 .prefix("/accounts/exchange/taler-wire-gateway"),
    462             TransferState::success,
    463             &ctx.customer_payto.as_uri(),
    464         )
    465         .await;
    466 
    467         let admin_req = json!(req + {
    468             "credit_account" : ctx.admin_payto
    469         });
    470 
    471         // Check conversion bounce
    472         for _ in 0..2 {
    473             ctx.posta("/accounts/exchange/taler-wire-gateway/transfer")
    474                 .json(&admin_req)
    475                 .await
    476                 .assert_ok();
    477         }
    478 
    479         let ctx = ctx.swap_cfg("test_no_conversion.conf").await;
    480         // Transfer works for common accounts
    481 
    482         let req = json!(req + {
    483             "request_uid": HashCode::rand(),
    484             "wtid": ShortHashCode::rand(),
    485         });
    486         for _ in 0..2 {
    487             ctx.posta("/accounts/exchange/taler-wire-gateway/transfer")
    488                 .json(&req)
    489                 .await
    490                 .assert_ok();
    491         }
    492         // But fails to admin accounts
    493         ctx.posta("/accounts/exchange/taler-wire-gateway/transfer")
    494             .json(json!(admin_req + {
    495                 "request_uid": HashCode::rand(),
    496                 "wtid": ShortHashCode::rand(),
    497             }))
    498             .await
    499             .assert_error(ErrorCode::BANK_ADMIN_CREDITOR);
    500 
    501         // Dot now fail for unknown account
    502         let res = ctx
    503             .posta("/accounts/exchange/taler-wire-gateway/transfer")
    504             .json(json!(req + {
    505                 "request_uid": HashCode::rand(),
    506                 "wtid": ShortHashCode::rand(),
    507                 "credit_account": ctx.unknown_payto
    508             }))
    509             .await
    510             .assert_ok_json::<TransferResponse>();
    511         let s: TransferStatus = ctx
    512             .geta(format!(
    513                 "/accounts/exchange/taler-wire-gateway/transfers/{}",
    514                 res.row_id
    515             ))
    516             .await
    517             .assert_ok_json();
    518         assert_eq!(
    519             TransferStatus {
    520                 status: TransferState::permanent_failure,
    521                 status_msg: Some("Unknown account".into()),
    522                 amount: amount("KUDOS:55"),
    523                 exchange_base_url: s.exchange_base_url.clone(),
    524                 metadata: None,
    525                 wtid: s.wtid,
    526                 credit_account: s.credit_account.clone(),
    527                 timestamp: s.timestamp,
    528             },
    529             s
    530         );
    531 
    532         // Not an exchange
    533         ctx.geta(format!(
    534             "/accounts/merchant/taler-wire-gateway/transfers/{}",
    535             res.row_id
    536         ))
    537         .await
    538         .assert_error(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE);
    539         // Another account
    540         ctx.patcha("/accounts/merchant")
    541             .json(json!({
    542                 "is_taler_exchange": true
    543             }))
    544             .await
    545             .assert_no_content();
    546         ctx.geta(format!(
    547             "/accounts/merchant/taler-wire-gateway/transfers/{}",
    548             res.row_id
    549         ))
    550         .await
    551         .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND);
    552     }
    553 
    554     #[db_test(raw)]
    555     async fn outgoing_history(db: PgConnectOptions) {
    556         let ctx = &bank_setup(db).await;
    557         ctx.auth_routine(
    558             Method::GET,
    559             "/accounts/merchant/taler-wire-gateway/history/outgoing",
    560             Auth::UserOnly,
    561         )
    562         .await;
    563         ctx.set_max_debt("exchange", "1000").await;
    564         out_history_routine(
    565             &ctx.admin_router()
    566                 .await
    567                 .prefix("/accounts/exchange/taler-wire-gateway"),
    568             tasks!(
    569                 // Transactions using clean add incoming logic
    570                 { ctx.transfer("10", &ctx.customer_payto, None).await },
    571                 // And with metadata
    572                 {
    573                     ctx.transfer("12", &ctx.customer_payto, Some("CON:ID".into()))
    574                         .await
    575                 }
    576             ),
    577             tasks!(
    578                 // // Failed transfer
    579                 { ctx.transfer("10", &ctx.unknown_payto, None).await },
    580                 // Ignore manual outgoing transaction
    581                 {
    582                     ctx.tx_s(
    583                         "exchange",
    584                         "10",
    585                         "merchant",
    586                         fmt_out_subject(
    587                             &ShortHashCode::rand(),
    588                             "https://exchange.example.com/",
    589                             None,
    590                         ),
    591                     )
    592                     .await
    593                 },
    594                 // Ignore malformed incoming transaction
    595                 { ctx.tx("merchant", "10", "exchange",).await },
    596                 // Ignore malformed outgoing transaction
    597                 { ctx.tx("exchange", "10", "merchant",).await },
    598             ),
    599         )
    600         .await;
    601     }
    602 
    603     #[db_test(raw)]
    604     async fn incoming_history(db: PgConnectOptions) {
    605         let ctx = &bank_setup(db).await;
    606         ctx.auth_routine(
    607             Method::GET,
    608             "/accounts/merchant/taler-wire-gateway/history/incoming",
    609             Auth::UserOnly,
    610         )
    611         .await;
    612         ctx.set_max_debt("customer", "1000").await;
    613         ctx.set_max_debt("merchant", "1000").await;
    614         let admin = ctx.admin_router().await;
    615         in_history_routine(
    616             &admin.prefix("/accounts/exchange/taler-wire-gateway"),
    617             &admin.prefix("/taler-prepared-transfer"),
    618             &ctx.customer_payto.as_uri(),
    619             &ctx.exchange_payto.as_uri(),
    620             tasks!(
    621                 // Reserve transactions using raw bank transaction logic
    622                 {
    623                     ctx.tx_s(
    624                         "merchant",
    625                         "10",
    626                         "exchange",
    627                         format!("history test with {} reserve pub", EddsaPublicKey::rand()),
    628                     )
    629                     .await
    630                 },
    631                 // Reserve transactions using withdraw logic
    632                 { ctx.withdrawal("9").await },
    633                 // KYC transactions using raw bank transaction logic
    634                 {
    635                     ctx.tx_s(
    636                         "merchant",
    637                         "10",
    638                         "exchange",
    639                         format!(
    640                             "history test with KYC:{} account pub",
    641                             EddsaPublicKey::rand()
    642                         ),
    643                     )
    644                     .await
    645                 }
    646             ),
    647             tasks!(
    648                 // Ignore malformed incoming transaction
    649                 { ctx.tx("merchant", "10", "exchange",).await },
    650                 // Ignore malformed outgoing transaction
    651                 { ctx.tx("exchange", "10", "merchant",).await },
    652             ),
    653         )
    654         .await;
    655     }
    656 
    657     #[db_test(raw)]
    658     async fn admin_add_incoming(db: PgConnectOptions) {
    659         let ctx = bank_setup(db).await;
    660         ctx.auth_routine(
    661             Method::GET,
    662             "/accounts/merchant/taler-wire-gateway/history/incoming",
    663             Auth::UserOnly,
    664         )
    665         .await;
    666         for ty in IncomingType::entries {
    667             let (path, key) = match ty {
    668                 IncomingType::reserve => ("incoming", "reserve_pub"),
    669                 IncomingType::kyc => ("kycauth", "account_pub"),
    670                 IncomingType::map => ("mapped", "authorization_pub"),
    671             };
    672             let path = format!("/accounts/exchange/taler-wire-gateway/admin/add-{path}");
    673             ctx.auth_routine(Method::POST, &path, Auth::Admin).await;
    674             let req = json!({
    675                 "amount": "KUDOS:44",
    676                 "debit_account": ctx.customer_payto,
    677                 key: EddsaPublicKey::rand(),
    678             });
    679 
    680             ctx.post_admin(path.replace("exchange", "merchant"))
    681                 .json(&req)
    682                 .await
    683                 .assert_error(ErrorCode::BANK_ACCOUNT_IS_NOT_EXCHANGE);
    684             ctx.post_admin(&path)
    685                 .json(json!(req + {
    686                     "debit_account": ctx.exchange_payto
    687                 }))
    688                 .await
    689                 .assert_error(ErrorCode::BANK_ACCOUNT_IS_EXCHANGE);
    690             if *ty != IncomingType::map {
    691                 ctx.post_admin(&path)
    692                     .json(json!(req + {
    693                          "amount": "KUDOS:44",
    694                     }))
    695                     .await
    696                     .assert_error(ErrorCode::BANK_UNALLOWED_DEBIT);
    697             }
    698         }
    699         ctx.set_max_debt("customer", "1000").await;
    700         let admin = ctx.admin_router().await;
    701         admin_add_incoming_routine(
    702             &admin.prefix("/accounts/exchange/taler-wire-gateway"),
    703             &admin.prefix("/taler-prepared-transfer"),
    704             &ctx.customer_payto.as_uri(),
    705             &ctx.exchange_payto.as_uri(),
    706         )
    707         .await;
    708     }
    709 
    710     #[db_test(raw)]
    711     async fn account_check(db: PgConnectOptions) {
    712         let ctx = bank_setup(db).await;
    713         ctx.geta("/accounts/exchange/taler-wire-gateway/account/check")
    714             .await
    715             .assert_error(ErrorCode::GENERIC_PARAMETER_MISSING);
    716         ctx.geta(format!(
    717             "/accounts/exchange/taler-wire-gateway/account/check?account={}",
    718             ctx.unknown_payto
    719         ))
    720         .await
    721         .assert_error(ErrorCode::BANK_UNKNOWN_ACCOUNT);
    722         ctx.geta(format!(
    723             "/accounts/exchange/taler-wire-gateway/account/check?account={}",
    724             ctx.merchant_payto
    725         ))
    726         .await
    727         .assert_ok_json::<AccountInfo>();
    728     }
    729 }