taler-rust

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

api.rs (22433B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 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 compact_str::CompactString;
     18 use jiff::Timestamp;
     19 use prometheus_client::{
     20     encoding::EncodeLabelSet,
     21     metrics::{family::Family, gauge::Gauge},
     22     registry::{Registry, Unit},
     23 };
     24 use sqlx::PgPool;
     25 use taler_api::{
     26     api::{
     27         TalerApi, observability::Observability, prepared::PreparedTransfer, revenue::Revenue,
     28         wire::WireGateway,
     29     },
     30     error::{ApiResult, failure_code},
     31     subject::{IncomingKey, fmt_in_subject},
     32 };
     33 use taler_common::{
     34     api::{
     35         params::{History, Page},
     36         prepared::{
     37             RegistrationRequest, RegistrationResponse, SubjectFormat, TransferSubject,
     38             Unregistration,
     39         },
     40         revenue::RevenueIncomingHistory,
     41         wire::{
     42             AddIncomingRequest, AddIncomingResponse, AddKycauthRequest, AddMappedRequest,
     43             IncomingHistory, OutgoingHistory, TransferList, TransferRequest, TransferResponse,
     44             TransferState, TransferStatus,
     45         },
     46     },
     47     db::IncomingType,
     48     error_code::ErrorCode,
     49     types::{amount::Currency, time::TalerTimestamp},
     50 };
     51 use tokio::sync::watch::Sender;
     52 
     53 use crate::{
     54     db::{self, AddIncomingResult, Transfer, TxInAdmin},
     55     payto::{CyclosPayto, FullCyclosPayto},
     56 };
     57 
     58 pub struct CyclosApi {
     59     pub pool: sqlx::PgPool,
     60     pub currency: Currency,
     61     pub payto: FullCyclosPayto,
     62     pub in_channel: Sender<i64>,
     63     pub taler_in_channel: Sender<i64>,
     64     pub out_channel: Sender<i64>,
     65     pub taler_out_channel: Sender<i64>,
     66     pub root: CompactString,
     67     metrics: Metrics,
     68     registry: Registry,
     69 }
     70 
     71 #[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)]
     72 struct TaskLabel {
     73     name: &'static str,
     74 }
     75 
     76 #[derive(Default)]
     77 struct Metrics {
     78     db_access: Gauge,
     79     task_execution: Family<TaskLabel, Gauge>,
     80     task_success: Family<TaskLabel, Gauge>,
     81 }
     82 
     83 impl Metrics {
     84     pub fn registry(&self) -> Registry {
     85         let mut registry = Registry::default();
     86 
     87         registry.register(
     88             "db_access",
     89             "Whether the last database metrics refresh succeeded",
     90             self.db_access.clone(),
     91         );
     92         registry.register_with_unit(
     93             "task_execution_timestamp_seconds",
     94             "Unix timestamp of the last task execution",
     95             Unit::Seconds,
     96             self.task_execution.clone(),
     97         );
     98         registry.register_with_unit(
     99             "task_success_timestamp_seconds",
    100             "Unix timestamp of the last successful task execution",
    101             Unit::Seconds,
    102             self.task_success.clone(),
    103         );
    104         registry
    105     }
    106 
    107     pub async fn sync(&self, db: &PgPool) {
    108         let result = sqlx::query_as::<_, (String, sqlx::types::Json<db::TaskStatus>)>(
    109             "SELECT key, value FROM kv WHERE key IN ('sync', 'submit')",
    110         )
    111         .fetch_all(db)
    112         .await;
    113         let success = match result {
    114             Ok(rows) => {
    115                 for name in ["sync", "submit"] {
    116                     let label = TaskLabel { name };
    117                     let status = rows.iter().find(|(key, _)| key == name).map(|(_, s)| &s.0);
    118                     if let Some(time) = status.and_then(|s| s.last_execution) {
    119                         self.task_execution
    120                             .get_or_create(&label)
    121                             .set(time.as_second());
    122                     } else {
    123                         self.task_execution.remove(&label);
    124                     }
    125                     if let Some(time) = status.and_then(|s| s.last_success) {
    126                         self.task_success
    127                             .get_or_create(&label)
    128                             .set(time.as_second());
    129                     } else {
    130                         self.task_success.remove(&label);
    131                     }
    132                 }
    133                 true
    134             }
    135             Err(_) => false,
    136         };
    137         self.db_access.set(if success { 1 } else { 0 });
    138     }
    139 }
    140 
    141 impl CyclosApi {
    142     pub fn start(
    143         pool: sqlx::PgPool,
    144         root: CompactString,
    145         payto: FullCyclosPayto,
    146         currency: Currency,
    147     ) -> Self {
    148         let in_channel = Sender::new(0);
    149         let taler_in_channel = Sender::new(0);
    150         let out_channel = Sender::new(0);
    151         let taler_out_channel = Sender::new(0);
    152 
    153         let metrics = Metrics::default();
    154 
    155         let tmp = Self {
    156             pool: pool.clone(),
    157             payto,
    158             currency,
    159             root,
    160             in_channel: in_channel.clone(),
    161             taler_in_channel: taler_in_channel.clone(),
    162             out_channel: out_channel.clone(),
    163             taler_out_channel: taler_out_channel.clone(),
    164             registry: metrics.registry(),
    165             metrics,
    166         };
    167         tokio::spawn(db::notification_listener(
    168             pool,
    169             in_channel,
    170             taler_in_channel,
    171             out_channel,
    172             taler_out_channel,
    173         ));
    174         tmp
    175     }
    176 }
    177 
    178 impl TalerApi for CyclosApi {
    179     fn currency(&self) -> Currency {
    180         self.currency
    181     }
    182 
    183     fn implementation(&self) -> &'static str {
    184         "urn:net:taler:specs:taler-cyclos:taler-rust"
    185     }
    186 }
    187 
    188 impl WireGateway for CyclosApi {
    189     async fn transfer(&self, req: TransferRequest) -> ApiResult<TransferResponse> {
    190         let creditor = FullCyclosPayto::try_from(&req.credit_account)?;
    191         let result = db::make_transfer(
    192             &self.pool,
    193             &Transfer {
    194                 request_uid: req.request_uid,
    195                 amount: req.amount.decimal(),
    196                 exchange_base_url: req.exchange_base_url,
    197                 metadata: req.metadata,
    198                 wtid: req.wtid,
    199                 creditor_id: *creditor.id,
    200                 creditor_name: creditor.name,
    201             },
    202             &Timestamp::now(),
    203         )
    204         .await?;
    205         match result {
    206             db::TransferResult::Success { id, initiated_at } => Ok(TransferResponse {
    207                 timestamp: initiated_at.into(),
    208                 row_id: id,
    209             }),
    210             db::TransferResult::RequestUidReuse => {
    211                 Err(failure_code(ErrorCode::BANK_TRANSFER_REQUEST_UID_REUSED))
    212             }
    213             db::TransferResult::WtidReuse => {
    214                 Err(failure_code(ErrorCode::BANK_TRANSFER_WTID_REUSED))
    215             }
    216         }
    217     }
    218 
    219     async fn transfer_page(
    220         &self,
    221         page: Page,
    222         status: Option<TransferState>,
    223     ) -> ApiResult<TransferList> {
    224         Ok(TransferList {
    225             transfers: db::transfer_page(&self.pool, &status, &self.currency, &self.root, &page)
    226                 .await?,
    227             debit_account: self.payto.as_uri(),
    228         })
    229     }
    230 
    231     async fn transfer_by_id(&self, id: u64) -> ApiResult<Option<TransferStatus>> {
    232         Ok(db::transfer_by_id(&self.pool, id, &self.currency, &self.root).await?)
    233     }
    234 
    235     async fn outgoing_history(&self, params: History) -> ApiResult<OutgoingHistory> {
    236         Ok(OutgoingHistory {
    237             outgoing_transactions: db::outgoing_history(
    238                 &self.pool,
    239                 &params,
    240                 &self.currency,
    241                 &self.root,
    242                 || self.taler_out_channel.subscribe(),
    243             )
    244             .await?,
    245             debit_account: self.payto.as_uri(),
    246         })
    247     }
    248 
    249     async fn incoming_history(&self, params: History) -> ApiResult<IncomingHistory> {
    250         Ok(IncomingHistory {
    251             incoming_transactions: db::incoming_history(
    252                 &self.pool,
    253                 &params,
    254                 &self.currency,
    255                 &self.root,
    256                 || self.taler_in_channel.subscribe(),
    257             )
    258             .await?,
    259             credit_account: self.payto.as_uri(),
    260         })
    261     }
    262 
    263     async fn add_incoming_reserve(
    264         &self,
    265         req: AddIncomingRequest,
    266     ) -> ApiResult<AddIncomingResponse> {
    267         let debtor = FullCyclosPayto::try_from(&req.debit_account)?;
    268         let res = db::register_tx_in_admin(
    269             &self.pool,
    270             &TxInAdmin {
    271                 amount: req.amount.decimal(),
    272                 subject: format!("Admin incoming {}", req.reserve_pub),
    273                 debtor_id: *debtor.id,
    274                 debtor_name: debtor.name,
    275                 metadata: IncomingKey::reserve(req.reserve_pub),
    276             },
    277             &Timestamp::now(),
    278         )
    279         .await?;
    280         match res {
    281             AddIncomingResult::Success {
    282                 row_id, valued_at, ..
    283             } => Ok(AddIncomingResponse {
    284                 row_id,
    285                 timestamp: valued_at.into(),
    286             }),
    287             AddIncomingResult::ReservePubReuse => {
    288                 Err(failure_code(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT))
    289             }
    290             AddIncomingResult::UnknownMapping | AddIncomingResult::MappingReuse => {
    291                 unreachable!("mapping unused")
    292             }
    293         }
    294     }
    295 
    296     async fn add_incoming_kyc(&self, req: AddKycauthRequest) -> ApiResult<AddIncomingResponse> {
    297         let debtor = FullCyclosPayto::try_from(&req.debit_account)?;
    298         let res = db::register_tx_in_admin(
    299             &self.pool,
    300             &TxInAdmin {
    301                 amount: req.amount.decimal(),
    302                 subject: format!("Admin incoming KYC:{}", req.account_pub),
    303                 debtor_id: *debtor.id,
    304                 debtor_name: debtor.name,
    305                 metadata: IncomingKey::kyc(req.account_pub),
    306             },
    307             &Timestamp::now(),
    308         )
    309         .await?;
    310         match res {
    311             AddIncomingResult::Success {
    312                 row_id, valued_at, ..
    313             } => Ok(AddIncomingResponse {
    314                 row_id,
    315                 timestamp: valued_at.into(),
    316             }),
    317             AddIncomingResult::ReservePubReuse => {
    318                 Err(failure_code(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT))
    319             }
    320             AddIncomingResult::UnknownMapping | AddIncomingResult::MappingReuse => {
    321                 unreachable!("mapping unused")
    322             }
    323         }
    324     }
    325 
    326     async fn add_incoming_mapped(&self, req: AddMappedRequest) -> ApiResult<AddIncomingResponse> {
    327         let debtor = FullCyclosPayto::try_from(&req.debit_account)?;
    328         let res = db::register_tx_in_admin(
    329             &self.pool,
    330             &TxInAdmin {
    331                 amount: req.amount.decimal(),
    332                 subject: format!("Admin incoming MAP:{}", req.authorization_pub),
    333                 debtor_id: *debtor.id,
    334                 debtor_name: debtor.name,
    335                 metadata: IncomingKey::map(req.authorization_pub),
    336             },
    337             &Timestamp::now(),
    338         )
    339         .await?;
    340         match res {
    341             AddIncomingResult::Success {
    342                 row_id, valued_at, ..
    343             } => Ok(AddIncomingResponse {
    344                 row_id,
    345                 timestamp: valued_at.into(),
    346             }),
    347             AddIncomingResult::ReservePubReuse => {
    348                 Err(failure_code(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT))
    349             }
    350             AddIncomingResult::UnknownMapping => {
    351                 Err(failure_code(ErrorCode::BANK_TRANSFER_MAPPING_UNKNOWN))
    352             }
    353             AddIncomingResult::MappingReuse => {
    354                 Err(failure_code(ErrorCode::BANK_TRANSFER_MAPPING_REUSED))
    355             }
    356         }
    357     }
    358 
    359     fn support_account_check(&self) -> bool {
    360         false
    361     }
    362 }
    363 
    364 impl Revenue for CyclosApi {
    365     async fn history(&self, params: History) -> ApiResult<RevenueIncomingHistory> {
    366         Ok(RevenueIncomingHistory {
    367             incoming_transactions: db::revenue_history(
    368                 &self.pool,
    369                 &params,
    370                 &self.currency,
    371                 &self.root,
    372                 || self.in_channel.subscribe(),
    373             )
    374             .await?,
    375             credit_account: self.payto.as_uri(),
    376         })
    377     }
    378 }
    379 
    380 impl PreparedTransfer for CyclosApi {
    381     fn supported_formats(&self) -> &[SubjectFormat] {
    382         &[SubjectFormat::SIMPLE]
    383     }
    384 
    385     async fn registration(&self, req: RegistrationRequest) -> ApiResult<RegistrationResponse> {
    386         let creditor = CyclosPayto::try_from(&req.credit_account)?;
    387         if *creditor != *self.payto {
    388             return Err(failure_code(ErrorCode::BANK_UNKNOWN_CREDITOR));
    389         }
    390         match db::transfer_register(&self.pool, &req).await? {
    391             db::RegistrationResult::Success => {
    392                 let simple = TransferSubject::Simple {
    393                     credit_amount: req.credit_amount,
    394                     subject: if req.authorization_pub == req.account_pub && !req.recurrent {
    395                         fmt_in_subject(req.r#type.into(), &req.account_pub).to_string()
    396                     } else {
    397                         fmt_in_subject(IncomingType::map, &req.authorization_pub).to_string()
    398                     },
    399                 };
    400                 ApiResult::Ok(RegistrationResponse {
    401                     subjects: vec![simple],
    402                     expiration: TalerTimestamp::Never,
    403                 })
    404             }
    405             db::RegistrationResult::ReservePubReuse => {
    406                 ApiResult::Err(failure_code(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT))
    407             }
    408         }
    409     }
    410 
    411     async fn unregistration(&self, req: Unregistration) -> ApiResult<bool> {
    412         Ok(db::transfer_unregister(&self.pool, &req).await?)
    413     }
    414 }
    415 
    416 impl Observability for CyclosApi {
    417     async fn metrics(&self) -> ApiResult<&Registry> {
    418         self.metrics.sync(&self.pool).await;
    419         Ok(&self.registry)
    420     }
    421 }
    422 
    423 #[cfg(test)]
    424 mod test {
    425     use std::sync::{
    426         Arc, LazyLock,
    427         atomic::{AtomicI64, Ordering},
    428     };
    429 
    430     use compact_str::CompactString;
    431     use jiff::Timestamp;
    432     use sqlx::{PgPool, Row as _, postgres::PgRow};
    433     use taler_api::{
    434         api::TalerRouter as _,
    435         auth::AuthMethod,
    436         db::TypeHelper as _,
    437         subject::{IncomingKey, OutgoingSubject},
    438     };
    439     use taler_common::{
    440         api::{
    441             EddsaPublicKey,
    442             observability::Config,
    443             prepared::PreparedTransferConfig,
    444             revenue::RevenueConfig,
    445             wire::{TransferState, WireConfig},
    446         },
    447         db::IncomingType,
    448         types::{
    449             amount::{Currency, decimal},
    450             payto::{PaytoURI, payto},
    451         },
    452     };
    453     use taler_macros::db_test;
    454     use taler_test_utils::{
    455         Router,
    456         routine::{
    457             Status, admin_add_incoming_routine, in_history_routine, out_history_routine,
    458             registration_routine, revenue_routine, transfer_routine,
    459         },
    460         server::TestServer as _,
    461         tasks,
    462     };
    463 
    464     use crate::{
    465         api::CyclosApi,
    466         db::{self, TxIn, TxOutKind},
    467         payto::{FullCyclosPayto, cyclos_payto},
    468     };
    469 
    470     static PAYTO: LazyLock<FullCyclosPayto> = LazyLock::new(|| {
    471         cyclos_payto("payto://cyclos/localhost/7762070814178012479?receiver-name=Smith")
    472     });
    473     static EXCHANGE: LazyLock<PaytoURI> = LazyLock::new(|| PAYTO.as_uri());
    474     static UNKNOWN: LazyLock<PaytoURI> = LazyLock::new(|| {
    475         payto("payto://cyclos/localhost/7762070814178012478?receiver-name=Unknown")
    476     });
    477 
    478     async fn setup(pool: &PgPool) -> Router {
    479         let api = Arc::new(CyclosApi::start(
    480             pool.clone(),
    481             CompactString::const_new("localhost"),
    482             PAYTO.clone(),
    483             Currency::TEST,
    484         ));
    485         Router::new()
    486             .wire_gateway(api.clone(), AuthMethod::None)
    487             .prepared_transfer(api.clone())
    488             .revenue(api.clone(), AuthMethod::None)
    489             .observability(api, AuthMethod::None)
    490             .finalize()
    491     }
    492 
    493     #[db_test]
    494     async fn config(pool: PgPool) {
    495         let server = setup(&pool).await;
    496         server
    497             .get("/taler-wire-gateway/config")
    498             .await
    499             .assert_ok_json::<WireConfig>();
    500         server
    501             .get("/taler-prepared-transfer/config")
    502             .await
    503             .assert_ok_json::<PreparedTransferConfig>();
    504         server
    505             .get("/taler-revenue/config")
    506             .await
    507             .assert_ok_json::<RevenueConfig>();
    508         server
    509             .get("/taler-observability/config")
    510             .await
    511             .assert_ok_json::<Config>();
    512         server.get("/taler-observability/metrics").await.assert_ok();
    513     }
    514 
    515     #[db_test]
    516     async fn transfer(pool: PgPool) {
    517         let server = setup(&pool).await;
    518         transfer_routine(
    519             &server.prefix("/taler-wire-gateway"),
    520             TransferState::pending,
    521             &EXCHANGE,
    522         )
    523         .await;
    524     }
    525 
    526     static CODE: AtomicI64 = AtomicI64::new(0);
    527 
    528     async fn r#in(db: &PgPool, subject: Option<IncomingKey>) {
    529         let now = Timestamp::now();
    530         db::register_tx_in(
    531             &mut db.acquire().await.unwrap(),
    532             &TxIn {
    533                 transfer_id: CODE.fetch_add(1, Ordering::Relaxed),
    534                 tx_id: None,
    535                 amount: decimal("10"),
    536                 subject: "subject".to_owned(),
    537                 debtor_id: 31000163100000000,
    538                 debtor_name: "Name".into(),
    539                 valued_at: Timestamp::now(),
    540             },
    541             &subject,
    542             &now,
    543         )
    544         .await
    545         .unwrap();
    546     }
    547 
    548     async fn in_malformed(db: &PgPool) {
    549         r#in(db, None).await
    550     }
    551 
    552     async fn in_talerable(db: &PgPool) {
    553         r#in(db, Some(IncomingKey::reserve(EddsaPublicKey::rand()))).await
    554     }
    555 
    556     async fn out(db: &PgPool, kind: &TxOutKind) {
    557         let i = CODE.fetch_add(1, Ordering::Relaxed);
    558         let now = Timestamp::now();
    559         db::register_tx_out(
    560             &mut db.acquire().await.unwrap(),
    561             &db::TxOut {
    562                 transfer_id: i,
    563                 tx_id: if i % 2 == 0 { Some(i % 2) } else { None },
    564                 amount: decimal("10"),
    565                 subject: "subject".to_owned(),
    566                 creditor_id: 31000163100000000,
    567                 creditor_name: "Name".into(),
    568                 valued_at: now,
    569             },
    570             kind,
    571             &now,
    572         )
    573         .await
    574         .unwrap();
    575     }
    576 
    577     async fn out_talerable(db: &PgPool) {
    578         out(db, &TxOutKind::Talerable(OutgoingSubject::rand())).await
    579     }
    580 
    581     async fn out_bounce(db: &PgPool) {
    582         out(db, &TxOutKind::Bounce(CODE.load(Ordering::Relaxed))).await
    583     }
    584 
    585     async fn out_malformed(db: &PgPool) {
    586         out(db, &TxOutKind::Simple).await
    587     }
    588 
    589     #[db_test]
    590     async fn outgoing_history(db: PgPool) {
    591         let db = &db;
    592         let server = setup(db).await;
    593 
    594         out_history_routine(
    595             &server.prefix("/taler-wire-gateway"),
    596             tasks!({ out_talerable(db).await }),
    597             tasks!(
    598                 { out_bounce(db).await },
    599                 { out_malformed(db).await },
    600                 { in_malformed(db).await },
    601                 { in_talerable(db).await }
    602             ),
    603         )
    604         .await;
    605     }
    606 
    607     #[db_test]
    608     async fn admin_add_incoming(db: PgPool) {
    609         let server = setup(&db).await;
    610         admin_add_incoming_routine(
    611             &server.prefix("/taler-wire-gateway"),
    612             &server.prefix("/taler-prepared-transfer"),
    613             &EXCHANGE,
    614             &EXCHANGE,
    615         )
    616         .await;
    617     }
    618 
    619     #[db_test]
    620     async fn in_history(db: PgPool) {
    621         let db = &db;
    622         let server = setup(db).await;
    623         in_history_routine(
    624             &server.prefix("/taler-wire-gateway"),
    625             &server.prefix("/taler-prepared-transfer"),
    626             &EXCHANGE,
    627             &EXCHANGE,
    628             tasks!({ in_talerable(db).await }),
    629             tasks!(
    630                 { out_malformed(db).await },
    631                 { out_talerable(db).await },
    632                 { out_bounce(db).await },
    633                 { in_malformed(db).await }
    634             ),
    635         )
    636         .await;
    637     }
    638 
    639     #[db_test]
    640     async fn revenue(db: PgPool) {
    641         let db = &db;
    642         let server = setup(db).await;
    643         revenue_routine(
    644             &server.prefix("/taler-wire-gateway"),
    645             &server.prefix("/taler-revenue"),
    646             &EXCHANGE,
    647             tasks!({ in_malformed(db).await }, { in_talerable(db).await },),
    648             tasks!({ out_malformed(db).await }, { out_talerable(db).await }, {
    649                 out_bounce(db).await
    650             }),
    651         )
    652         .await;
    653     }
    654 
    655     async fn check_in(pool: &PgPool) -> Vec<Status> {
    656         sqlx::query(
    657             "
    658             SELECT pending_recurrent_in.authorization_pub IS NOT NULL, bounced.tx_in_id IS NOT NULL, type, metadata
    659             FROM tx_in
    660                 LEFT JOIN taler_in USING (tx_in_id)
    661                 LEFT JOIN pending_recurrent_in USING (tx_in_id)
    662                 LEFT JOIN bounced USING (tx_in_id)
    663             ORDER BY tx_in.tx_in_id
    664         ",
    665         )
    666         .try_map(|r: PgRow| {
    667             Ok(
    668                 if r.try_get_flag(0)? {
    669                     Status::Pending
    670                 } else if r.try_get_flag(1)? {
    671                     Status::Bounced
    672                 } else {
    673                     match r.try_get(2)? {
    674                         None => Status::Simple,
    675                         Some(IncomingType::reserve) => Status::Reserve(r.try_get(3)?),
    676                         Some(IncomingType::kyc) => Status::Kyc(r.try_get(3)?),
    677                         Some(e) => unreachable!("{e:?}")
    678                     }
    679                 }
    680             )
    681         })
    682         .fetch_all(pool)
    683         .await
    684         .unwrap()
    685     }
    686 
    687     #[db_test]
    688     async fn registration(pool: PgPool) {
    689         let server = setup(&pool).await;
    690         registration_routine(
    691             &server.prefix("/taler-wire-gateway"),
    692             &server.prefix("/taler-prepared-transfer"),
    693             &EXCHANGE,
    694             &EXCHANGE,
    695             &UNKNOWN,
    696             || check_in(&pool),
    697         )
    698         .await;
    699     }
    700 }