taler-rust

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

commit 5eca882b053c170ac6f7e28c5715b45e23fbec16
parent 7797b39802f6254f87bd15ae1909396daea74113
Author: Antoine A <>
Date:   Sun, 19 Jan 2025 17:52:04 +0100

magnet-bank: dev tx cmd

Diffstat:
MCargo.lock | 4++--
Mwire-gateway/magnet-bank/src/dev.rs | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mwire-gateway/magnet-bank/src/keys.rs | 10+++-------
Mwire-gateway/magnet-bank/src/magnet.rs | 171++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 234 insertions(+), 33 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -2203,9 +2203,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.135" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" +checksum = "336a0c23cf42a38d9eaa7cd22c7040d04e1228a19a933890805ffd00a16437d2" dependencies = [ "itoa", "memchr", diff --git a/wire-gateway/magnet-bank/src/dev.rs b/wire-gateway/magnet-bank/src/dev.rs @@ -14,33 +14,95 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -use sqlx::PgPool; -use taler_common::config::Config; +use clap::ValueEnum; +use taler_common::{ + config::Config, + types::{payto::payto, timestamp::Timestamp}, +}; +use tracing::info; use crate::{ - config::{DbConfig, MagnetConfig}, + config::MagnetConfig, + db::{TxIn, TxOut}, keys, - magnet::AuthClient, + magnet::{AuthClient, Direction}, }; +#[derive(Debug, Clone, PartialEq, Eq, ValueEnum)] +pub enum DirArg { + #[value(alias("in"))] + Incoming, + #[value(alias("out"))] + Outgoing, + Both, +} + #[derive(clap::Subcommand, Debug)] pub enum DevCmd { /// Print account info - Account, + Accounts, + Tx { + #[clap(long, short)] + account: String, + #[clap(long, short, value_enum, default_value_t = DirArg::Both)] + direction: DirArg, + }, } pub async fn dev(cfg: Config, cmd: DevCmd) -> anyhow::Result<()> { - let db = DbConfig::parse(&cfg)?; - let pool = PgPool::connect_with(db.cfg).await?; let cfg = MagnetConfig::parse(&cfg)?; let keys = keys::load(&cfg)?; - let client = AuthClient::new(reqwest::Client::new(), cfg.api_url, cfg.consumer) - .upgrade(keys.access_token); + let client = reqwest::Client::new(); + let client = AuthClient::new(&client, &cfg.api_url, &cfg.consumer).upgrade(&keys.access_token); match cmd { - DevCmd::Account => { + DevCmd::Accounts => { let res = client.list_accounts().await?; dbg!(res); } + DevCmd::Tx { account, direction } => { + let client = client.account(&account); + let dir = match direction { + DirArg::Incoming => Direction::Incoming, + DirArg::Outgoing => Direction::Outgoing, + DirArg::Both => Direction::Both, + }; + // Register incoming + let mut next = None; + loop { + let page = client.page_tx(dir, 5, &next, &None).await?; + next = page.next; + for item in page.list { + let tx = item.tx; + if tx.amount.is_sign_positive() { + let amount = format!("{}:{}", tx.currency, tx.amount); + let tx = TxIn { + code: tx.code, + amount: amount.parse().unwrap(), + subject: tx.subject, + debit_payto: payto("payto://"), + timestamp: Timestamp::from(tx.value_date), + }; + info!("incoming {} '{}'", tx.amount, tx.subject); + } else { + let amount = format!("{}:{}", tx.currency, -tx.amount); + let tx_out = TxOut { + code: tx.code, + amount: amount.parse().unwrap(), + subject: tx.subject, + credit_payto: payto("payto://"), + timestamp: Timestamp::from(tx.value_date), + }; + info!( + "outgoing {} {} {} '{}' {:?}", + tx_out.code, tx.tx_date, tx_out.amount, tx_out.subject, tx.status + ); + } + } + if next.is_none() { + break; + } + } + } } Ok(()) } diff --git a/wire-gateway/magnet-bank/src/keys.rs b/wire-gateway/magnet-bank/src/keys.rs @@ -78,12 +78,8 @@ pub async fn setup(cfg: MagnetConfig, reset: bool) -> anyhow::Result<()> { Err(e) if e.kind() == ErrorKind::NotFound => KeysFile::default(), Err(e) => Err(e)?, }; - - let client = AuthClient::new( - reqwest::Client::new(), - cfg.api_url.clone(), - cfg.consumer.clone(), - ); + let client = reqwest::Client::new(); + let client = AuthClient::new(&client, &cfg.api_url, &cfg.consumer); info!("Setup OAuth access token"); if keys.access_token.is_none() { @@ -107,7 +103,7 @@ pub async fn setup(cfg: MagnetConfig, reset: bool) -> anyhow::Result<()> { json_file::persist(&cfg.keys_path, &keys)?; } - let client = client.upgrade(keys.access_token.clone().unwrap()); + let client = client.upgrade(keys.access_token.as_ref().unwrap()); info!("Setup Strong Customer Authentication"); // TODO find a proper way to check if SCA is required without trigerring SCA.GLOBAL_FEATURE_NOT_ENABLED diff --git a/wire-gateway/magnet-bank/src/magnet.rs b/wire-gateway/magnet-bank/src/magnet.rs @@ -16,6 +16,7 @@ use base64::{prelude::BASE64_STANDARD, Engine}; use error::ApiResult; +use jiff::Timestamp; use p256::{ecdsa::SigningKey, PublicKey}; use serde_json::{json, Value}; use spki::EncodePublicKey; @@ -147,14 +148,102 @@ pub struct PartnerList { parteners: Vec<PartnerAccounts>, } -pub struct AuthClient { - client: reqwest::Client, - api_url: reqwest::Url, - consumer: Token, +#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum TxStatus { + #[serde(rename = "G")] + ToBeRecorded, + #[serde(rename = "1")] + PendingFirstSignature, + #[serde(rename = "2")] + PendingSecondSignature, + #[serde(rename = "F")] + PendingProcessing, + #[serde(rename = "L")] + Verified, + #[serde(rename = "R")] + PartiallyCompleted, + #[serde(rename = "T")] + Completed, + #[serde(rename = "E")] + Rejected, + #[serde(rename = "M")] + Canceled, + #[serde(rename = "P")] + UnderReview, } -impl AuthClient { - pub fn new(client: reqwest::Client, api_url: reqwest::Url, consumer: Token) -> Self { +#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum Direction { + #[serde(rename = "T")] + Outgoing, + #[serde(rename = "J")] + Incoming, + #[serde(rename = "M")] + Both, +} + +#[derive(Debug, serde::Deserialize)] +pub struct Transaction { + #[serde(rename = "kod")] + pub code: u64, + #[serde(rename = "bankszamla")] + pub bank_account: String, + #[serde(rename = "bankszamlaTulajdonos")] + pub bank_acount_owner: String, + #[serde(rename = "deviza")] + pub currency: amount::Currency, + #[serde(rename = "osszeg")] + pub amount: f64, + #[serde(rename = "kozlemeny")] + pub subject: String, + #[serde(rename = "statusz")] + pub status: TxStatus, + #[serde(rename = "tranzakcioAltipus")] + pub kind: Option<String>, + #[serde(rename = "eredetiErteknap")] + pub tx_date: jiff::Timestamp, + #[serde(rename = "erteknap")] + pub value_date: jiff::Timestamp, + #[serde(rename = "eszamla")] + pub debtor: String, + #[serde(rename = "tranzakcioTipus")] + pub ty: String, +} + +#[derive(Debug, serde::Deserialize)] +pub struct Next { + #[serde(rename = "next")] + pub next_id: u64, + #[serde(rename = "nextTipus")] + pub next_type: String, +} + +#[derive(Debug, serde::Deserialize)] +pub struct TransactionPage { + #[serde(flatten)] + pub next: Option<Next>, + #[serde(rename = "tranzakcioList", default)] + pub list: Vec<TransactionWrapper>, +} + +#[derive(Debug, serde::Deserialize)] +pub struct TransactionWrapper { + #[serde(rename = "tranzakcioDto")] + pub tx: Transaction, +} + +pub struct AuthClient<'a> { + client: &'a reqwest::Client, + api_url: &'a reqwest::Url, + consumer: &'a Token, +} + +impl<'a> AuthClient<'a> { + pub fn new( + client: &'a reqwest::Client, + api_url: &'a reqwest::Url, + consumer: &'a Token, + ) -> Self { Self { client, api_url, @@ -193,7 +282,7 @@ impl AuthClient { .await } - pub fn upgrade(self, access: Token) -> ApiClient { + pub fn upgrade(self, access: &'a Token) -> ApiClient<'a> { ApiClient { client: self.client, api_url: self.api_url, @@ -203,15 +292,15 @@ impl AuthClient { } } -pub struct ApiClient { - client: reqwest::Client, - api_url: reqwest::Url, - consumer: Token, - access: Token, +pub struct ApiClient<'a> { + client: &'a reqwest::Client, + api_url: &'a reqwest::Url, + consumer: &'a Token, + access: &'a Token, } -impl ApiClient { - pub fn join(&self, path: &str) -> reqwest::Url { +impl<'a> ApiClient<'a> { + fn join(&self, path: &str) -> reqwest::Url { self.api_url.join(path).unwrap() } @@ -267,4 +356,58 @@ impl ApiClient { .magnet_json() .await } + + pub fn account(self, account: &'a str) -> AccountClient<'a> { + AccountClient { + client: self.client, + api_url: self.api_url, + consumer: self.consumer, + access: self.access, + account: account, + } + } +} + +pub struct AccountClient<'a> { + client: &'a reqwest::Client, + api_url: &'a reqwest::Url, + consumer: &'a Token, + access: &'a Token, + account: &'a str, +} + +impl<'a> AccountClient<'a> { + fn join(&self, path: &str) -> reqwest::Url { + self.api_url.join(path).unwrap() + } + + pub async fn page_tx( + &self, + direction: Direction, + limit: u16, + next: &Option<Next>, + status: &Option<TxStatus>, + ) -> ApiResult<TransactionPage> { + let mut req = self.client.get(self.join(&format!( + "/RESTApi/resources/v2/tranzakcio/paginator/{}/{limit}", + self.account + ))); + if let Some(next) = next { + req = req + .query(&[("nextId", next.next_id)]) + .query(&[("nextTipus", &next.next_type)]); + } + if let Some(status) = status { + req = req.query(&[("statusz", status)]); + } + if direction != Direction::Both { + req = req.query(&[("terheles", direction)]) + } + + req.query(&[("tranzakciofrissites", "true")]) + .oauth(&self.consumer, Some(&self.access), None) + .await + .magnet_call() + .await + } }