taler-rust

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

commit 45785f6b58d3eb93fe04de5c352a274249d08b4c
parent 4e0704b3732a7a139f9ec7eef8a5adfdfa391aaf
Author: Antoine A <>
Date:   Fri, 12 Jun 2026 10:31:44 +0200

common: improve config parser and add new cli & repl test utils

Diffstat:
MMakefile | 6+++---
Mcommon/http-client/src/lib.rs | 9++++++---
Mcommon/taler-common/src/config.rs | 71++++++++++++++++++++++++++++++++++++++++++-----------------------------
Mcommon/taler-common/src/lib.rs | 4++--
Mcommon/taler-test-utils/Cargo.toml | 8++++++--
Acommon/taler-test-utils/src/cli.rs | 26++++++++++++++++++++++++++
Mcommon/taler-test-utils/src/lib.rs | 2++
Acommon/taler-test-utils/src/repl.rs | 212+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtaler-apns-relay/src/main.rs | 2+-
Mtaler-cyclos/src/bin/cyclos-harness.rs | 18+++++++++---------
Mtaler-cyclos/src/db.rs | 6++++--
Mtaler-cyclos/src/main.rs | 2+-
Mtaler-magnet-bank/src/bin/magnet-bank-harness.rs | 26+++++++++++++-------------
Mtaler-magnet-bank/src/db.rs | 6++++--
Mtaler-magnet-bank/src/main.rs | 2+-
15 files changed, 332 insertions(+), 68 deletions(-)

diff --git a/Makefile b/Makefile @@ -33,14 +33,14 @@ install-nobuild-files: install -m 644 -D -t $(man_dir)/man1 doc/prebuilt/man/taler-apns-relay.1 install -m 644 -D -t $(man_dir)/man5 doc/prebuilt/man/taler-apns-relay.conf.5 install -D -t $(bin_dir) contrib/taler-magnet-bank-dbconfig - install -D -t $(bin_dir) target/release/taler-magnet-bank install -D -t $(bin_dir) contrib/taler-cyclos-dbconfig - install -D -t $(bin_dir) target/release/taler-cyclos install -D -t $(bin_dir) contrib/taler-apns-relay-dbconfig - install -D -t $(bin_dir) target/release/taler-apns-relay .PHONY: install install: build install-nobuild-files + install -D -t $(bin_dir) target/release/taler-magnet-bank + install -D -t $(bin_dir) target/release/taler-cyclos + install -D -t $(bin_dir) target/release/taler-apns-relay .PHONY: check check: install-nobuild-files diff --git a/common/http-client/src/lib.rs b/common/http-client/src/lib.rs @@ -20,6 +20,7 @@ use http_body_util::Full; use hyper::{Method, StatusCode, body::Bytes}; use hyper_rustls::ConfigBuilderExt as _; use hyper_util::rt::TokioExecutor; +use rustls::crypto::CryptoProvider; use taler_common::error::FmtSource; use thiserror::Error; @@ -58,9 +59,11 @@ pub enum ClientErr { } pub fn client() -> Client { - rustls::crypto::aws_lc_rs::default_provider() - .install_default() - .expect("failed to install the default TLS provider"); + if CryptoProvider::get_default().is_none() { + rustls::crypto::aws_lc_rs::default_provider() + .install_default() + .expect("failed to install the default TLS provider"); + } // Prepare the TLS client config let tls = rustls::ClientConfig::builder() diff --git a/common/taler-common/src/config.rs b/common/taler-common/src/config.rs @@ -20,6 +20,7 @@ use std::{ os::unix::fs::PermissionsExt, path::PathBuf, str::FromStr, + sync::Arc, time::Duration, }; @@ -41,13 +42,14 @@ pub mod parser { io::{BufRead, BufReader}, path::PathBuf, str::FromStr, + sync::Arc, }; use indexmap::IndexMap; use tracing::{trace, warn}; use super::{Config, ValueErr}; - use crate::config::{Line, Location}; + use crate::config::{Inner, Line, Location}; #[derive(Debug, thiserror::Error)] pub enum ConfigErr { @@ -120,7 +122,7 @@ pub mod parser { ) -> ParserErr { ParserErr::Line(msg.into(), path.into(), line, Some(cause.to_string())) } - struct Parser { + pub struct Parser { sections: IndexMap<String, IndexMap<String, Line>>, files: Vec<PathBuf>, install_path: PathBuf, @@ -128,7 +130,7 @@ pub mod parser { } impl Parser { - fn empty() -> Self { + pub fn empty() -> Self { Self { sections: IndexMap::new(), files: Vec::new(), @@ -137,7 +139,7 @@ pub mod parser { } } - fn load_env(&mut self, src: ConfigSource) -> Result<(), ParserErr> { + pub fn load_env(&mut self, src: ConfigSource) -> Result<(), ParserErr> { let ConfigSource { project_name, .. } = src; // Load default path @@ -190,7 +192,15 @@ pub mod parser { Ok(()) } - fn parse_file(&mut self, src: PathBuf, depth: u8) -> Result<(), ParserErr> { + pub fn parse_str(&mut self, str: &str) -> Result<(), ParserErr> { + self.parse( + std::io::Cursor::new(str), + PathBuf::from_str("mem").unwrap(), + 0, + ) + } + + pub fn parse_file(&mut self, src: PathBuf, depth: u8) -> Result<(), ParserErr> { trace!(target: "config", "load file at '{}'", src.to_string_lossy()); match std::fs::File::open(&src) { Ok(file) => self.parse(BufReader::new(file), src, depth + 1), @@ -332,11 +342,11 @@ pub mod parser { pub fn finish(self) -> Config { // Convert to a readonly config struct without location info - Config { + Config(Arc::new(Inner { sections: self.sections, files: self.files, install_path: self.install_path, - } + })) } } @@ -462,22 +472,14 @@ pub mod parser { pub fn from_mem(str: &str) -> Result<Config, ParserErr> { let mut parser = Parser::empty(); - parser.parse( - std::io::Cursor::new(str), - PathBuf::from_str("mem").unwrap(), - 0, - )?; + parser.parse_str(str)?; Ok(parser.finish()) } pub fn from_mem_with_env(src: ConfigSource, str: &str) -> Result<Config, ParserErr> { let mut parser = Parser::empty(); parser.load_env(src)?; - parser.parse( - std::io::Cursor::new(str), - PathBuf::from_str("mem").unwrap(), - 0, - )?; + parser.parse_str(str)?; Ok(parser.finish()) } } @@ -525,24 +527,33 @@ struct Line { loc: Option<Location>, } -#[derive(Debug, Clone)] -pub struct Config { +#[derive(Debug)] +pub struct Inner { sections: IndexMap<String, IndexMap<String, Line>>, files: Vec<PathBuf>, install_path: PathBuf, } +#[derive(Clone)] +pub struct Config(Arc<Inner>); + +impl Debug for Config { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + impl Config { pub fn section<'cfg, 'arg>(&'cfg self, section: &'arg str) -> Section<'cfg, 'arg> { Section { name: section, config: self, - values: self.sections.get(&section.to_uppercase()), + values: self.0.sections.get(&section.to_uppercase()), } } pub fn sections<'cfg>(&'cfg self) -> impl Iterator<Item = Section<'cfg, 'cfg>> { - self.sections.iter().map(|(section, values)| Section { + self.0.sections.iter().map(|(section, values)| Section { name: section, config: self, values: Some(values), @@ -566,6 +577,7 @@ impl Config { /** Lookup for variable value from PATHS section in the configuration and environment variables */ fn lookup(cfg: &Config, name: &str, depth: u8) -> Option<Result<String, PathsubErr>> { if let Some(path_res) = cfg + .0 .sections .get("PATHS") .and_then(|section| section.get(name)) @@ -649,24 +661,25 @@ impl Config { } pub fn print(&self, mut f: impl std::io::Write, diagnostics: bool) -> std::io::Result<()> { + let Inner { + sections, + files, + install_path, + } = self.0.as_ref(); if diagnostics { writeln!(f, "#")?; writeln!(f, "# Configuration file diagnostics")?; writeln!(f, "#")?; writeln!(f, "# File Loaded:")?; - for path in &self.files { + for path in files { writeln!(f, "# {}", path.to_string_lossy())?; } writeln!(f, "#")?; - writeln!( - f, - "# Installation path: {}", - self.install_path.to_string_lossy() - )?; + writeln!(f, "# Installation path: {}", install_path.to_string_lossy())?; writeln!(f, "#")?; writeln!(f)?; } - for (sect, values) in &self.sections { + for (sect, values) in sections { writeln!(f, "[{sect}]")?; if diagnostics { writeln!(f)?; @@ -675,7 +688,7 @@ impl Config { if diagnostics { match loc { Some(Location { file, line }) => { - let path = &self.files[*file]; + let path = &files[*file]; writeln!(f, "# {}:{line}", path.to_string_lossy())?; } None => writeln!(f, "# default")?, diff --git a/common/taler-common/src/lib.rs b/common/taler-common/src/lib.rs @@ -56,7 +56,7 @@ pub struct CommonArgs { pub fn taler_main( src: ConfigSource, args: CommonArgs, - app: impl AsyncFnOnce(Config) -> Result<(), anyhow::Error>, + app: impl AsyncFnOnce(&Config) -> Result<(), anyhow::Error>, ) { taler_logger(args.log, args.verbose).init(); let cfg = match Config::from_file(src, args.config) { @@ -74,7 +74,7 @@ pub fn taler_main( .unwrap(); // Run app - let result = runtime.block_on(app(cfg)); + let result = runtime.block_on(app(&cfg)); if let Err(err) = result { error!(target: "cli", "{}", err); std::process::exit(1); diff --git a/common/taler-test-utils/Cargo.toml b/common/taler-test-utils/Cargo.toml @@ -27,4 +27,8 @@ sqlx.workspace = true http-body-util.workspace = true url.workspace = true aws-lc-rs.workspace = true -jiff.workspace = true -\ No newline at end of file +jiff.workspace = true +clap.workspace = true +reedline = "0.48" +shlex = "2.0" +nu-ansi-term = "0.50" +\ No newline at end of file diff --git a/common/taler-test-utils/src/cli.rs b/common/taler-test-utils/src/cli.rs @@ -0,0 +1,26 @@ +/* + This file is part of TALER + Copyright (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ + +pub fn clap_parse<T: clap::Parser>(cmd: &str) -> Result<T, clap::Error> { + let parts = shlex::split(cmd).unwrap(); + let cmd = T::command(); + if cmd.is_no_binary_name_set() { + T::try_parse_from(parts) + } else { + let args = std::iter::once(cmd.get_name()).chain(parts.iter().map(|it| it.as_str())); + T::try_parse_from(args) + } +} diff --git a/common/taler-test-utils/src/lib.rs b/common/taler-test-utils/src/lib.rs @@ -19,8 +19,10 @@ use taler_common::log::taler_logger; use tracing::Level; use tracing_subscriber::util::SubscriberInitExt; +pub mod cli; pub mod db; pub mod json; +pub mod repl; pub mod routine; pub mod server; diff --git a/common/taler-test-utils/src/repl.rs b/common/taler-test-utils/src/repl.rs @@ -0,0 +1,212 @@ +/* + This file is part of TALER + Copyright (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ + +use std::borrow::Cow; + +use nu_ansi_term::Color; +use reedline::{ + ColumnarMenu, DefaultHinter, FileBackedHistory, KeyCode, KeyModifiers, MenuBuilder as _, + Reedline, ReedlineEvent, ReedlineMenu, Signal, Span, Suggestion, Vi, + default_vi_insert_keybindings, default_vi_normal_keybindings, +}; + +use crate::cli::clap_parse; + +pub async fn test_repl<T: clap::Parser>( + history: &str, + prompt: String, + prepare: impl AsyncFn(), + run: impl AsyncFn(T) -> bool, +) { + let history = Box::new( + FileBackedHistory::with_file(1000, history.into()) + .expect("Error configuring history with file"), + ); + let completion_menu = Box::new( + ColumnarMenu::default() + .with_name("completion_menu") + .with_columns(4) + .with_column_width(None) + .with_column_padding(2), + ); + + let mut insert = default_vi_insert_keybindings(); + insert.add_binding( + KeyModifiers::NONE, + KeyCode::Tab, + ReedlineEvent::UntilFound(vec![ + ReedlineEvent::Menu("completion_menu".to_string()), + ReedlineEvent::MenuNext, + ]), + ); + let mut relp = Reedline::create() + .with_history(history) + .with_completer(Box::new(ClapCompleter(T::command()))) + .with_menu(ReedlineMenu::EngineCompleter(completion_menu)) + .with_quick_completions(true) + .with_partial_completions(true) + .with_hinter(Box::new(DefaultHinter::default().with_style( + nu_ansi_term::Style::new().italic().fg(Color::LightGray), + ))) + .with_edit_mode(Box::new(Vi::new(insert, default_vi_normal_keybindings()))); + + let prompt = ReplPrompt(prompt); + loop { + prepare().await; + let Signal::Success(buf) = relp.read_line(&prompt).unwrap() else { + break; + }; + match clap_parse(&buf) { + Ok(cmd) => { + if run(cmd).await { + break; + } + } + Err(e) => { + println!("{e}"); + } + } + } +} + +struct ClapCompleter(clap::Command); + +impl reedline::Completer for ClapCompleter { + fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> { + let mut suggestions = Vec::new(); + let text_before_cursor = &line[..pos]; + + let words = shlex::split(text_before_cursor).unwrap_or_default(); + let is_whitespace_at_end = text_before_cursor.ends_with(' '); + + let mut active_cmd = &self.0; + let mut last_word = ""; + + // Determine the word currently being typed + if !is_whitespace_at_end && !words.is_empty() { + last_word = words.last().unwrap(); + } + + // Identify which subcommand we are currently inside by walking the tokens + let tokens = if is_whitespace_at_end { + &words[..] + } else { + &words[..words.len().saturating_sub(1)] + }; + + for token in tokens { + if let Some(subcmd) = active_cmd + .get_subcommands() + .find(|s| s.get_name() == *token) + { + active_cmd = subcmd; + } + } + + let start = pos - last_word.len(); + let span = Span::new(start, pos); + + // Suggest Long Flags + if last_word.starts_with("--") { + for arg in active_cmd.get_arguments() { + if let Some(long) = arg.get_long() { + let flag = format!("--{}", long); + if flag.starts_with(last_word) { + suggestions.push(Suggestion { + value: flag, + description: arg.get_help().map(|s| s.to_string()), + span, + append_whitespace: true, + ..Suggestion::default() + }); + } + } + } + } + // Suggest Short & Long Flags + else if last_word.starts_with("-") { + for arg in active_cmd.get_arguments() { + if let Some(short) = arg.get_short() { + let flag = format!("-{}", short); + if flag.starts_with(last_word) { + suggestions.push(Suggestion { + value: flag, + description: arg.get_help().map(|s| s.to_string()), + span, + append_whitespace: true, + ..Suggestion::default() + }); + } + } + if let Some(long) = arg.get_long() { + let flag = format!("--{}", long); + if flag.starts_with(last_word) { + suggestions.push(Suggestion { + value: flag, + description: arg.get_help().map(|s| s.to_string()), + span, + append_whitespace: true, + ..Suggestion::default() + }); + } + } + } + } + // Suggest Subcommands + else { + for subcmd in active_cmd.get_subcommands() { + if subcmd.get_name().starts_with(last_word) { + suggestions.push(Suggestion { + value: subcmd.get_name().to_string(), + description: subcmd.get_about().map(|s| s.to_string()), + span, + append_whitespace: true, + ..Suggestion::default() + }); + } + } + } + + suggestions + } +} + +struct ReplPrompt(String); + +impl reedline::Prompt for ReplPrompt { + fn render_prompt_left(&self) -> Cow<'_, str> { + Cow::Borrowed(&self.0) + } + + fn render_prompt_right(&self) -> Cow<'_, str> { + Cow::Borrowed("") + } + + fn render_prompt_indicator(&self, _: reedline::PromptEditMode) -> Cow<'_, str> { + Cow::Borrowed(">") + } + + fn render_prompt_multiline_indicator(&self) -> Cow<'_, str> { + Cow::Borrowed(":") + } + + fn render_prompt_history_search_indicator( + &self, + _: reedline::PromptHistorySearch, + ) -> Cow<'_, str> { + Cow::Borrowed(">") + } +} diff --git a/taler-apns-relay/src/main.rs b/taler-apns-relay/src/main.rs @@ -91,6 +91,6 @@ async fn run(cmd: Command, cfg: &Config) -> anyhow::Result<()> { fn main() { let args = Args::parse(); taler_main(CONFIG_SOURCE, args.common, async |cfg| { - run(args.cmd, &cfg).await + run(args.cmd, cfg).await }); } diff --git a/taler-cyclos/src/bin/cyclos-harness.rs b/taler-cyclos/src/bin/cyclos-harness.rs @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -use std::time::Duration; +use std::{assert_matches, time::Duration}; use clap::Parser as _; use compact_str::CompactString; @@ -175,10 +175,10 @@ impl<'a> Harness<'a> { ) .await .unwrap(); - assert!(matches!( + assert_matches!( transfer.first().unwrap(), IncomingBankTransaction::Reserve { reserve_pub, .. } if *reserve_pub == key, - )); + ); } async fn custom_transfer(&self, amount: Decimal, creditor_id: i64, creditor_name: &str) -> u64 { @@ -496,10 +496,10 @@ async fn logic_harness(cfg: &Config, reset: bool) -> anyhow::Result<()> { let amount = decimal("10.3"); harness.transfer(amount).await; set_failure_scenario(&["direct-payment"]); - assert!(matches!( + assert_matches!( harness.worker().await.unwrap_err(), WorkerError::Injected(InjectedErr("direct-payment")) - )); + ); harness.worker().await?; balance.expect_sub(amount).await; harness.worker().await?; @@ -513,10 +513,10 @@ async fn logic_harness(cfg: &Config, reset: bool) -> anyhow::Result<()> { balance.expect_add(amount).await; // Sync and bounce set_failure_scenario(&["chargeback"]); - assert!(matches!( + assert_matches!( harness.worker().await.unwrap_err(), WorkerError::Injected(InjectedErr("chargeback")) - )); + ); balance.expect_sub(amount).await; // Sync recover harness.worker().await?; @@ -627,7 +627,7 @@ async fn online_harness(config: &Config, reset: bool) -> anyhow::Result<()> { fn main() { let args = Args::parse(); taler_main(CONFIG_SOURCE, args.common, async |cfg| match args.cmd { - Command::Logic { reset } => logic_harness(&cfg, reset).await, - Command::Online { reset } => online_harness(&cfg, reset).await, + Command::Logic { reset } => logic_harness(cfg, reset).await, + Command::Online { reset } => online_harness(cfg, reset).await, }); } diff --git a/taler-cyclos/src/db.rs b/taler-cyclos/src/db.rs @@ -913,6 +913,8 @@ impl CyclosTypeHelper for PgRow { #[cfg(test)] mod test { + use std::assert_matches; + use compact_str::CompactString; use jiff::{Span, Timestamp}; use serde_json::json; @@ -1185,7 +1187,7 @@ mod test { creditor_name: "Name".into(), valued_at: now, }; - assert!(matches!( + assert_matches!( db::make_transfer( &pool, &Transfer { @@ -1202,7 +1204,7 @@ mod test { .await .unwrap(), TransferResult::Success { .. } - )); + ); db::initiated_submit_success(&mut db, 1, &Timestamp::now(), transfer_id) .await .expect("status success"); diff --git a/taler-cyclos/src/main.rs b/taler-cyclos/src/main.rs @@ -145,6 +145,6 @@ async fn run(cmd: Command, cfg: &Config) -> anyhow::Result<()> { fn main() { let args = Args::parse(); taler_main(CONFIG_SOURCE, args.common, async |cfg| { - run(args.cmd, &cfg).await + run(args.cmd, cfg).await }); } diff --git a/taler-magnet-bank/src/bin/magnet-bank-harness.rs b/taler-magnet-bank/src/bin/magnet-bank-harness.rs @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -use std::{fmt::Debug, time::Duration}; +use std::{assert_matches, fmt::Debug, time::Duration}; use aws_lc_rs::signature::EcdsaKeyPair; use clap::Parser as _; @@ -196,10 +196,10 @@ impl<'a> Harness<'a> { ) .await .unwrap(); - assert!(matches!( + assert_matches!( transfer.first().unwrap(), IncomingBankTransaction::Reserve { reserve_pub, .. } if *reserve_pub == key, - )); + ); } /// Send a transaction between two magnet accounts @@ -436,10 +436,10 @@ async fn logic_harness(cfg: &Config, reset: bool) -> anyhow::Result<()> { step("Test transfer failure init-tx"); harness.transfer(10).await; set_failure_scenario(&["init-tx"]); - assert!(matches!( + assert_matches!( harness.worker().await, Err(WorkerError::Injected(InjectedErr("init-tx"))) - )); + ); harness.worker().await?; balance.expect(-10).await; harness.worker().await?; @@ -447,10 +447,10 @@ async fn logic_harness(cfg: &Config, reset: bool) -> anyhow::Result<()> { step("Test transfer failure submit-tx"); harness.transfer(11).await; set_failure_scenario(&["submit-tx"]); - assert!(matches!( + assert_matches!( harness.worker().await, Err(WorkerError::Injected(InjectedErr("submit-tx"))) - )); + ); harness.worker().await?; balance.expect(-11).await; harness.worker().await?; @@ -458,14 +458,14 @@ async fn logic_harness(cfg: &Config, reset: bool) -> anyhow::Result<()> { step("Test transfer all failures"); harness.transfer(13).await; set_failure_scenario(&["init-tx", "submit-tx"]); - assert!(matches!( + assert_matches!( harness.worker().await, Err(WorkerError::Injected(InjectedErr("init-tx"))) - )); - assert!(matches!( + ); + assert_matches!( harness.worker().await, Err(WorkerError::Injected(InjectedErr("submit-tx"))) - )); + ); harness.worker().await?; balance.expect(-13).await; harness.worker().await?; @@ -579,7 +579,7 @@ async fn online_harness(config: &Config, reset: bool) -> anyhow::Result<()> { fn main() { let args = Args::parse(); taler_main(CONFIG_SOURCE, args.common, async |cfg| match args.cmd { - Command::Logic { reset } => logic_harness(&cfg, reset).await, - Command::Online { reset } => online_harness(&cfg, reset).await, + Command::Logic { reset } => logic_harness(cfg, reset).await, + Command::Online { reset } => online_harness(cfg, reset).await, }); } diff --git a/taler-magnet-bank/src/db.rs b/taler-magnet-bank/src/db.rs @@ -880,6 +880,8 @@ pub async fn transfer_unregister(db: &PgPool, req: &Unregistration) -> sqlx::Res #[cfg(test)] mod test { + use std::assert_matches; + use jiff::{Span, Timestamp, Zoned}; use serde_json::json; use sqlx::{PgPool, Postgres, pool::PoolConnection, postgres::PgRow}; @@ -1153,7 +1155,7 @@ mod test { value_date: date, status: TxStatus::Completed, }; - assert!(matches!( + assert_matches!( make_transfer( &pool, &db::Transfer { @@ -1169,7 +1171,7 @@ mod test { .await .unwrap(), TransferResult::Success { .. } - )); + ); db::initiated_submit_success(&mut db, 1, &Timestamp::now(), tx.code) .await .expect("status success"); diff --git a/taler-magnet-bank/src/main.rs b/taler-magnet-bank/src/main.rs @@ -145,6 +145,6 @@ async fn run(cmd: Command, cfg: &Config) -> anyhow::Result<()> { fn main() { let args = Args::parse(); taler_main(CONFIG_SOURCE, args.common, async |cfg| { - run(args.cmd, &cfg).await + run(args.cmd, cfg).await }); }