taler-rust

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

commit 8ac0bdbd74316b5f6957f40114c90a0b38c269e0
parent b49e2c1855081a70b8a790c72a230e8e1976645b
Author: Antoine A <>
Date:   Tue, 30 Dec 2025 18:05:16 +0100

cyclos: new payto format

Diffstat:
Mcommon/taler-common/src/config.rs | 29+++++++++++++++++++++++++++++
Mdebian/etc/taler-cyclos/conf.d/cyclos-worker.conf | 3---
Mdebian/etc/taler-cyclos/taler-cyclos.conf | 4+++-
Mtaler-cyclos/README.md | 8++++----
Mtaler-cyclos/cyclos.conf | 8++++----
Mtaler-cyclos/src/api.rs | 36+++++++++++++++++++++++++++++-------
Mtaler-cyclos/src/bin/cyclos-harness.rs | 59++++++++++++++++++++++++++++++++++++++++-------------------
Mtaler-cyclos/src/config.rs | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mtaler-cyclos/src/cyclos_api/api.rs | 1+
Mtaler-cyclos/src/cyclos_api/types.rs | 7++++---
Mtaler-cyclos/src/db.rs | 177++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mtaler-cyclos/src/lib.rs | 39++++++++++++++++++++++++---------------
Mtaler-cyclos/src/main.rs | 5+++--
Mtaler-cyclos/src/worker.rs | 17++++++++---------
Mtaler-cyclos/tests/api.rs | 13+++++++------
Mtaler-magnet-bank/src/config.rs | 2+-
16 files changed, 327 insertions(+), 173 deletions(-)

diff --git a/common/taler-common/src/config.rs b/common/taler-common/src/config.rs @@ -849,6 +849,35 @@ impl<'cfg, 'arg> Section<'cfg, 'arg> { self.parse("url", option) } + /** Access [option] as base url */ + pub fn base_url(&self, option: &'arg str) -> Value<'arg, Url> { + self.value("url", option, |s| { + let url = Url::from_str(s).map_err(|e| e.to_string())?; + if url.scheme() != "http" && url.scheme() != "https" { + Err(format!( + "only 'http' and 'https' are accepted for baseURL got '{}''", + url.scheme() + )) + } else if !url.has_host() { + Err(format!("missing host in baseURL got '{url}'")) + } else if url.query().is_some() { + Err(format!( + "require no query in baseURL got '{}'", + url.query().unwrap() + )) + } else if url.fragment().is_some() { + Err(format!( + "require no fragment in baseURL got '{}'", + url.fragment().unwrap() + )) + } else if !url.path().ends_with('/') { + Err(format!("baseURL path must end with / got '{}'", url.path())) + } else { + Ok(url) + } + }) + } + /** Access [option] as payto */ pub fn payto(&self, option: &'arg str) -> Value<'arg, PaytoURI> { self.parse("payto", option) diff --git a/debian/etc/taler-cyclos/conf.d/cyclos-worker.conf b/debian/etc/taler-cyclos/conf.d/cyclos-worker.conf @@ -1,9 +1,6 @@ # Configuration the cyclos adapter worker. [cyclos-worker] -# URL of the Cyclos API server -API_URL = - # Cyclos account type ID to index ACCOUNT_TYPE_ID = diff --git a/debian/etc/taler-cyclos/taler-cyclos.conf b/debian/etc/taler-cyclos/taler-cyclos.conf @@ -20,10 +20,12 @@ # ".secret.conf". [cyclos] - # Adapter currency CURRENCY = +# Cyclos root url with an optional specific network +CYCLOS_URL = + # Account ID of the Cyclos account to sync ACCOUNT_ID = diff --git a/taler-cyclos/README.md b/taler-cyclos/README.md @@ -29,8 +29,8 @@ Configure the exchange account like you would in production and add the addition ```ini # dev.json -[cyclos-harness] -API_URL = http://localhost:8080/api/ +[cyclos] +CYCLOS_URL = http://localhost:8080/ [cyclos-harness] USERNAME = client @@ -55,8 +55,8 @@ Create manually two accounts with some money. ```ini # dev.json -[cyclos-harness] -API_URL = https://demo.cyclos.org/api/ +[cyclos] +CYCLOS_URL = https://demo.cyclos.org/ [cyclos-harness] USERNAME = client diff --git a/taler-cyclos/cyclos.conf b/taler-cyclos/cyclos.conf @@ -1,6 +1,9 @@ [cyclos] # Adapter currency -CURRENCY = +CURRENCY = + +# Cyclos root url with an optional specific network +CYCLOS_URL = # Account ID of the Cyclos account to sync ACCOUNT_ID = @@ -12,9 +15,6 @@ NAME = # How often should worker run when no notification is received FREQUENCY = 30m -# URL of the Cyclos API server -API_URL = - # Account username USERNAME = diff --git a/taler-cyclos/src/api.rs b/taler-cyclos/src/api.rs @@ -14,6 +14,7 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +use compact_str::CompactString; use jiff::Timestamp; use taler_api::{ api::{TalerApi, revenue::Revenue, wire::WireGateway}, @@ -47,10 +48,16 @@ pub struct CyclosApi { pub taler_in_channel: Sender<i64>, pub out_channel: Sender<i64>, pub taler_out_channel: Sender<i64>, + pub root: CompactString, } impl CyclosApi { - pub async fn start(pool: sqlx::PgPool, payto: PaytoURI, currency: Currency) -> Self { + pub async fn start( + pool: sqlx::PgPool, + root: CompactString, + payto: PaytoURI, + currency: Currency, + ) -> Self { let in_channel = Sender::new(0); let taler_in_channel = Sender::new(0); let out_channel = Sender::new(0); @@ -59,6 +66,7 @@ impl CyclosApi { pool: pool.clone(), payto, currency, + root, in_channel: in_channel.clone(), taler_in_channel: taler_in_channel.clone(), out_channel: out_channel.clone(), @@ -88,7 +96,14 @@ impl TalerApi for CyclosApi { impl WireGateway for CyclosApi { async fn transfer(&self, req: TransferRequest) -> ApiResult<TransferResponse> { let creditor = FullCyclosPayto::try_from(&req.credit_account)?; - let result = db::make_transfer(&self.pool, &req, &creditor, &Timestamp::now()).await?; + let result = db::make_transfer( + &self.pool, + &req, + *creditor.id, + &creditor.name, + &Timestamp::now(), + ) + .await?; match result { db::TransferResult::Success { id, initiated_at } => Ok(TransferResponse { timestamp: initiated_at.into(), @@ -111,13 +126,14 @@ impl WireGateway for CyclosApi { status: Option<TransferState>, ) -> ApiResult<TransferList> { Ok(TransferList { - transfers: db::transfer_page(&self.pool, &status, &self.currency, &page).await?, + transfers: db::transfer_page(&self.pool, &status, &self.currency, &self.root, &page) + .await?, debit_account: self.payto.clone(), }) } async fn transfer_by_id(&self, id: u64) -> ApiResult<Option<TransferStatus>> { - Ok(db::transfer_by_id(&self.pool, id, &self.currency).await?) + Ok(db::transfer_by_id(&self.pool, id, &self.currency, &self.root).await?) } async fn outgoing_history(&self, params: History) -> ApiResult<OutgoingHistory> { @@ -126,6 +142,7 @@ impl WireGateway for CyclosApi { &self.pool, &params, &self.currency, + &self.root, || self.taler_out_channel.subscribe(), ) .await?, @@ -139,6 +156,7 @@ impl WireGateway for CyclosApi { &self.pool, &params, &self.currency, + &self.root, || self.taler_in_channel.subscribe(), ) .await?, @@ -211,9 +229,13 @@ impl WireGateway for CyclosApi { impl Revenue for CyclosApi { async fn history(&self, params: History) -> ApiResult<RevenueIncomingHistory> { Ok(RevenueIncomingHistory { - incoming_transactions: db::revenue_history(&self.pool, &params, &self.currency, || { - self.in_channel.subscribe() - }) + incoming_transactions: db::revenue_history( + &self.pool, + &params, + &self.currency, + &self.root, + || self.in_channel.subscribe(), + ) .await?, credit_account: self.payto.clone(), }) diff --git a/taler-cyclos/src/bin/cyclos-harness.rs b/taler-cyclos/src/bin/cyclos-harness.rs @@ -17,6 +17,7 @@ use std::time::Duration; use clap::Parser as _; +use compact_str::CompactString; use failure_injection::{InjectedErr, set_failure_scenario}; use jiff::Timestamp; use owo_colors::OwoColorize as _; @@ -32,11 +33,12 @@ use taler_common::{ taler_main, types::{ amount::{Amount, Currency, Decimal, decimal}, + payto::payto, url, }, }; use taler_cyclos::{ - CyclosId, FullCyclosPayto, + FullCyclosPayto, config::{AccountType, HarnessCfg}, constants::CONFIG_SOURCE, cyclos_api::{ @@ -87,6 +89,7 @@ struct Harness<'a> { payment_type_id: u64, account_type_id: u64, currency: Currency, + root: CompactString, } impl<'a> Harness<'a> { @@ -109,7 +112,7 @@ impl<'a> Harness<'a> { async fn client_send(&self, subject: &str, amount: Decimal) -> u64 { *self .client - .direct_payment(**self.wire_payto, self.payment_type_id, amount, subject) + .direct_payment(*self.wire_payto.id, self.payment_type_id, amount, subject) .await .unwrap() .id @@ -119,7 +122,7 @@ impl<'a> Harness<'a> { async fn exchange_send(&self, subject: &str, amount: Decimal) -> u64 { *self .wire - .direct_payment(**self.client_payto, self.payment_type_id, amount, subject) + .direct_payment(*self.client_payto.id, self.payment_type_id, amount, subject) .await .unwrap() .id @@ -133,7 +136,7 @@ impl<'a> Harness<'a> { /// Fetch last transfer related to client async fn client_last_transfer(&self) -> HistoryItem { self.client - .history(**self.client_payto, OrderBy::DateDesc, 0) + .history(*self.client_payto.id, OrderBy::DateDesc, 0) .await .unwrap() .page @@ -166,6 +169,7 @@ impl<'a> Harness<'a> { timeout_ms: None, }, &self.currency, + &self.root, || tokio::sync::watch::channel(0).1, ) .await @@ -176,7 +180,7 @@ impl<'a> Harness<'a> { )); } - async fn custom_transfer(&self, amount: Decimal, creditor: &FullCyclosPayto) -> u64 { + async fn custom_transfer(&self, amount: Decimal, creditor_id: u64, creditor_name: &str) -> u64 { let res = db::make_transfer( self.pool, &TransferRequest { @@ -184,9 +188,10 @@ impl<'a> Harness<'a> { amount: Amount::new_decimal(&self.currency, amount), exchange_base_url: url("https://test.com"), wtid: ShortHashCode::rand(), - credit_account: creditor.as_payto(), + credit_account: payto("payto://"), }, - creditor, + creditor_id, + creditor_name, &Timestamp::now(), ) .await @@ -198,7 +203,8 @@ impl<'a> Harness<'a> { } async fn transfer(&self, amount: Decimal) -> u64 { - self.custom_transfer(amount, &self.client_payto).await + self.custom_transfer(amount, *self.client_payto.id, &self.client_payto.name) + .await } async fn transfer_id(&self, transfer_id: u64) -> u64 { @@ -219,7 +225,7 @@ impl<'a> Harness<'a> { async fn expect_transfer_status(&self, id: u64, status: TransferState, msg: Option<&str>) { let mut attempts = 0; loop { - let transfer = db::transfer_by_id(self.pool, id, &self.currency) + let transfer = db::transfer_by_id(self.pool, id, &self.currency, &self.root) .await .unwrap() .unwrap(); @@ -330,11 +336,16 @@ async fn logic_harness(cfg: &Config, reset: bool) -> anyhow::Result<()> { }; let harness = Harness { pool: &pool, - client_payto: client.whoami().await.unwrap().payto(), - wire_payto: wire.whoami().await.unwrap().payto(), + client_payto: client + .whoami() + .await + .unwrap() + .payto(cfg.worker.root.clone()), + wire_payto: wire.whoami().await.unwrap().payto(cfg.worker.root.clone()), client, wire, currency: cfg.worker.currency, + root: cfg.worker.root, payment_type_id: *cfg.worker.payment_type_id, account_type_id: *cfg.worker.account_type_id, }; @@ -389,7 +400,11 @@ async fn logic_harness(cfg: &Config, reset: bool) -> anyhow::Result<()> { step("Test transfer to self"); // Init a transfer to self let transfer_id = harness - .custom_transfer(decimal("10.1"), &harness.wire_payto) + .custom_transfer( + decimal("10.1"), + *harness.wire_payto.id, + &harness.wire_payto.name, + ) .await; // Should failed harness.worker().await?; @@ -405,10 +420,7 @@ async fn logic_harness(cfg: &Config, reset: bool) -> anyhow::Result<()> { step("Test transfer to unknown account"); // Init a transfer to unknown let transfer_id = harness - .custom_transfer( - decimal("10.1"), - &FullCyclosPayto::new(CyclosId(42), "Unknown"), - ) + .custom_transfer(decimal("10.1"), 42, "Unknown") .await; // Should failed harness.worker().await?; @@ -542,11 +554,16 @@ async fn online_harness(config: &Config, reset: bool) -> anyhow::Result<()> { let harness = Harness { pool: &pool, - client_payto: client.whoami().await.unwrap().payto(), - wire_payto: wire.whoami().await.unwrap().payto(), + client_payto: client + .whoami() + .await + .unwrap() + .payto(cfg.worker.root.clone()), + wire_payto: wire.whoami().await.unwrap().payto(cfg.worker.root.clone()), client, wire, currency: cfg.worker.currency, + root: cfg.worker.root, payment_type_id: *cfg.worker.payment_type_id, account_type_id: *cfg.worker.account_type_id, }; @@ -580,7 +597,11 @@ async fn online_harness(config: &Config, reset: bool) -> anyhow::Result<()> { let taler_amount = decimal("2"); let transfer_self = harness - .custom_transfer(self_amount, &harness.wire_payto) + .custom_transfer( + self_amount, + *harness.wire_payto.id, + &harness.wire_payto.name, + ) .await; let transfer_id = harness.transfer(taler_amount).await; balance.expect_sub(taler_amount).await; diff --git a/taler-cyclos/src/config.rs b/taler-cyclos/src/config.rs @@ -16,6 +16,7 @@ use std::time::Duration; +use compact_str::{CompactString, format_compact}; use reqwest::Url; use taler_api::{ Serve, @@ -27,7 +28,7 @@ use taler_common::{ types::{amount::Currency, payto::PaytoURI}, }; -use crate::{CyclosId, FullCyclosPayto}; +use crate::{CyclosAccount, CyclosId, FullCyclosPayto}; #[derive(Debug, Clone, Copy)] pub enum AccountType { @@ -39,18 +40,63 @@ pub fn parse_db_cfg(cfg: &Config) -> Result<DbCfg, ValueErr> { DbCfg::parse(cfg.section("cyclosdb-postgres")) } -pub fn parse_account_payto(cfg: &Config) -> Result<FullCyclosPayto, ValueErr> { +pub fn parse_account_payto(cfg: &Config, main: &MainCfg) -> Result<FullCyclosPayto, ValueErr> { let sect = cfg.section("cyclos"); let id = sect.parse("cyclos account id", "ACCOUNT_ID").require()?; let name = sect.str("NAME").require()?; - Ok(FullCyclosPayto::new(id, &name)) + Ok(FullCyclosPayto::new( + CyclosAccount { + id, + root: main.root.clone(), + }, + &name, + )) +} + +/// Cyclos main config +pub struct MainCfg { + pub currency: Currency, + pub url: Url, + pub root: CompactString, +} + +impl MainCfg { + pub fn parse(cfg: &Config) -> Result<Self, ValueErr> { + let sect = cfg.section("cyclos"); + let url = sect.base_url("CYCLOS_URL").require()?; + let root = format_compact!("{}{}", url.host_str().unwrap_or_default(), url.path()); + Ok(Self { + currency: sect.parse("currency", "CURRENCY").require()?, + url, + root, + }) + } +} + +/// Cyclos API config +pub struct HostCfg { + pub api_url: Url, + pub username: String, + pub password: String, +} + +impl HostCfg { + pub fn parse(cfg: &Config, main: &MainCfg) -> Result<Self, ValueErr> { + let sect = cfg.section("cyclos-worker"); + Ok(Self { + username: sect.str("USERNAME").require()?, + api_url: main.url.join("api/").unwrap(), + password: sect.str("PASSWORD").require()?, + }) + } } /// taler-cyclos httpd config pub struct ServeCfg { pub payto: PaytoURI, pub currency: Currency, + pub root: CompactString, pub serve: Serve, pub wire_gateway: Option<ApiCfg>, pub revenue: Option<ApiCfg>, @@ -58,9 +104,9 @@ pub struct ServeCfg { impl ServeCfg { pub fn parse(cfg: &Config) -> Result<Self, ValueErr> { - let payto = parse_account_payto(cfg)?; + let main = MainCfg::parse(cfg)?; + let payto = parse_account_payto(cfg, &main)?; - let main_sect = cfg.section("cyclos"); let sect = cfg.section("cyclos-httpd"); let serve = Serve::parse(sect)?; @@ -70,7 +116,8 @@ impl ServeCfg { Ok(Self { payto: payto.as_payto(), - currency: main_sect.parse("currency", "CURRENCY").require()?, + currency: main.currency, + root: main.root, serve, wire_gateway, revenue, @@ -78,24 +125,6 @@ impl ServeCfg { } } -/// Cyclos API config -pub struct HostCfg { - pub api_url: Url, - pub username: String, - pub password: String, -} - -impl HostCfg { - pub fn parse(cfg: &Config) -> Result<Self, ValueErr> { - let sect = cfg.section("cyclos-worker"); - Ok(Self { - username: sect.str("USERNAME").require()?, - api_url: sect.parse("URL", "API_URL").require()?, - password: sect.str("PASSWORD").require()?, - }) - } -} - /// taler-cyclos setup config pub struct SetupCfg { pub currency: Currency, @@ -108,10 +137,10 @@ pub struct SetupCfg { impl SetupCfg { pub fn parse(cfg: &Config) -> Result<Self, ValueErr> { + let main = MainCfg::parse(cfg)?; let main_s = cfg.section("cyclos"); let worker_s = cfg.section("cyclos-worker"); Ok(Self { - currency: main_s.parse("currency", "CURRENCY").require()?, id: main_s.parse("cyclos account id", "ACCOUNT_ID").opt()?, account_type_id: worker_s .parse("cyclos account type id", "ACCOUNT_TYPE_ID") @@ -120,7 +149,8 @@ impl SetupCfg { .parse("cyclos payment type id", "PAYMENT_TYPE_ID") .opt()?, name: main_s.str("NAME").opt()?, - host: HostCfg::parse(cfg)?, + host: HostCfg::parse(cfg, &main)?, + currency: main.currency }) } } @@ -128,6 +158,7 @@ impl SetupCfg { /// taler-cyclos worker config pub struct WorkerCfg { pub currency: Currency, + pub root: CompactString, pub frequency: Duration, pub host: HostCfg, pub account_type: AccountType, @@ -137,10 +168,9 @@ pub struct WorkerCfg { impl WorkerCfg { pub fn parse(cfg: &Config) -> Result<Self, ValueErr> { - let main_sect = cfg.section("cyclos"); + let main = MainCfg::parse(&cfg)?; let sect = cfg.section("cyclos-worker"); Ok(Self { - currency: main_sect.parse("currency", "CURRENCY").require()?, frequency: sect.duration("FREQUENCY").require()?, account_type: map_config!(sect, "account type", "ACCOUNT_TYPE", "exchange" => { Ok(AccountType::Exchange) }, @@ -153,7 +183,9 @@ impl WorkerCfg { payment_type_id: sect .parse("cyclos payment type id", "PAYMENT_TYPE_ID") .require()?, - host: HostCfg::parse(cfg)?, + host: HostCfg::parse(cfg, &main)?, + currency: main.currency, + root: main.root }) } } @@ -162,7 +194,7 @@ impl WorkerCfg { pub struct HarnessCfg { pub worker: WorkerCfg, pub username: String, - pub password: String, + pub password: String } impl HarnessCfg { diff --git a/taler-cyclos/src/cyclos_api/api.rs b/taler-cyclos/src/cyclos_api/api.rs @@ -319,6 +319,7 @@ impl SseClient { } pub async fn next(&mut self) -> Option<Result<SseMessage, LinesCodecError>> { + // TODO add tests let mut event = CompactString::new("message"); let mut data = String::new(); while let Some(res) = self.stream.next().await { diff --git a/taler-cyclos/src/cyclos_api/types.rs b/taler-cyclos/src/cyclos_api/types.rs @@ -16,11 +16,12 @@ use std::collections::BTreeMap; +use compact_str::CompactString; use jiff::Timestamp; use serde::{Deserialize, Serialize}; use taler_common::types::{amount::Decimal, payto::FullPayto}; -use crate::{CyclosId, FullCyclosPayto}; +use crate::{CyclosAccount, CyclosId, FullCyclosPayto}; #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] @@ -75,8 +76,8 @@ pub struct User { } impl User { - pub fn payto(&self) -> FullCyclosPayto { - FullPayto::new(self.id, &self.name) + pub fn payto(&self, root: CompactString) -> FullCyclosPayto { + FullPayto::new(CyclosAccount { id: self.id, root }, &self.name) } } diff --git a/taler-cyclos/src/db.rs b/taler-cyclos/src/db.rs @@ -16,6 +16,7 @@ use std::fmt::Display; +use compact_str::CompactString; use jiff::Timestamp; use serde::{Serialize, de::DeserializeOwned}; use sqlx::{PgConnection, PgExecutor, PgPool, QueryBuilder, Row, postgres::PgRow}; @@ -38,7 +39,7 @@ use taler_common::{ }; use tokio::sync::watch::{Receiver, Sender}; -use crate::{CyclosId, FullCyclosPayto, config::parse_db_cfg}; +use crate::{CyclosAccount, CyclosId, FullCyclosPayto, config::parse_db_cfg}; const SCHEMA: &str = "cyclos"; @@ -85,7 +86,8 @@ pub struct TxIn { pub tx_id: Option<u64>, pub amount: Decimal, pub subject: String, - pub debtor: FullCyclosPayto, + pub debtor_id: u64, + pub debtor_name: String, pub valued_at: Timestamp, } @@ -96,8 +98,9 @@ impl Display for TxIn { tx_id, amount, subject, - debtor, valued_at, + debtor_id, + debtor_name, } = self; let tx_id = match tx_id { Some(id) => format_args!(":{}", *id), @@ -105,8 +108,7 @@ impl Display for TxIn { }; write!( f, - "{valued_at} {transfer_id}{tx_id} {amount} ({} {}) '{subject}'", - debtor.0, debtor.name + "{valued_at} {transfer_id}{tx_id} {amount} ({debtor_id} {debtor_name}) '{subject}'" ) } } @@ -117,7 +119,8 @@ pub struct TxOut { pub tx_id: Option<u64>, pub amount: Decimal, pub subject: String, - pub creditor: FullCyclosPayto, + pub creditor_id: u64, + pub creditor_name: String, pub valued_at: Timestamp, } @@ -128,7 +131,8 @@ impl Display for TxOut { tx_id, amount, subject, - creditor, + creditor_id, + creditor_name, valued_at, } = self; let tx_id = match tx_id { @@ -137,8 +141,7 @@ impl Display for TxOut { }; write!( f, - "{valued_at} {transfer_id}{tx_id} {amount} ({} {}) '{subject}'", - creditor.0, creditor.name + "{valued_at} {transfer_id}{tx_id} {amount} ({creditor_id} {creditor_name}) '{subject}'" ) } } @@ -148,7 +151,8 @@ pub struct Initiated { pub id: u64, pub amount: Decimal, pub subject: String, - pub creditor: FullCyclosPayto, + pub creditor_id: u64, + pub creditor_name: String, } impl Display for Initiated { @@ -157,12 +161,12 @@ impl Display for Initiated { id, amount, subject, - creditor, + creditor_id, + creditor_name, } = self; write!( f, - "{id} {amount} ({} {}) '{subject}'", - creditor.0, creditor.name + "{id} {amount} ({creditor_id} {creditor_name}) '{subject}'" ) } } @@ -206,7 +210,7 @@ pub async fn register_tx_in_admin( ) .bind_decimal(&tx.amount) .bind(&tx.subject) - .bind(tx.debtor.0 as i64) + .bind(*tx.debtor.id as i64) .bind(&tx.debtor.name) .bind_timestamp(now) .bind(tx.metadata.ty()) @@ -242,8 +246,8 @@ pub async fn register_tx_in( .bind(tx.tx_id.map(|it| it as i64)) .bind_decimal(&tx.amount) .bind(&tx.subject) - .bind(tx.debtor.0 as i64) - .bind(&tx.debtor.name) + .bind(tx.debtor_id as i64) + .bind(&tx.debtor_name) .bind(tx.valued_at.as_microsecond()) .bind(subject.as_ref().map(|it| it.ty())) .bind(subject.as_ref().map(|it| it.key())) @@ -304,8 +308,8 @@ pub async fn register_tx_out( .bind(tx.tx_id.map(|it| it as i64)) .bind_decimal(&tx.amount) .bind(&tx.subject) - .bind(tx.creditor.0 as i64) - .bind(&tx.creditor.name) + .bind(tx.creditor_id as i64) + .bind(&tx.creditor_name) .bind_timestamp(&tx.valued_at); let query = match kind { TxOutKind::Simple => query @@ -343,7 +347,8 @@ pub enum TransferResult { pub async fn make_transfer<'a>( db: impl PgExecutor<'a>, req: &TransferRequest, - creditor: &FullCyclosPayto, + creditor_id: u64, + creditor_name: &str, now: &Timestamp, ) -> sqlx::Result<TransferResult> { let subject = format!("{} {}", req.wtid, req.exchange_base_url); @@ -358,8 +363,8 @@ pub async fn make_transfer<'a>( .bind(&subject) .bind_amount(&req.amount) .bind(req.exchange_base_url.as_str()) - .bind(creditor.0 as i64) - .bind(&creditor.name) + .bind(creditor_id as i64) + .bind(creditor_name) .bind_timestamp(now) .try_map(|r: PgRow| { Ok(if r.try_get(0)? { @@ -400,8 +405,8 @@ pub async fn register_bounced_tx_in( .bind(tx.tx_id.map(|it| it as i64)) .bind_decimal(&tx.amount) .bind(&tx.subject) - .bind(tx.debtor.0 as i64) - .bind(&tx.debtor.name) + .bind(tx.debtor_id as i64) + .bind(&tx.debtor_name) .bind_timestamp(&tx.valued_at) .bind(chargeback_id as i64) .bind(reason) @@ -420,6 +425,7 @@ pub async fn transfer_page<'a>( db: impl PgExecutor<'a>, status: &Option<TransferState>, currency: &Currency, + root: &CompactString, params: &Page, ) -> sqlx::Result<Vec<TransferListStatus>> { page( @@ -452,7 +458,7 @@ pub async fn transfer_page<'a>( row_id: r.try_get_safeu64(0)?, status: r.try_get(1)?, amount: r.try_get_amount_i(2, currency)?, - credit_account: r.try_get_cyclos_fullpaytouri(4, 5)?, + credit_account: r.try_get_cyclos_fullpaytouri(4, 5, root)?, timestamp: r.try_get_timestamp(6)?.into(), }) }, @@ -464,6 +470,7 @@ pub async fn outgoing_history( db: &PgPool, params: &History, currency: &Currency, + root: &CompactString, listen: impl FnOnce() -> Receiver<i64>, ) -> sqlx::Result<Vec<OutgoingBankTransaction>> { history( @@ -493,7 +500,7 @@ pub async fn outgoing_history( Ok(OutgoingBankTransaction { row_id: r.try_get_safeu64(0)?, amount: r.try_get_amount_i(1, currency)?, - credit_account: r.try_get_cyclos_fullpaytouri(3, 4)?, + credit_account: r.try_get_cyclos_fullpaytouri(3, 4, root)?, date: r.try_get_timestamp(5)?.into(), exchange_base_url: r.try_get_url(6)?, wtid: r.try_get_base32(7)?, @@ -507,6 +514,7 @@ pub async fn incoming_history( db: &PgPool, params: &History, currency: &Currency, + root: &CompactString, listen: impl FnOnce() -> Receiver<i64>, ) -> sqlx::Result<Vec<IncomingBankTransaction>> { history( @@ -537,14 +545,14 @@ pub async fn incoming_history( IncomingType::reserve => IncomingBankTransaction::Reserve { row_id: r.try_get_safeu64(1)?, amount: r.try_get_amount_i(2, currency)?, - debit_account: r.try_get_cyclos_fullpaytouri(4, 5)?, + debit_account: r.try_get_cyclos_fullpaytouri(4, 5, root)?, date: r.try_get_timestamp(6)?.into(), reserve_pub: r.try_get_base32(7)?, }, IncomingType::kyc => IncomingBankTransaction::Kyc { row_id: r.try_get_safeu64(1)?, amount: r.try_get_amount_i(2, currency)?, - debit_account: r.try_get_cyclos_fullpaytouri(4, 5)?, + debit_account: r.try_get_cyclos_fullpaytouri(4, 5, root)?, date: r.try_get_timestamp(6)?.into(), account_pub: r.try_get_base32(7)?, }, @@ -561,6 +569,7 @@ pub async fn revenue_history( db: &PgPool, params: &History, currency: &Currency, + root: &CompactString, listen: impl FnOnce() -> Receiver<i64>, ) -> sqlx::Result<Vec<RevenueIncomingBankTransaction>> { history( @@ -590,7 +599,7 @@ pub async fn revenue_history( date: r.try_get_timestamp(1)?.into(), amount: r.try_get_amount_i(2, currency)?, credit_fee: None, - debit_account: r.try_get_cyclos_fullpaytouri(4, 5)?, + debit_account: r.try_get_cyclos_fullpaytouri(4, 5, root)?, subject: r.try_get(6)?, }) }, @@ -602,6 +611,7 @@ pub async fn transfer_by_id<'a>( db: impl PgExecutor<'a>, id: u64, currency: &Currency, + root: &CompactString, ) -> sqlx::Result<Option<TransferStatus>> { sqlx::query( " @@ -628,7 +638,7 @@ pub async fn transfer_by_id<'a>( amount: r.try_get_amount_i(2, currency)?, origin_exchange_url: r.try_get(4)?, wtid: r.try_get_base32(5)?, - credit_account: r.try_get_cyclos_fullpaytouri(6, 7)?, + credit_account: r.try_get_cyclos_fullpaytouri(6, 7, root)?, timestamp: r.try_get_timestamp(8)?.into(), }) }) @@ -657,7 +667,8 @@ pub async fn pending_batch<'a>( id: r.try_get_u64(0)?, amount: r.try_get_decimal(1, 2)?, subject: r.try_get(3)?, - creditor: r.try_get_cyclos_fullpayto(4, 5)?, + creditor_id: r.try_get_u64(4)?, + creditor_name: r.try_get(5)?, }) }) .fetch_all(db) @@ -766,11 +777,13 @@ pub trait CyclosTypeHelper { &self, idx: I, name: I, + root: &CompactString, ) -> sqlx::Result<FullCyclosPayto>; fn try_get_cyclos_fullpaytouri<I: sqlx::ColumnIndex<Self>>( &self, idx: I, name: I, + root: &CompactString, ) -> sqlx::Result<PaytoURI>; } @@ -779,19 +792,31 @@ impl CyclosTypeHelper for PgRow { &self, idx: I, name: I, + root: &CompactString, ) -> sqlx::Result<FullCyclosPayto> { let idx = self.try_get_u64(idx)?; let name = self.try_get(name)?; - Ok(FullCyclosPayto::new(CyclosId(idx), name)) + Ok(FullCyclosPayto::new( + CyclosAccount { + id: CyclosId(idx), + root: root.clone(), + }, + name, + )) } fn try_get_cyclos_fullpaytouri<I: sqlx::ColumnIndex<Self>>( &self, idx: I, name: I, + root: &CompactString, ) -> sqlx::Result<PaytoURI> { let idx = self.try_get_u64(idx)?; let name = self.try_get(name)?; - Ok(CyclosId(idx).as_full_payto(name)) + Ok(CyclosAccount { + id: CyclosId(idx), + root: root.clone(), + } + .as_full_payto(name)) } } @@ -799,6 +824,7 @@ impl CyclosTypeHelper for PgRow { mod test { use std::sync::LazyLock; + use compact_str::CompactString; use jiff::{Span, Timestamp}; use serde_json::json; use sqlx::{PgConnection, PgPool, postgres::PgRow}; @@ -829,6 +855,7 @@ mod test { }; pub static CURRENCY: LazyLock<Currency> = LazyLock::new(|| "TEST".parse().unwrap()); + pub const ROOT: CompactString = CompactString::const_new("localhost"); fn fake_listen<T: Default>() -> Receiver<T> { tokio::sync::watch::channel(T::default()).1 @@ -886,7 +913,8 @@ mod test { tx_id: None, amount: decimal("10"), subject: "subject".to_owned(), - debtor: cyclos_payto("payto://cyclos/31000163100000000?receiver-name=name"), + debtor_id: 31000163100000000, + debtor_name: "Name".to_string(), valued_at: now, }; // Insert @@ -943,13 +971,13 @@ mod test { // Empty db assert_eq!( - db::revenue_history(&pool, &History::default(), &CURRENCY, fake_listen) + db::revenue_history(&pool, &History::default(), &CURRENCY, &ROOT, fake_listen) .await .unwrap(), Vec::new() ); assert_eq!( - db::incoming_history(&pool, &History::default(), &CURRENCY, fake_listen) + db::incoming_history(&pool, &History::default(), &CURRENCY, &ROOT, fake_listen) .await .unwrap(), Vec::new() @@ -976,14 +1004,14 @@ mod test { // History assert_eq!( - db::revenue_history(&pool, &History::default(), &CURRENCY, fake_listen) + db::revenue_history(&pool, &History::default(), &CURRENCY, &ROOT, fake_listen) .await .unwrap() .len(), 6 ); assert_eq!( - db::incoming_history(&pool, &History::default(), &CURRENCY, fake_listen) + db::incoming_history(&pool, &History::default(), &CURRENCY, &ROOT, fake_listen) .await .unwrap() .len(), @@ -997,7 +1025,7 @@ mod test { // Empty db assert_eq!( - db::incoming_history(&pool, &History::default(), &CURRENCY, fake_listen) + db::incoming_history(&pool, &History::default(), &CURRENCY, &ROOT, fake_listen) .await .unwrap(), Vec::new() @@ -1008,7 +1036,7 @@ mod test { let tx = TxInAdmin { amount: decimal("10"), subject: "subject".to_owned(), - debtor: cyclos_payto("payto://cyclos/31000163100000000?receiver-name=name"), + debtor: cyclos_payto("payto://cyclos/localhost/31000163100000000?receiver-name=name"), metadata: IncomingSubject::Reserve(EddsaPublicKey::rand()), }; // Insert @@ -1055,7 +1083,7 @@ mod test { // History assert_eq!( - db::incoming_history(&pool, &History::default(), &CURRENCY, fake_listen) + db::incoming_history(&pool, &History::default(), &CURRENCY, &ROOT, fake_listen) .await .unwrap() .len(), @@ -1080,7 +1108,8 @@ mod test { tx_id: Some(transfer_id), amount: decimal("10"), subject: "subject".to_owned(), - creditor: cyclos_payto("payto://cyclos/31000163100000000?receiver-name=name"), + creditor_id: 31000163100000000, + creditor_name: "Name".to_string(), valued_at: now, }; assert!(matches!( @@ -1092,10 +1121,11 @@ mod test { exchange_base_url: url("https://exchange.test.com/"), wtid: ShortHashCode::rand(), credit_account: payto( - "payto://cyclos/31000163100000000?receiver-name=name" + "payto://cyclos/localhost/31000163100000000?receiver-name=name" ), }, - &tx.creditor, + 31000163100000000, + "Name", &now ) .await @@ -1158,7 +1188,7 @@ mod test { // Empty db assert_eq!( - db::outgoing_history(&pool, &History::default(), &CURRENCY, fake_listen) + db::outgoing_history(&pool, &History::default(), &CURRENCY, &ROOT, fake_listen) .await .unwrap(), Vec::new() @@ -1186,7 +1216,7 @@ mod test { // History assert_eq!( - db::outgoing_history(&pool, &History::default(), &CURRENCY, fake_listen) + db::outgoing_history(&pool, &History::default(), &CURRENCY, &ROOT, fake_listen) .await .unwrap() .len(), @@ -1202,11 +1232,13 @@ mod test { // Empty db assert_eq!( - db::transfer_by_id(&mut db, 0, &CURRENCY).await.unwrap(), + db::transfer_by_id(&mut db, 0, &CURRENCY, &ROOT) + .await + .unwrap(), None ); assert_eq!( - db::transfer_page(&mut db, &None, &CURRENCY, &Page::default()) + db::transfer_page(&mut db, &None, &CURRENCY, &ROOT, &Page::default()) .await .unwrap(), Vec::new() @@ -1219,12 +1251,11 @@ mod test { wtid: ShortHashCode::rand(), credit_account: payto("payto://iban/HU02162000031000164800000000?receiver-name=name"), }; - let payto = cyclos_payto("payto://cyclos/31000163100000000?receiver-name=name"); let now = now_sql_stable_timestamp(); let later = now + Span::new().hours(2); // Insert assert_eq!( - db::make_transfer(&mut db, &req, &payto, &now) + db::make_transfer(&mut db, &req, 31000163100000000, "Name", &now) .await .expect("transfer"), TransferResult::Success { @@ -1234,7 +1265,7 @@ mod test { ); // Idempotent assert_eq!( - db::make_transfer(&mut db, &req, &payto, &later) + db::make_transfer(&mut db, &req, 31000163100000000, "Name", &later) .await .expect("transfer"), TransferResult::Success { @@ -1250,7 +1281,8 @@ mod test { wtid: ShortHashCode::rand(), ..req.clone() }, - &payto, + 31000163100000000, + "Name", &now ) .await @@ -1265,7 +1297,8 @@ mod test { request_uid: HashCode::rand(), ..req.clone() }, - &payto, + 31000163100000000, + "Name", &now ) .await @@ -1281,7 +1314,8 @@ mod test { wtid: ShortHashCode::rand(), ..req }, - &payto, + 31000163100000000, + "Name", &later ) .await @@ -1294,25 +1328,25 @@ mod test { // Get assert!( - db::transfer_by_id(&mut db, 1, &CURRENCY) + db::transfer_by_id(&mut db, 1, &CURRENCY, &ROOT) .await .unwrap() .is_some() ); assert!( - db::transfer_by_id(&mut db, 2, &CURRENCY) + db::transfer_by_id(&mut db, 2, &CURRENCY, &ROOT) .await .unwrap() .is_some() ); assert!( - db::transfer_by_id(&mut db, 3, &CURRENCY) + db::transfer_by_id(&mut db, 3, &CURRENCY, &ROOT) .await .unwrap() .is_none() ); assert_eq!( - db::transfer_page(&mut db, &None, &CURRENCY, &Page::default()) + db::transfer_page(&mut db, &None, &CURRENCY, &ROOT, &Page::default()) .await .unwrap() .len(), @@ -1325,7 +1359,6 @@ mod test { let (mut db, _) = setup().await; let amount = decimal("10"); - let payto = cyclos_payto("payto://cyclos/31000163100000000?receiver-name=name"); let now = now_sql_stable_timestamp(); // Bounce @@ -1337,7 +1370,8 @@ mod test { tx_id: None, amount, subject: "subject".to_owned(), - debtor: payto.clone(), + debtor_id: 31000163100000000, + debtor_name: "Name".to_string(), valued_at: now }, 22, @@ -1360,7 +1394,8 @@ mod test { tx_id: None, amount: amount.clone(), subject: "subject".to_owned(), - debtor: payto.clone(), + debtor_id: 31000163100000000, + debtor_name: "Name".to_string(), valued_at: now }, 22, @@ -1384,7 +1419,8 @@ mod test { tx_id: None, amount: amount.clone(), subject: "subject".to_owned(), - debtor: payto.clone(), + debtor_id: 31000163100000000, + debtor_name: "Name".to_string(), valued_at: now }, 23, @@ -1403,7 +1439,6 @@ mod test { #[tokio::test] async fn status() { let (mut db, _) = setup().await; - let cyclos_payto = cyclos_payto("payto://cyclos/31000163100000000?receiver-name=name"); async fn check_status( db: &mut PgConnection, @@ -1411,7 +1446,7 @@ mod test { status: TransferState, msg: Option<&str>, ) { - let transfer = db::transfer_by_id(db, id, &CURRENCY) + let transfer = db::transfer_by_id(db, id, &CURRENCY, &ROOT) .await .unwrap() .unwrap(); @@ -1445,7 +1480,8 @@ mod test { "payto://iban/HU02162000031000164800000000?receiver-name=name", ), }, - &cyclos_payto, + 31000163100000000, + "Name", &Timestamp::now(), ) .await @@ -1474,7 +1510,8 @@ mod test { "payto://iban/HU02162000031000164800000000?receiver-name=name", ), }, - &cyclos_payto, + 31000163100000000, + "Name", &Timestamp::now(), ) .await @@ -1491,7 +1528,8 @@ mod test { tx_id: Some(3), amount: decimal("2"), subject: "".to_string(), - creditor: cyclos_payto, + creditor_id: 31000163100000000, + creditor_name: "Name".to_string(), valued_at: Timestamp::now(), }, &TxOutKind::Simple, @@ -1523,7 +1561,6 @@ mod test { async fn batch() { let (mut db, _) = setup().await; let start = Timestamp::now(); - let cyclos_payto = cyclos_payto("payto://cyclos/31000163100000000?receiver-name=name"); // Empty db let pendings = db::pending_batch(&mut db, &start) @@ -1544,7 +1581,8 @@ mod test { "payto://iban/HU02162000031000164800000000?receiver-name=name", ), }, - &cyclos_payto, + 31000163100000000, + "Name", &Timestamp::now(), ) .await @@ -1568,7 +1606,8 @@ mod test { "payto://iban/HU02162000031000164800000000?receiver-name=name", ), }, - &cyclos_payto, + 31000163100000000, + "Name", &Timestamp::now(), ) .await diff --git a/taler-cyclos/src/lib.rs b/taler-cyclos/src/lib.rs @@ -16,6 +16,7 @@ use std::{fmt::Display, num::ParseIntError, ops::Deref, str::FromStr, sync::Arc}; +use compact_str::CompactString; use sqlx::PgPool; use taler_api::api::{Router, TalerRouter as _}; use taler_common::{ @@ -30,14 +31,14 @@ pub mod config; pub mod constants; pub mod cyclos_api; pub mod db; +pub mod dev; pub mod notification; pub mod setup; pub mod worker; -pub mod dev; pub async fn run_serve(cfg: &Config, pool: PgPool) -> anyhow::Result<()> { let cfg = ServeCfg::parse(cfg)?; - let api = Arc::new(CyclosApi::start(pool, cfg.payto, cfg.currency).await); + let api = Arc::new(CyclosApi::start(pool, cfg.root, cfg.payto, cfg.currency).await); let mut router = Router::new(); if let Some(cfg) = cfg.wire_gateway { router = router.wire_gateway(api.clone(), cfg.auth.method()); @@ -49,6 +50,12 @@ pub async fn run_serve(cfg: &Config, pool: PgPool) -> anyhow::Result<()> { Ok(()) } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CyclosAccount { + pub id: CyclosId, + pub root: CompactString, +} + #[derive( Debug, Clone, Copy, PartialEq, Eq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay, )] @@ -83,12 +90,12 @@ impl FromStr for CyclosId { const CYCLOS: &str = "cyclos"; #[derive(Debug, thiserror::Error)] -#[error("missing cyclos account id in path")] -pub struct MissingCyclos; +#[error("missing cyclos root and account id in path")] +pub struct MissingParts; -impl PaytoImpl for CyclosId { +impl PaytoImpl for CyclosAccount { fn as_payto(&self) -> PaytoURI { - PaytoURI::from_parts(CYCLOS, format_args!("/{}", self.0)) + PaytoURI::from_parts(CYCLOS, format_args!("{}/{}", self.root, self.id)) } fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> { @@ -99,14 +106,14 @@ impl PaytoImpl for CyclosId { url.domain().unwrap_or_default().to_owned(), )); } - let Some(mut segments) = url.path_segments() else { - return Err(PaytoErr::custom(MissingCyclos)); - }; - let Some(first) = segments.next() else { - return Err(PaytoErr::custom(MissingCyclos)); + let Some((root, id)) = url.path().trim_start_matches("/").rsplit_once('/') else { + return Err(PaytoErr::custom(MissingParts)); }; - CyclosId::from_str(first).map_err(PaytoErr::custom) + Ok(CyclosAccount { + id: CyclosId::from_str(id).map_err(PaytoErr::custom)?, + root: CompactString::new(root), + }) } } @@ -115,6 +122,8 @@ pub fn cyclos_payto(url: impl AsRef<str>) -> FullCyclosPayto { url.as_ref().parse().expect("invalid cyclos payto") } -pub type CyclosPayto = Payto<CyclosId>; -pub type FullCyclosPayto = FullPayto<CyclosId>; -pub type TransferCyclosPayto = TransferPayto<CyclosId>; +// TODO should we check the root url ? + +pub type CyclosPayto = Payto<CyclosAccount>; +pub type FullCyclosPayto = FullPayto<CyclosAccount>; +pub type TransferCyclosPayto = TransferPayto<CyclosAccount>; diff --git a/taler-cyclos/src/main.rs b/taler-cyclos/src/main.rs @@ -20,7 +20,7 @@ use taler_api::config::{ApiCfg, AuthCfg}; use taler_build::long_version; use taler_common::{CommonArgs, cli::ConfigCmd, config::Config, taler_main}; use taler_cyclos::{ - config::{ServeCfg, parse_account_payto}, + config::{MainCfg, ServeCfg, parse_account_payto}, constants::CONFIG_SOURCE, db::{dbinit, pool}, dev::{self, DevCmd}, @@ -112,7 +112,8 @@ async fn run(cmd: Command, cfg: &Config) -> anyhow::Result<()> { Command::Dev(cmd) => dev::dev(cfg, cmd).await?, Command::TalerDeployment(cmd) => match cmd { TalerDeployment::ExchangePayto => { - let payto = parse_account_payto(cfg)?; + let main = MainCfg::parse(&cfg)?; + let payto = parse_account_payto(cfg, &main)?; println!("{payto}"); } TalerDeployment::WireGatewayCredentials => { diff --git a/taler-cyclos/src/worker.rs b/taler-cyclos/src/worker.rs @@ -29,7 +29,6 @@ use tokio::{join, sync::Notify}; use tracing::{debug, error, info, trace, warn}; use crate::{ - FullCyclosPayto, config::{AccountType, WorkerCfg}, cyclos_api::{ api::{ApiErr, CyclosAuth, ErrKind}, @@ -205,7 +204,7 @@ impl Worker<'_> { let res = self .client .direct_payment( - initiated.creditor.0, + initiated.creditor_id, self.payment_type_id, initiated.amount, &initiated.subject, @@ -455,11 +454,9 @@ pub enum Tx { pub fn extract_tx_info(tx: HistoryItem) -> Tx { let amount = amount::decimal(tx.amount.trim_start_matches('-')); - let payto = match tx.related_account.kind { - AccountKind::System => { - FullCyclosPayto::new(tx.related_account.ty.id, &tx.related_account.ty.name) - } - AccountKind::User { user } => FullCyclosPayto::new(user.id, &user.display), + let (id, name) = match tx.related_account.kind { + AccountKind::System => (tx.related_account.ty.id, tx.related_account.ty.name), + AccountKind::User { user } => (user.id, user.display), }; if tx.amount.starts_with("-") { Tx::Out(TxOut { @@ -467,7 +464,8 @@ pub fn extract_tx_info(tx: HistoryItem) -> Tx { tx_id: tx.transaction.map(|it| *it.id), amount, subject: tx.description.unwrap_or_default(), - creditor: payto, + creditor_id: *id, + creditor_name: name, valued_at: tx.date, }) } else { @@ -476,7 +474,8 @@ pub fn extract_tx_info(tx: HistoryItem) -> Tx { tx_id: tx.transaction.map(|it| *it.id), amount, subject: tx.description.unwrap_or_default(), - debtor: payto, + debtor_id: *id, + debtor_name: name, valued_at: tx.date, }) } diff --git a/taler-cyclos/tests/api.rs b/taler-cyclos/tests/api.rs @@ -16,6 +16,7 @@ use std::{str::FromStr, sync::Arc}; +use compact_str::CompactString; use jiff::Timestamp; use sqlx::PgPool; use taler_api::{api::TalerRouter as _, auth::AuthMethod, subject::OutgoingSubject}; @@ -47,6 +48,7 @@ async fn setup() -> (Router, PgPool) { let api = Arc::new( CyclosApi::start( pool.clone(), + CompactString::const_new("localhost"), payto("payto://iban/HU02162000031000164800000000?receiver-name=name"), Currency::from_str("TEST").unwrap(), ) @@ -79,7 +81,7 @@ async fn transfer() { transfer_routine( &server, TransferState::pending, - &payto("payto://cyclos/7762070814178012479?receiver-name=name"), + &payto("payto://cyclos/localhost/7762070814178012479?receiver-name=name"), ) .await; } @@ -111,9 +113,8 @@ async fn outgoing_history() { }, amount: decimal("10"), subject: "subject".to_owned(), - creditor: cyclos_payto( - "payto://cyclos/7762070814178012479?receiver-name=name", - ), + creditor_id: 31000163100000000, + creditor_name: "Name".to_string(), valued_at: Timestamp::now(), }, &TxOutKind::Talerable(OutgoingSubject( @@ -135,7 +136,7 @@ async fn admin_add_incoming() { let (server, _) = setup().await; admin_add_incoming_routine( &server, - &payto("payto://cyclos/7762070814178012479?receiver-name=name"), + &payto("payto://cyclos/localhost/7762070814178012479?receiver-name=name"), true, ) .await; @@ -146,7 +147,7 @@ async fn revenue() { let (server, _) = setup().await; revenue_routine( &server, - &payto("payto://cyclos/7762070814178012479?receiver-name=name"), + &payto("payto://cyclos/localhost/7762070814178012479?receiver-name=name"), true, ) .await; diff --git a/taler-magnet-bank/src/config.rs b/taler-magnet-bank/src/config.rs @@ -100,7 +100,7 @@ impl WorkerCfg { "normal" => { Ok(AccountType::Normal) } ) .require()?, - api_url: sect.parse("URL", "API_URL").require()?, + api_url: sect.base_url("API_URL").require()?, consumer: Token { key: sect.str("CONSUMER_KEY").require()?, secret: sect.str("CONSUMER_SECRET").require()?,