libeufin

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

commit 9046bc648dc0e4764c6d86af32dc1e27c7de43df
parent 3510a910ae9816f3a7ede0fc94f1f21367b0f25f
Author: Antoine A <>
Date:   Tue, 15 Sep 2026 18:26:37 +0200

bank: mfa hash username and uuid in addition to the raw body

Diffstat:
MCargo.lock | 74+++++++++++++++++++++++++++++++++-----------------------------------------
Mlibeufin-bank/src/api/account.rs | 12++++++------
Mlibeufin-bank/src/api/cashout.rs | 4++--
Mlibeufin-bank/src/api/tan.rs | 39+++++++++++++++++++++------------------
Mlibeufin-bank/src/api/token.rs | 4++--
Mlibeufin-bank/src/api/tx.rs | 4++--
Mlibeufin-bank/src/api/withdrawal.rs | 3++-
Mlibeufin-bank/src/db/tan.rs | 6+++---
Mlibeufin-bank/src/mfa.rs | 82++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Mlibeufin-nexus/src/api.rs | 19+++++++++++--------
10 files changed, 134 insertions(+), 113 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -390,9 +390,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" +checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4" dependencies = [ "find-msvc-tools", "jobserver", @@ -447,9 +447,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" dependencies = [ "clap_builder", "clap_derive", @@ -457,9 +457,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" dependencies = [ "anstream", "anstyle", @@ -469,9 +469,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.4" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" dependencies = [ "heck", "proc-macro2", @@ -481,9 +481,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" [[package]] name = "cmake" @@ -496,9 +496,9 @@ dependencies = [ [[package]] name = "codepage" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f68d061bc2828ae826206326e61251aca94c1e4a5305cf52d9138639c918b4" +checksum = "bdff162541cd8b79de82e2edcc7eff3a8c2a6dc3d75152636028f96d93de3b26" dependencies = [ "encoding_rs", ] @@ -650,9 +650,9 @@ checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] name = "crc32fast" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" dependencies = [ "cfg-if", ] @@ -1661,9 +1661,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.35" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" dependencies = [ "defmt", "jiff-core", @@ -1676,18 +1676,19 @@ dependencies = [ [[package]] name = "jiff-core" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" dependencies = [ "defmt", + "log", ] [[package]] name = "jiff-static" -version = "0.2.35" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" dependencies = [ "jiff-core", "proc-macro2", @@ -2005,9 +2006,9 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "lru-slab" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" [[package]] name = "matchit" @@ -2441,9 +2442,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.11" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" dependencies = [ "bytes", "cfg_aliases", @@ -2461,9 +2462,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.17" +version = "0.11.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" +checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc" dependencies = [ "aws-lc-rs", "bytes", @@ -2821,9 +2822,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.44" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6725596c3f2c3a0aef021139e145d4eafe314a6623e4680ca83852b2c67ab2ba" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "aws-lc-rs", "log", @@ -3734,18 +3735,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf0ded5c4e56918d8f8a339e1bb67d038d3bc6d144ac407904015ba2e4cde9b" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tokio" @@ -4711,9 +4703,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" [[package]] name = "zmij" diff --git a/libeufin-bank/src/api/account.rs b/libeufin-bank/src/api/account.rs @@ -540,8 +540,8 @@ pub fn account_api() -> Router<Arc<BankState>> { .route( "/accounts/{username}", patch( - async |State(state): State<Arc<BankState>>, - MfaReq { mut auth, req, mfa }: MfaReq<AccountReconfigOp>| { + async |State(state): State<Arc<BankState>>, req: MfaReq<AccountReconfigOp>| { + let (mut auth, req, mfa) = req.solve(&state, &[]).await?; if let Some(tans) = mfa.pending_mfa() { return mfa.response_validation(&auth, &state.db, tans).await; } @@ -586,8 +586,8 @@ pub fn account_api() -> Router<Arc<BankState>> { }, ) .delete( - async |State(state): State<Arc<BankState>>, - MfaReq { mut auth, mfa, .. }: MfaReq<AccountDeletionOp>| { + async |State(state): State<Arc<BankState>>, req: MfaReq<AccountDeletionOp>| { + let (mut auth, _, mfa) = req.solve(&state, &[]).await?; if !state.cfg.allow_account_deletion && !auth.is_admin() { return Err(require_admin()); } @@ -645,8 +645,8 @@ pub fn account_api() -> Router<Arc<BankState>> { .route( "/accounts/{username}/auth", patch( - async |State(state): State<Arc<BankState>>, - MfaReq { mut auth, req, mfa }: MfaReq<AccountPasswordOp>| { + async |State(state): State<Arc<BankState>>, req: MfaReq<AccountPasswordOp>| { + let (mut auth, req, mfa) = req.solve(&state, &[]).await?; if !auth.is_admin() && req.old_password.is_none() { return Err(failure_code( ErrorCode::BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD, diff --git a/libeufin-bank/src/api/cashout.rs b/libeufin-bank/src/api/cashout.rs @@ -121,8 +121,8 @@ pub fn cashout_api(state: Arc<BankState>) -> Router<Arc<BankState>> { .route( "/accounts/{username}/cashouts", post( - async |State(state): State<Arc<BankState>>, - MfaReq { mut auth, req, mfa }: MfaReq<CashoutOp>| { + async |State(state): State<Arc<BankState>>, req: MfaReq<CashoutOp>| { + let (mut auth, req, mfa) = req.solve(&state, &[]).await?; state.cfg.check_regio(&req.amount_debit)?; state.cfg.check_fiat(&req.amount_credit)?; match create( diff --git a/libeufin-bank/src/api/tan.rs b/libeufin-bank/src/api/tan.rs @@ -220,13 +220,13 @@ pub mod test { expect_mfa( patch!({ "contact_data": { - "phone": "+99", + "phone": "+199", "email": "email@example.com" }, "tan_channel": "sms" }), true, - &[(TanChannel::sms, "+99")], + &[(TanChannel::sms, "+199")], ) .await .assert_no_content(); @@ -234,10 +234,10 @@ pub mod test { // Update 2fa settings - first 2FA challenge then new tan channel check expect_mfa( patch!({ // Info change - "contact_data": { "phone": "+98" }, + "contact_data": { "phone": "+198" }, }), true, - &[(TanChannel::sms, "+99"), (TanChannel::sms, "+98")], + &[(TanChannel::sms, "+199"), (TanChannel::sms, "+198")], ) .await .assert_no_content(); @@ -247,7 +247,7 @@ pub mod test { }), true, &[ - (TanChannel::sms, "+98"), + (TanChannel::sms, "+198"), (TanChannel::email, "email@example.com"), ], ) @@ -255,13 +255,13 @@ pub mod test { .assert_no_content(); expect_mfa( patch!({ // Both change - "contact_data": { "phone": "+97" }, + "contact_data": { "phone": "+197" }, "tan_channel": "sms" }), true, &[ (TanChannel::email, "email@example.com"), - (TanChannel::sms, "+97"), + (TanChannel::sms, "+197"), ], ) .await @@ -273,7 +273,7 @@ pub mod test { "tan_channel": () }), true, - &[(TanChannel::sms, "+97")], + &[(TanChannel::sms, "+197")], ) .await .assert_no_content(); @@ -285,7 +285,7 @@ pub mod test { }), true, &[ - (TanChannel::sms, "+97"), + (TanChannel::sms, "+197"), (TanChannel::email, "email@example.com"), ], ) @@ -295,20 +295,20 @@ pub mod test { expect_mfa( patch!({ "contact_data": { - "phone": "+99", + "phone": "+199", "email": "email2@example.com" } }), false, &[ - (TanChannel::sms, "+97"), + (TanChannel::sms, "+197"), (TanChannel::email, "email@example.com"), ], ) .await, true, &[ - (TanChannel::sms, "+99"), + (TanChannel::sms, "+199"), (TanChannel::email, "email2@example.com"), ], ) @@ -320,7 +320,7 @@ pub mod test { patch!({ "tan_channels": [] }), false, &[ - (TanChannel::sms, "+99"), + (TanChannel::sms, "+199"), (TanChannel::email, "email2@example.com"), ], ) @@ -330,7 +330,7 @@ pub mod test { // Admin has no 2FA ctx.patch_admin("/accounts/merchant") .json(json!({ - "contact_data": { "phone": "+99" }, + "contact_data": { "phone": "+199" }, "tan_channel": "sms" })) .await @@ -351,7 +351,7 @@ pub mod test { // Check retry and invalidate { patch!({ - "contact_data": { "phone": "+88" }, + "contact_data": { "phone": "+188" }, "tan_channel": "sms" }) .assert_challenge(&ctx) @@ -367,13 +367,13 @@ pub mod test { let challenge = &res.challenges[0]; // Check ok send(challenge).await; - let code = tan_code("+88").unwrap(); + let code = tan_code("+188").unwrap(); // Check retry send(challenge).await; - assert!(tan_code("+88").is_none()); + assert!(tan_code("+188").is_none()); // Idempotent patch does nothing patch!({ - "contact_data": { "phone": "+88" }, + "contact_data": { "phone": "+188" }, "tan_channel": "sms" }) .assert_accepted(); @@ -576,4 +576,7 @@ pub mod test { .assert_error(ErrorCode::BANK_TAN_CHALLENGE_EXPIRED); } } + + #[tokio::test] + async fn unique_challenge() {} } diff --git a/libeufin-bank/src/api/token.rs b/libeufin-bank/src/api/token.rs @@ -91,8 +91,8 @@ pub fn token_api() -> Router<Arc<BankState>> { .route( "/accounts/{username}/token", post( - async |State(state): State<Arc<BankState>>, - MfaReq { mut auth, req, mfa }: MfaReq<TokenOp>| { + async |State(state): State<Arc<BankState>>, req: MfaReq<TokenOp>| { + let (mut auth, req, mfa) = req.solve(&state, &[]).await?; if let Some(token) = &auth.token { // This block checks permissions ONLY IF the call was authenticated with a token let token = access(&state.db, token, &Timestamp::now()).await?; diff --git a/libeufin-bank/src/api/tx.rs b/libeufin-bank/src/api/tx.rs @@ -88,8 +88,8 @@ pub fn tx_api() -> Router<Arc<BankState>> { .route( "/accounts/{username}/transactions", post( - async |State(state): State<Arc<BankState>>, - MfaReq { mut auth, req, mfa }: MfaReq<BankTxOp>| { + async |State(state): State<Arc<BankState>>, req: MfaReq<BankTxOp>| { + let (mut auth, req, mfa) = req.solve(&state, &[]).await?; let subject = req .payto_uri .subject diff --git a/libeufin-bank/src/api/withdrawal.rs b/libeufin-bank/src/api/withdrawal.rs @@ -231,7 +231,8 @@ pub fn withdrawal_api() -> Router<Arc<BankState>> { post( async |Path((_, uuid)): Path<((), Uuid)>, State(state): State<Arc<BankState>>, - MfaReq { mut auth, req, mfa }: MfaReq<WithdrawalOp>| { + req: MfaReq<WithdrawalOp>| { + let (mut auth, req, mfa) = req.solve(&state, uuid.as_bytes()).await?; if let Some(amount) = &req.amount { state.cfg.check_regio(amount)?; } diff --git a/libeufin-bank/src/db/tan.rs b/libeufin-bank/src/db/tan.rs @@ -239,7 +239,7 @@ pub async fn solve( } #[derive(Debug)] -pub struct SolvedChallenge { +pub struct Challenge { pub id: Uuid, pub salt: Base32<16>, pub hash: Base32<64>, @@ -249,7 +249,7 @@ pub struct SolvedChallenge { pub op: Operation, } -pub async fn challenge(db: &PgPool, uuids: &[Uuid]) -> sqlx::Result<Vec<SolvedChallenge>> { +pub async fn challenge(db: &PgPool, uuids: &[Uuid]) -> sqlx::Result<Vec<Challenge>> { serialized!( sqlx::query( " @@ -260,7 +260,7 @@ pub async fn challenge(db: &PgPool, uuids: &[Uuid]) -> sqlx::Result<Vec<SolvedCh ) .bind(uuids) .try_map(|r: PgRow| { - Ok(SolvedChallenge { + Ok(Challenge { id: r.try_get("uuid")?, salt: r.try_get("salt")?, hash: r.try_get("hbody")?, diff --git a/libeufin-bank/src/mfa.rs b/libeufin-bank/src/mfa.rs @@ -24,7 +24,7 @@ use axum::{ Json, body::Bytes, extract::{FromRequest, FromRequestParts, Request}, - http::{HeaderName, StatusCode}, + http::{HeaderMap, HeaderName, StatusCode}, response::IntoResponse, }; use compact_str::CompactString; @@ -144,9 +144,11 @@ impl MfaOp for WithdrawalOp { type Body = BankAccountConfirmWithdrawalRequest; } -fn mfa_body_hash(body: &[u8], salt: &Base32<16>) -> Base32<64> { +fn mfa_body_hash(body: &[u8], username: &str, salt: &Base32<16>, ctx: &[u8]) -> Base32<64> { let mut digest = aws_lc_rs::digest::Context::new(&SHA512); digest.update(salt.as_ref()); + digest.update(username.as_bytes()); + digest.update(ctx); digest.update(body); Base32::try_from(digest.finish().as_ref()).unwrap() } @@ -159,13 +161,14 @@ enum Mfa { } #[derive(Debug)] -pub struct MfaCtx<O: MfaOp> { - raw: Bytes, +pub struct MfaCtx<'a, O: MfaOp> { + body: Bytes, + ctx: &'a [u8], mfa: Mfa, op: PhantomData<O>, } -impl<O: MfaOp> MfaCtx<O> { +impl<'a, O: MfaOp> MfaCtx<'a, O> { async fn respond_challenges( &self, db: &PgPool, @@ -176,7 +179,7 @@ impl<O: MfaOp> MfaCtx<O> { const TAN_VALIDITY_PERIOD: Duration = Duration::from_mins(30); let salt = Base32::secure_rand(); - let hash = mfa_body_hash(&self.raw, &salt); + let hash = mfa_body_hash(&self.body, username, &salt, self.ctx); let mut challenges = Vec::new(); for (channel, info) in tans { @@ -271,25 +274,20 @@ pub const TALER_CHALLENGE_IDS: HeaderName = HeaderName::from_static("taler-chall #[must_use] pub struct MfaReq<O: MfaOp> { - pub auth: UserAuth<O::Scope>, - pub req: O::Body, - pub mfa: MfaCtx<O>, + auth: UserAuth<O::Scope>, + body: Bytes, + req: O::Body, + headers: HeaderMap, } -impl<O: MfaOp> FromRequest<Arc<BankState>> for MfaReq<O> { - type Rejection = ApiError; - - async fn from_request(req: Request, state: &Arc<BankState>) -> Result<Self, Self::Rejection> { - let (mut parts, body) = req.into_parts(); - let mut auth = UserAuth::from_request_parts(&mut parts, state).await?; - let raw = if TypeId::of::<O::Body>() == TypeId::of::<Empty>() { - Bytes::default() - } else { - decompressed_strict_body(&parts.headers, body).await? - }; - let Req(req) = Req::<O::Body>::try_from(&raw)?; +impl<O: MfaOp> MfaReq<O> { + pub async fn solve<'a>( + mut self, + state: &Arc<BankState>, + ctx: &'a [u8], + ) -> ApiResult<(UserAuth<O::Scope>, O::Body, MfaCtx<'a, O>)> { // Check if challenges are used - let mfa = match parts.headers.get(&TALER_CHALLENGE_IDS) { + let mfa = match self.headers.get(&TALER_CHALLENGE_IDS) { Some(header) => { let uuids: Option<Vec<Uuid>> = header.to_str().ok().and_then(|s| { s.split(',') @@ -311,7 +309,9 @@ impl<O: MfaOp> FromRequest<Arc<BankState>> for MfaReq<O> { "Challenge '{}' is for a different operation", challenge.id ))); - } else if mfa_body_hash(&raw, &challenge.salt) != challenge.hash { + } else if mfa_body_hash(&self.body, &self.auth.username, &challenge.salt, ctx) + != challenge.hash + { return Err(forbidden(format_args!( "Challenge '{}' is for a different request", challenge.id @@ -323,9 +323,9 @@ impl<O: MfaOp> FromRequest<Arc<BankState>> for MfaReq<O> { if !validated.is_empty() { // Check if challenges are solved - let info = auth.bank_info(&state.db, &state.cfg.ctx).await?; + let info = self.auth.bank_info(&state.db, &state.cfg.ctx).await?; - if let Some(validation) = O::required_validation(&req, info)? { + if let Some(validation) = O::required_validation(&self.req, info)? { // Check mfa & new TAN validation if validation.iter().all(|it| validated.contains(it)) { Mfa::Challenged @@ -348,14 +348,36 @@ impl<O: MfaOp> FromRequest<Arc<BankState>> for MfaReq<O> { } None => Mfa::None, }; - Ok(Self { - auth, - req, - mfa: MfaCtx { - raw, + Ok(( + self.auth, + self.req, + MfaCtx { mfa, op: PhantomData, + body: self.body, + ctx, }, + )) + } +} + +impl<O: MfaOp> FromRequest<Arc<BankState>> for MfaReq<O> { + type Rejection = ApiError; + + async fn from_request(req: Request, state: &Arc<BankState>) -> Result<Self, Self::Rejection> { + let (mut parts, body) = req.into_parts(); + let auth = UserAuth::from_request_parts(&mut parts, state).await?; + let body = if TypeId::of::<O::Body>() == TypeId::of::<Empty>() { + Bytes::default() + } else { + decompressed_strict_body(&parts.headers, body).await? + }; + let Req(req) = Req::<O::Body>::try_from(&body)?; + Ok(Self { + auth, + body, + req, + headers: parts.headers, }) } } diff --git a/libeufin-nexus/src/api.rs b/libeufin-nexus/src/api.rs @@ -64,15 +64,18 @@ use taler_common::{ }; use tokio::sync::watch::Sender; -use crate::{FETCH_TASK_KEY, SUBMIT_TASK_KEY, db::{ - self, - exchange::{ - TransferResult, incoming_history, outgoing_history, revenue_history, transfer, - transfer_by_id, transfer_page, +use crate::{ + FETCH_TASK_KEY, SUBMIT_TASK_KEY, + db::{ + self, + exchange::{ + TransferResult, incoming_history, outgoing_history, revenue_history, transfer, + transfer_by_id, transfer_page, + }, + payment::{IncomingRegistrationResult, register_in_talerable}, + transfer::{RegistrationResult, transfer_register, transfer_unregister}, }, - payment::{IncomingRegistrationResult, register_in_talerable}, - transfer::{RegistrationResult, transfer_register, transfer_unregister}, -}}; +}; pub struct NexusApi { pub pool: sqlx::PgPool,