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:
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,