commit 5eca882b053c170ac6f7e28c5715b45e23fbec16
parent 7797b39802f6254f87bd15ae1909396daea74113
Author: Antoine A <>
Date: Sun, 19 Jan 2025 17:52:04 +0100
magnet-bank: dev tx cmd
Diffstat:
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
+ }
}