commit d1f3df0bc7123e804dca57d528f979c919793619
parent 4836612545adef76c0329c47e620f7ab34a2032d
Author: Antoine A <>
Date: Tue, 24 Mar 2026 14:08:37 +0100
WIP
Diffstat:
10 files changed, 401 insertions(+), 289 deletions(-)
diff --git a/common/taler-api/db/taler-api-0001.sql b/common/taler-api/db/taler-api-0001.sql
@@ -21,28 +21,39 @@ SET search_path TO taler_api;
CREATE TYPE taler_amount AS (val INT8, frac INT4);
COMMENT ON TYPE taler_amount IS 'Stores an amount, fraction is in units of 1/100000000 of the base value';
-CREATE TYPE incoming_type AS ENUM
- ('reserve' ,'kyc', 'wad');
-COMMENT ON TYPE incoming_type IS 'Types of incoming talerable transactions';
CREATE TABLE tx_in (
tx_in_id INT8 PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
amount taler_amount NOT NULL,
subject TEXT NOT NULL,
debit_payto TEXT NOT NULL,
- type incoming_type NOT NULL,
- metadata BYTEA NOT NULL,
- origin_exchange_url TEXT,
- created_at INT8 NOT NULL,
- CONSTRAINT polymorphism CHECK(
- CASE type
- WHEN 'wad' THEN LENGTH(metadata)=24 AND origin_exchange_url IS NOT NULL
- ELSE LENGTH(metadata)=32 AND origin_exchange_url IS NULL
- END
- )
+ created_at INT8 NOT NULL
);
-CREATE UNIQUE INDEX tx_in_unique_reserve_pub ON tx_in (metadata) WHERE type = 'reserve';
COMMENT ON TABLE tx_in IS 'Incoming transactions';
+CREATE TABLE tx_out (
+ tx_out_id INT8 PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
+ amount taler_amount NOT NULL,
+ subject TEXT NOT NULL,
+ credit_payto TEXT NOT NULL,
+ created_at INT8 NOT NULL
+);
+COMMENT ON TABLE tx_out IS 'Outgoing transactions';
+
+CREATE TYPE incoming_type AS ENUM
+ ('reserve' ,'kyc', 'map');
+COMMENT ON TYPE incoming_type IS 'Types of incoming talerable transactions';
+
+CREATE TABLE taler_in (
+ tx_in_id INT8 PRIMARY KEY REFERENCES tx_in(tx_in_id) ON DELETE CASCADE,
+ type incoming_type NOT NULL,
+ account_pub BYTEA NOT NULL CHECK (LENGTH(account_pub)=32),
+ authorization_pub BYTEA CHECK (LENGTH(authorization_pub)=32),
+ authorization_sig BYTEA CHECK (LENGTH(authorization_sig)=64)
+);
+COMMENT ON TABLE tx_in IS 'Incoming talerable transactions';
+
+CREATE UNIQUE INDEX taler_in_unique_reserve_pub ON taler_in (account_pub) WHERE type = 'reserve';
+
CREATE TYPE transfer_status AS ENUM (
'pending'
,'transient_failure'
@@ -53,14 +64,19 @@ COMMENT ON TYPE transfer_status IS 'Status of a Wire Gateway transfer';
CREATE TABLE transfer (
transfer_id INT8 PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
+ tx_out_id INT8 NOT NULL UNIQUE REFERENCES tx_out(tx_out_id) ON DELETE CASCADE,
request_uid BYTEA UNIQUE NOT NULL CHECK (LENGTH(request_uid)=64),
- amount taler_amount NOT NULL,
- subject TEXT NOT NULL,
- credit_payto TEXT NOT NULL,
wtid BYTEA UNIQUE NOT NULL CHECK (LENGTH(wtid)=32),
exchange_base_url TEXT NOT NULL,
+ metadata TEXT,
status transfer_status NOT NULL,
- status_msg TEXT,
- created_at INT8 NOT NULL
+ status_msg TEXT
);
COMMENT ON TABLE transfer IS 'Wire Gateway transfers';
+
+CREATE TABLE bounced(
+ tx_in_id INT8 NOT NULL UNIQUE REFERENCES tx_in(tx_in_id) ON DELETE CASCADE,
+ tx_out_id INT8 NOT NULL UNIQUE REFERENCES tx_out(tx_out_id) ON DELETE CASCADE,
+ reason TEXT NOT NULL
+);
+COMMENT ON TABLE tx_in IS 'Bounced transaction';
diff --git a/common/taler-api/db/taler-api-0002.sql b/common/taler-api/db/taler-api-0002.sql
@@ -1,21 +0,0 @@
---
--- 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 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 General Public License for more details.
---
--- You should have received a copy of the GNU General Public License along with
--- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-
-SELECT _v.register_patch('taler-api-0002', NULL, NULL);
-
-SET search_path TO taler_api;
-
--- Add outgoing transactions metadata field
-ALTER TABLE transfer ADD COLUMN metadata TEXT;
diff --git a/common/taler-api/db/taler-api-procedures.sql b/common/taler-api/db/taler-api-procedures.sql
@@ -55,6 +55,8 @@ CREATE FUNCTION taler_transfer(
OUT out_created_at INT8
)
LANGUAGE plpgsql AS $$
+DECLARE
+ local_tx_id INT8;
BEGIN
-- Check for idempotence and conflict
SELECT (amount != in_amount
@@ -65,6 +67,7 @@ SELECT (amount != in_amount
,transfer_id, created_at
INTO out_request_uid_reuse, out_transfer_row_id, out_created_at
FROM transfer
+ JOIN tx_out USING (tx_out_id)
WHERE request_uid = in_request_uid;
IF FOUND THEN
RETURN;
@@ -76,28 +79,33 @@ IF out_wtid_reuse THEN
END IF;
out_created_at=in_now;
-- Register exchange
-INSERT INTO transfer (
+INSERT INTO tx_out (
amount,
- exchange_base_url,
- metadata,
subject,
credit_payto,
+ created_at
+) VALUES (
+ in_amount,
+ in_subject,
+ in_credit_payto,
+ in_now
+) RETURNING tx_out_id INTO local_tx_id;
+INSERT INTO transfer (
+ tx_out_id,
+ exchange_base_url,
+ metadata,
request_uid,
wtid,
status,
- status_msg,
- created_at
+ status_msg
) VALUES (
- in_amount,
+ local_tx_id,
in_exchange_base_url,
in_metadata,
- in_subject,
- in_credit_payto,
in_request_uid,
in_wtid,
'success',
- NULL,
- in_now
+ NULL
) RETURNING transfer_id INTO out_transfer_row_id;
-- Notify new transaction
PERFORM pg_notify('outgoing_tx', out_transfer_row_id || '');
@@ -109,7 +117,7 @@ CREATE FUNCTION add_incoming(
IN in_subject TEXT,
IN in_debit_payto TEXT,
IN in_type incoming_type,
- IN in_metadata BYTEA,
+ IN in_account_pub BYTEA,
IN in_now INT8,
-- Error status
OUT out_reserve_pub_reuse BOOLEAN,
@@ -120,7 +128,7 @@ CREATE FUNCTION add_incoming(
LANGUAGE plpgsql AS $$
BEGIN
-- Check conflict
-SELECT in_type = 'reserve'::incoming_type AND EXISTS(SELECT FROM tx_in WHERE metadata = in_metadata AND type = 'reserve')
+SELECT in_type = 'reserve'::incoming_type AND EXISTS(SELECT FROM taler_in WHERE account_pub = in_account_pub AND type = 'reserve')
INTO out_reserve_pub_reuse;
IF out_reserve_pub_reuse THEN
RETURN;
@@ -131,19 +139,22 @@ INSERT INTO tx_in (
amount,
debit_payto,
created_at,
- subject,
- type,
- metadata,
- origin_exchange_url
+ subject
) VALUES (
in_amount,
in_debit_payto,
in_now,
- in_subject,
- in_type,
- in_metadata,
- NULL
+ in_subject
) RETURNING tx_in_id INTO out_tx_row_id;
+INSERT INTO taler_in (
+ tx_in_id,
+ type,
+ account_pub
+) VALUES (
+ out_tx_row_id,
+ in_type,
+ in_account_pub
+);
-- Notify new incoming transaction
PERFORM pg_notify('incoming_tx', out_tx_row_id || '');
END $$;
diff --git a/common/taler-api/src/db.rs b/common/taler-api/src/db.rs
@@ -46,7 +46,7 @@ pub type PgQueryBuilder<'b> = QueryBuilder<'b, Postgres>;
pub enum IncomingType {
reserve,
kyc,
- wad,
+ map,
}
/* ------ Serialization ----- */
diff --git a/common/taler-api/src/subject.rs b/common/taler-api/src/subject.rs
@@ -19,6 +19,7 @@ use std::{
str::FromStr,
};
+use aws_lc_rs::digest::{SHA256, digest};
use compact_str::CompactString;
use taler_common::{
api_common::{EddsaPublicKey, ShortHashCode},
@@ -33,6 +34,8 @@ use crate::db::IncomingType;
pub enum IncomingSubject {
Reserve(EddsaPublicKey),
Kyc(EddsaPublicKey),
+ Map(EddsaPublicKey),
+ AdminBalanceAdjust,
}
impl IncomingSubject {
@@ -40,12 +43,17 @@ impl IncomingSubject {
match self {
IncomingSubject::Reserve(_) => IncomingType::reserve,
IncomingSubject::Kyc(_) => IncomingType::kyc,
+ IncomingSubject::Map(_) => IncomingType::map,
+ IncomingSubject::AdminBalanceAdjust => panic!("Admin balance adjust"),
}
}
pub fn key(&self) -> &[u8] {
match self {
- IncomingSubject::Kyc(key) | IncomingSubject::Reserve(key) => key.as_ref(),
+ IncomingSubject::Kyc(key)
+ | IncomingSubject::Reserve(key)
+ | IncomingSubject::Map(key) => key.as_ref(),
+ IncomingSubject::AdminBalanceAdjust => panic!("Admin balance adjust"),
}
}
}
@@ -162,7 +170,7 @@ pub fn fmt_in_subject(ty: IncomingType, key: &EddsaPublicKey) -> String {
match ty {
IncomingType::reserve => format!("{key}"),
IncomingType::kyc => format!("KYC:{key}"),
- IncomingType::wad => unreachable!(),
+ IncomingType::map => format!("MAP:{key}"),
}
}
@@ -182,22 +190,31 @@ pub fn parse_incoming_unstructured(
assert!(subject.len() <= u16::MAX as usize);
const KEY_SIZE: usize = 52;
- const KYC_SIZE: usize = KEY_SIZE + 3;
+ const PREFIXED_SIZE: usize = KEY_SIZE + 3;
+ const ADMIN_BALANCE_ADJUST: &str = "ADMINBALANCEADJUST";
/** Parse an incoming subject */
#[inline]
fn parse_single(str: &str) -> Option<Candidate> {
+ if str == ADMIN_BALANCE_ADJUST {
+ return Some(Candidate {
+ subject: IncomingSubject::AdminBalanceAdjust,
+ quality: Base32Quality::UpperStandard,
+ });
+ }
// Check key type
- let (is_kyc, raw) = match str.len() {
- KEY_SIZE => (false, str),
- KYC_SIZE => {
+ let (ty, raw) = match str.len() {
+ KEY_SIZE => (IncomingType::reserve, str),
+ PREFIXED_SIZE => {
if let Some(key) = str.strip_prefix("KYC") {
- (true, key)
+ (IncomingType::kyc, key)
+ } else if let Some(key) = str.strip_prefix("MAP") {
+ (IncomingType::map, key)
} else {
return None;
}
}
- _ => unreachable!(),
+ _ => return None,
};
// Check key validity
@@ -205,10 +222,10 @@ pub fn parse_incoming_unstructured(
let quality = Base32Quality::measure(raw);
Some(Candidate {
- subject: if is_kyc {
- IncomingSubject::Kyc(key)
- } else {
- IncomingSubject::Reserve(key)
+ subject: match ty {
+ IncomingType::reserve => IncomingSubject::Reserve(key),
+ IncomingType::kyc => IncomingSubject::Kyc(key),
+ IncomingType::map => IncomingSubject::Map(key),
},
quality,
})
@@ -217,7 +234,7 @@ pub fn parse_incoming_unstructured(
// Find and concatenate valid parts of a keys
let (parts, concatenated) = {
let mut parts = Vec::with_capacity(4);
- let mut concatenated = String::with_capacity(subject.len().min(KYC_SIZE + 10));
+ let mut concatenated = String::with_capacity(subject.len().min(PREFIXED_SIZE + 10));
parts.push(0u16);
for part in subject.as_bytes().split(|b| !b.is_ascii_alphanumeric()) {
// SAFETY: part are all valid ASCII alphanumeric
@@ -235,9 +252,9 @@ pub fn parse_incoming_unstructured(
for &end in parts[i..].iter().skip(1) {
let len = (end - start) as usize;
// Until they are to long to be a key
- if len > KYC_SIZE {
+ if len > PREFIXED_SIZE {
break;
- } else if len != KEY_SIZE && len != KYC_SIZE {
+ } else if len != KEY_SIZE && len != PREFIXED_SIZE && len != ADMIN_BALANCE_ADJUST.len() {
continue;
}
@@ -251,7 +268,7 @@ pub fn parse_incoming_unstructured(
if other.quality > best.quality // We prefer high quality keys
|| matches!( // We prefer prefixed keys over reserve keys
(&best.subject.ty(), &other.subject.ty()),
- (IncomingType::reserve, IncomingType::kyc | IncomingType::wad)
+ (IncomingType::reserve, IncomingType::kyc | IncomingType::map)
)
{
*best = other
@@ -259,7 +276,7 @@ pub fn parse_incoming_unstructured(
&& best.quality == other.quality // Of same quality
&& !matches!( // And prefixing is different
(&best.subject.ty(), &other.subject.ty()),
- (IncomingType::kyc | IncomingType::wad, IncomingType::reserve)
+ (IncomingType::kyc | IncomingType::map, IncomingType::reserve)
)
{
return Err(IncomingSubjectErr::Ambiguous);
@@ -274,209 +291,292 @@ pub fn parse_incoming_unstructured(
Ok(best.map(|it| it.subject))
}
-#[test]
-/** Test parsing logic */
-fn parse() {
- let key = "4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0";
- let other = "00Q979QSMJ29S7BJT3DDAVC5A0DR5Z05B7N0QT1RCBQ8FXJPZ6RG";
-
- // Common checks
- for ty in [IncomingType::reserve, IncomingType::kyc] {
- let prefix = match ty {
- IncomingType::reserve => "",
- IncomingType::kyc => "KYC",
- IncomingType::wad => unreachable!(),
- };
- let standard = &format!("{prefix}{key}");
- let (standard_l, standard_r) = standard.split_at(standard.len() / 2);
- let mixed = &format!("{prefix}4mzt6RS3rvb3b0e2rdmyw0yra3y0vphyv0cyde6xbb0ympfxceg0");
- let (mixed_l, mixed_r) = mixed.split_at(mixed.len() / 2);
- let other_standard = &format!("{prefix}{other}");
- let other_mixed = &format!("{prefix}TEGY6d9mh9pgwvwpgs0z0095z854xegfy7jj202yd0esp8p0za60");
-
- let result = Ok(Some(match ty {
- IncomingType::reserve => {
- IncomingSubject::Reserve(EddsaPublicKey::from_str(key).unwrap())
+// Modulo 10 Recursive
+fn mod10_recursive(bytes: &[u8]) -> u8 {
+ const LOOKUP_TABLE: [u8; 10] = [0, 9, 4, 6, 8, 2, 7, 1, 3, 5];
+ // Modulo 10 Recursive calculation
+ let mut carry = 0u8;
+ for &b in bytes {
+ // ASCII '0'-'9' is 0x30-0x39. Subtracting b'0' (48) gives the integer.
+ let digit = b - b'0';
+ carry = LOOKUP_TABLE[((carry + digit) % 10) as usize];
+ }
+ carry
+}
+
+/// Encode a public key as a QR-Bill reference
+pub fn subject_fmt_qr_bill(key_bytes: &[u8]) -> String {
+ // High-Entropy Hash (SHA-256) to ensure even distribution
+ let hash = digest(&SHA256, key_bytes);
+
+ // Compute hash % 10^26
+ let hash_mod = hash.as_ref().chunks(3).fold(0u128, |rem, chunk| {
+ chunk.iter().fold(rem, |r, &b| r * 256 + b as u128) % 10u128.pow(26)
+ });
+
+ // Format to 26 digits with leading zeros
+ let reference_base = format!("{:0>26}", hash_mod);
+
+ // Modulo 10 Recursive calculation
+ let carry = mod10_recursive(reference_base.as_bytes());
+ let checksum = (10 - carry) % 10;
+
+ // Combine base (26) + checksum (1) = 27 characters
+ format!("{}{}", reference_base, checksum)
+}
+
+/// Check if a string is a valid QR-Bill reference
+pub fn subject_is_qr_bill(reference: &str) -> bool {
+ // Quick length and numeric check
+ if reference.len() != 27 || !reference.chars().all(|c| c.is_ascii_digit()) {
+ return false;
+ }
+
+ // If the check digit is correct, the final carry will be 0
+ mod10_recursive(reference.as_bytes()) == 0
+}
+
+#[cfg(test)]
+mod test {
+ use std::str::FromStr as _;
+
+ use taler_common::{api_common::{EddsaPublicKey, ShortHashCode}, types::url};
+
+ use crate::{
+ db::IncomingType,
+ subject::{
+ IncomingSubject, IncomingSubjectErr, OutgoingSubject, fmt_out_subject, mod10_recursive,
+ parse_incoming_unstructured, parse_outgoing, subject_fmt_qr_bill, subject_is_qr_bill,
+ },
+ };
+
+ #[test]
+ fn qrbill() {
+ let reference = "210000000003139471430009017";
+
+ let input = "21000000000313947143000901";
+ let carry = mod10_recursive(input.as_bytes());
+ let checksum = (10 - carry) % 10;
+ assert_eq!(checksum, 7);
+
+ assert_eq!(mod10_recursive(reference.as_bytes()), 0);
+ assert!(subject_is_qr_bill(reference));
+ assert!(!subject_is_qr_bill(input));
+ assert!(!subject_is_qr_bill(""));
+ assert!(!subject_is_qr_bill("210000000003139471430009019"));
+ assert!(!subject_is_qr_bill("21000000000313947143000901A"));
+
+ let key = "4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0";
+ let key = EddsaPublicKey::from_str(key).unwrap();
+ assert_eq!(
+ subject_fmt_qr_bill(key.as_ref()),
+ "442862674560948379842733643"
+ );
+ }
+
+ #[test]
+ /** Test parsing logic */
+ fn incoming_parse() {
+ let key = "4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0";
+ let other = "00Q979QSMJ29S7BJT3DDAVC5A0DR5Z05B7N0QT1RCBQ8FXJPZ6RG";
+
+ // Common checks
+ for ty in [IncomingType::reserve, IncomingType::kyc, IncomingType::map] {
+ let prefix = match ty {
+ IncomingType::reserve => "",
+ IncomingType::kyc => "KYC",
+ IncomingType::map => "MAP",
+ };
+ let standard = &format!("{prefix}{key}");
+ let (standard_l, standard_r) = standard.split_at(standard.len() / 2);
+ let mixed = &format!("{prefix}4mzt6RS3rvb3b0e2rdmyw0yra3y0vphyv0cyde6xbb0ympfxceg0");
+ let (mixed_l, mixed_r) = mixed.split_at(mixed.len() / 2);
+ let other_standard = &format!("{prefix}{other}");
+ let other_mixed =
+ &format!("{prefix}TEGY6d9mh9pgwvwpgs0z0095z854xegfy7jj202yd0esp8p0za60");
+ let key = EddsaPublicKey::from_str(key).unwrap();
+ let result = Ok(Some(match ty {
+ IncomingType::reserve => IncomingSubject::Reserve(key),
+ IncomingType::kyc => IncomingSubject::Kyc(key),
+ IncomingType::map => IncomingSubject::Map(key),
+ }));
+
+ // Check succeed if standard or mixed
+ for case in [standard, mixed] {
+ for test in [
+ format!("noise {case} noise"),
+ format!("{case} noise to the right"),
+ format!("noise to the left {case}"),
+ format!(" {case} "),
+ format!("noise\n{case}\nnoise"),
+ format!("Test+{case}"),
+ ] {
+ assert_eq!(parse_incoming_unstructured(&test), result);
+ }
}
- IncomingType::kyc => IncomingSubject::Kyc(EddsaPublicKey::from_str(key).unwrap()),
- IncomingType::wad => unreachable!(),
- }));
-
- // Check succeed if standard or mixed
- for case in [standard, mixed] {
- for test in [
- format!("noise {case} noise"),
- format!("{case} noise to the right"),
- format!("noise to the left {case}"),
- format!(" {case} "),
- format!("noise\n{case}\nnoise"),
- format!("Test+{case}"),
- ] {
- assert_eq!(parse_incoming_unstructured(&test), result);
+
+ // Check succeed if standard or mixed and split
+ for (l, r) in [(standard_l, standard_r), (mixed_l, mixed_r)] {
+ for case in [
+ format!("left {l}{r} right"),
+ format!("left {l} {r} right"),
+ format!("left {l}-{r} right"),
+ format!("left {l}+{r} right"),
+ format!("left {l}\n{r} right"),
+ format!("left {l}-+\n{r} right"),
+ format!("left {l} - {r} right"),
+ format!("left {l} + {r} right"),
+ format!("left {l} \n {r} right"),
+ format!("left {l} - + \n {r} right"),
+ ] {
+ assert_eq!(parse_incoming_unstructured(&case), result);
+ }
+ }
+
+ // Check concat parts
+ for chunk_size in 1..standard.len() {
+ let chunked: String = standard
+ .as_bytes()
+ .chunks(chunk_size)
+ .flat_map(|c| [std::str::from_utf8(c).unwrap(), " "])
+ .collect();
+ for case in [chunked.clone(), format!("left {chunked} right")] {
+ assert_eq!(parse_incoming_unstructured(&case), result);
+ }
}
- }
- // Check succeed if standard or mixed and split
- for (l, r) in [(standard_l, standard_r), (mixed_l, mixed_r)] {
+ // Check failed when multiple key
for case in [
- format!("left {l}{r} right"),
- format!("left {l} {r} right"),
- format!("left {l}-{r} right"),
- format!("left {l}+{r} right"),
- format!("left {l}\n{r} right"),
- format!("left {l}-+\n{r} right"),
- format!("left {l} - {r} right"),
- format!("left {l} + {r} right"),
- format!("left {l} \n {r} right"),
- format!("left {l} - + \n {r} right"),
+ format!("{standard} {other_standard}"),
+ format!("{mixed} {other_mixed}"),
] {
- assert_eq!(parse_incoming_unstructured(&case), result);
+ assert_eq!(
+ parse_incoming_unstructured(&case),
+ Err(IncomingSubjectErr::Ambiguous)
+ );
}
- }
- // Check concat parts
- for chunk_size in 1..standard.len() {
- let chunked: String = standard
- .as_bytes()
- .chunks(chunk_size)
- .flat_map(|c| [std::str::from_utf8(c).unwrap(), " "])
- .collect();
- for case in [chunked.clone(), format!("left {chunked} right")] {
+ // Check accept redundant key
+ for case in [
+ format!("{standard} {standard} {mixed} {mixed}"), // Accept redundant key
+ format!("{standard} {other_mixed}"), // Prefer high quality
+ ] {
assert_eq!(parse_incoming_unstructured(&case), result);
}
- }
- // Check failed when multiple key
- for case in [
- format!("{standard} {other_standard}"),
- format!("{mixed} {other_mixed}"),
- ] {
- assert_eq!(
- parse_incoming_unstructured(&case),
- Err(IncomingSubjectErr::Ambiguous)
- );
- }
+ // Check prefer prefixed over simple
+ for case in [format!("{mixed_l}-{mixed_r} {standard_l}-{standard_r}")] {
+ let res = parse_incoming_unstructured(&case);
+ if ty == IncomingType::reserve {
+ assert_eq!(res, Err(IncomingSubjectErr::Ambiguous));
+ } else {
+ assert_eq!(res, result);
+ }
+ }
- // Check accept redundant key
- for case in [
- format!("{standard} {standard} {mixed} {mixed}"), // Accept redundant key
- format!("{standard} {other_mixed}"), // Prefer high quality
- ] {
- assert_eq!(parse_incoming_unstructured(&case), result);
- }
+ // Check failure if malformed or missing
+ for case in [
+ "does not contain any reserve", // Check fail if none
+ &standard[1..], // Check fail if missing char
+ //"2MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0", // Check fail if not a valid key
+ ] {
+ assert_eq!(parse_incoming_unstructured(case), Ok(None));
+ }
- // Check prefer prefixed over simple
- for case in [format!("{mixed_l}-{mixed_r} {standard_l}-{standard_r}")] {
- let res = parse_incoming_unstructured(&case);
- if ty == IncomingType::reserve {
- assert_eq!(res, Err(IncomingSubjectErr::Ambiguous));
- } else {
- assert_eq!(res, result);
+ if ty == IncomingType::kyc || ty == IncomingType::map {
+ // Prefer prefixed over unprefixed
+ for case in [format!("{other} {standard}"), format!("{other} {mixed}")] {
+ assert_eq!(parse_incoming_unstructured(&case), result);
+ }
}
}
+ }
- // Check failure if malformed or missing
- for case in [
- "does not contain any reserve", // Check fail if none
- &standard[1..], // Check fail if missing char
- //"2MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0", // Check fail if not a valid key
+ #[test]
+ /** Test parsing logic using real cases */
+ fn real() {
+ // Good reserve case
+ for (subject, key) in [
+ (
+ "Taler TEGY6d9mh9pgwvwpgs0z0095z854xegfy7j j202yd0esp8p0za60",
+ "TEGY6d9mh9pgwvwpgs0z0095z854xegfy7jj202yd0esp8p0za60",
+ ),
+ (
+ "00Q979QSMJ29S7BJT3DDAVC5A0DR5Z05B7N 0QT1RCBQ8FXJPZ6RG",
+ "00Q979QSMJ29S7BJT3DDAVC5A0DR5Z05B7N0QT1RCBQ8FXJPZ6RG",
+ ),
+ (
+ "Taler NDDCAM9XN4HJZFTBD8V6FNE2FJE8G Y734PJ5AGQMY06C8D4HB3Z0",
+ "NDDCAM9XN4HJZFTBD8V6FNE2FJE8GY734PJ5AGQMY06C8D4HB3Z0",
+ ),
+ (
+ "KYCVEEXTBXBEMCS5R64C24GFNQVWBN5R2F9QSQ7PN8QXAP1NG4NG",
+ "KYCVEEXTBXBEMCS5R64C24GFNQVWBN5R2F9QSQ7PN8QXAP1NG4NG",
+ ),
] {
- assert_eq!(parse_incoming_unstructured(case), Ok(None));
+ assert_eq!(
+ Ok(Some(IncomingSubject::Reserve(
+ EddsaPublicKey::from_str(key).unwrap(),
+ ))),
+ parse_incoming_unstructured(subject)
+ )
}
-
- if ty == IncomingType::kyc {
- // Prefer prefixed over unprefixed
- for case in [format!("{other} {standard}"), format!("{other} {mixed}")] {
- assert_eq!(parse_incoming_unstructured(&case), result);
- }
+ // Good kyc case
+ for (subject, key) in [(
+ "KYC JW398X85FWPKKMS0EYB6TQ1799RMY5DDXTZ FPW4YC3WJ2DWSJT70",
+ "JW398X85FWPKKMS0EYB6TQ1799RMY5DDXTZFPW4YC3WJ2DWSJT70",
+ )] {
+ assert_eq!(
+ Ok(Some(IncomingSubject::Kyc(
+ EddsaPublicKey::from_str(key).unwrap(),
+ ))),
+ parse_incoming_unstructured(subject)
+ )
}
}
-}
-#[test]
-/** Test parsing logic using real cases */
-fn real() {
- // Good reserve case
- for (subject, key) in [
- (
- "Taler TEGY6d9mh9pgwvwpgs0z0095z854xegfy7j j202yd0esp8p0za60",
- "TEGY6d9mh9pgwvwpgs0z0095z854xegfy7jj202yd0esp8p0za60",
- ),
- (
- "00Q979QSMJ29S7BJT3DDAVC5A0DR5Z05B7N 0QT1RCBQ8FXJPZ6RG",
- "00Q979QSMJ29S7BJT3DDAVC5A0DR5Z05B7N0QT1RCBQ8FXJPZ6RG",
- ),
- (
- "Taler NDDCAM9XN4HJZFTBD8V6FNE2FJE8G Y734PJ5AGQMY06C8D4HB3Z0",
- "NDDCAM9XN4HJZFTBD8V6FNE2FJE8GY734PJ5AGQMY06C8D4HB3Z0",
- ),
- (
- "KYCVEEXTBXBEMCS5R64C24GFNQVWBN5R2F9QSQ7PN8QXAP1NG4NG",
- "KYCVEEXTBXBEMCS5R64C24GFNQVWBN5R2F9QSQ7PN8QXAP1NG4NG",
- ),
- ] {
+ #[test]
+ fn outgoing() {
+ let key = ShortHashCode::rand();
+
+ // Without metadata
+ let subject = format!("{key} http://exchange.example.com/");
+ let parsed = parse_outgoing(&subject).unwrap();
assert_eq!(
- Ok(Some(IncomingSubject::Reserve(
- EddsaPublicKey::from_str(key).unwrap(),
- ))),
- parse_incoming_unstructured(subject)
- )
- }
- // Good kyc case
- for (subject, key) in [(
- "KYC JW398X85FWPKKMS0EYB6TQ1799RMY5DDXTZ FPW4YC3WJ2DWSJT70",
- "JW398X85FWPKKMS0EYB6TQ1799RMY5DDXTZFPW4YC3WJ2DWSJT70",
- )] {
+ parsed,
+ OutgoingSubject {
+ wtid: key.clone(),
+ exchange_base_url: url("http://exchange.example.com/"),
+ metadata: None
+ }
+ );
assert_eq!(
- Ok(Some(IncomingSubject::Kyc(
- EddsaPublicKey::from_str(key).unwrap(),
- ))),
- parse_incoming_unstructured(subject)
- )
+ subject,
+ fmt_out_subject(
+ &parsed.wtid,
+ &parsed.exchange_base_url,
+ parsed.metadata.as_deref()
+ )
+ );
+
+ // With metadata
+ let subject = format!("Accounting:id.4 {key} http://exchange.example.com/");
+ let parsed = parse_outgoing(&subject).unwrap();
+ assert_eq!(
+ parsed,
+ OutgoingSubject {
+ wtid: key.clone(),
+ exchange_base_url: url("http://exchange.example.com/"),
+ metadata: Some("Accounting:id.4".into())
+ }
+ );
+ assert_eq!(
+ subject,
+ fmt_out_subject(
+ &parsed.wtid,
+ &parsed.exchange_base_url,
+ parsed.metadata.as_deref()
+ )
+ );
}
}
-
-#[test]
-fn outgoing() {
- let key = ShortHashCode::rand();
-
- // Without metadata
- let subject = format!("{key} http://exchange.example.com/");
- let parsed = parse_outgoing(&subject).unwrap();
- assert_eq!(
- parsed,
- OutgoingSubject {
- wtid: key.clone(),
- exchange_base_url: url("http://exchange.example.com/"),
- metadata: None
- }
- );
- assert_eq!(
- subject,
- fmt_out_subject(
- &parsed.wtid,
- &parsed.exchange_base_url,
- parsed.metadata.as_deref()
- )
- );
-
- // With metadata
- let subject = format!("Accounting:id.4 {key} http://exchange.example.com/");
- let parsed = parse_outgoing(&subject).unwrap();
- assert_eq!(
- parsed,
- OutgoingSubject {
- wtid: key.clone(),
- exchange_base_url: url("http://exchange.example.com/"),
- metadata: Some("Accounting:id.4".into())
- }
- );
- assert_eq!(
- subject,
- fmt_out_subject(
- &parsed.wtid,
- &parsed.exchange_base_url,
- parsed.metadata.as_deref()
- )
- );
-}
diff --git a/common/taler-api/tests/common/db.rs b/common/taler-api/tests/common/db.rs
@@ -109,7 +109,9 @@ pub async fn transfer_page(
amount,
credit_payto,
created_at
- FROM transfer WHERE
+ FROM transfer
+ JOIN tx_out USING (tx_out_id)
+ WHERE
",
);
if let Some(status) = status {
@@ -147,7 +149,9 @@ pub async fn transfer_by_id(
wtid,
credit_payto,
created_at
- FROM transfer WHERE transfer_id = $1
+ FROM transfer
+ JOIN tx_out USING (tx_out_id)
+ WHERE transfer_id = $1
",
)
.bind(id as i64)
@@ -189,7 +193,9 @@ pub async fn outgoing_revenue(
wtid,
credit_payto,
created_at
- FROM transfer WHERE status = 'success' AND
+ FROM transfer
+ JOIN tx_out USING (tx_out_id)
+ WHERE status = 'success' AND
",
)
},
@@ -270,9 +276,12 @@ pub async fn incoming_history(
amount,
created_at,
debit_payto,
- metadata,
- origin_exchange_url
- FROM tx_in WHERE
+ account_pub,
+ authorization_pub,
+ authorization_sig
+ FROM tx_in
+ JOIN taler_in USING (tx_in_id)
+ WHERE
",
)
},
@@ -284,9 +293,9 @@ pub async fn incoming_history(
amount: r.try_get_amount("amount", currency)?,
credit_fee: None,
debit_account: r.try_get_payto("debit_payto")?,
- reserve_pub: r.try_get("metadata")?,
- authorization_pub: None,
- authorization_sig: None,
+ reserve_pub: r.try_get("account_pub")?,
+ authorization_pub: r.try_get("authorization_pub")?,
+ authorization_sig: r.try_get("authorization_sig")?,
},
IncomingType::kyc => IncomingBankTransaction::Kyc {
row_id: r.try_get_safeu64("tx_in_id")?,
@@ -294,18 +303,11 @@ pub async fn incoming_history(
amount: r.try_get_amount("amount", currency)?,
credit_fee: None,
debit_account: r.try_get_payto("debit_payto")?,
- account_pub: r.try_get("metadata")?,
- authorization_pub: None,
- authorization_sig: None,
- },
- IncomingType::wad => IncomingBankTransaction::Wad {
- row_id: r.try_get_safeu64("tx_in_id")?,
- date: r.try_get_timestamp("created_at")?.into(),
- amount: r.try_get_amount("amount", currency)?,
- debit_account: r.try_get_payto("debit_payto")?,
- origin_exchange_url: r.try_get_url("origin_exchange_url")?,
- wad_id: r.try_get("metadata")?,
+ account_pub: r.try_get("account_pub")?,
+ authorization_pub: r.try_get("authorization_pub")?,
+ authorization_sig: r.try_get("authorization_sig")?,
},
+ IncomingType::map => unimplemented!("MAP are never listed in the history"),
})
},
)
@@ -332,7 +334,8 @@ pub async fn revenue_history(
created_at,
debit_payto,
subject
- FROM tx_in WHERE
+ FROM tx_in
+ WHERE
",
)
},
diff --git a/common/taler-common/src/types/payto.rs b/common/taler-common/src/types/payto.rs
@@ -495,5 +495,12 @@ mod test {
TransferPayto::from_str(&transfer_payto.as_payto().to_string()).unwrap()
);
assert_eq!(full_payto, transfer_payto.clone().into());
+
+ let malformed = FullPayto::<IBAN>::from_str(
+ "payto://iban/CH0400766000103138557?receiver-name=NYM%20Technologies%SA",
+ )
+ .unwrap();
+ assert_eq!(malformed.as_ref().to_string(), "CH0400766000103138557");
+ assert_eq!(malformed.name, "NYM Technologies%SA");
}
}
diff --git a/common/taler-test-utils/src/routine.rs b/common/taler-test-utils/src/routine.rs
@@ -481,7 +481,7 @@ async fn add_incoming_routine(
let (path, key) = match kind {
IncomingType::reserve => ("/taler-wire-gateway/admin/add-incoming", "reserve_pub"),
IncomingType::kyc => ("/taler-wire-gateway/admin/add-kycauth", "account_pub"),
- IncomingType::wad => unreachable!(),
+ IncomingType::map => unreachable!(),
};
let valid_req = json!({
"amount": format!("{currency}:44"),
@@ -507,7 +507,7 @@ async fn add_incoming_routine(
// Non conflict on reuse
server.post(path).json(&valid_req).await.assert_ok();
}
- IncomingType::wad => unreachable!(),
+ IncomingType::map => unreachable!(),
}
// Currency mismatch
diff --git a/taler-cyclos/src/db.rs b/taler-cyclos/src/db.rs
@@ -587,9 +587,7 @@ pub async fn incoming_history(
authorization_pub: None,
authorization_sig: None,
},
- IncomingType::wad => {
- unimplemented!("WAD is not yet supported")
- }
+ IncomingType::map => unimplemented!("MAP are never listed in the history"),
})
},
)
diff --git a/taler-magnet-bank/src/db.rs b/taler-magnet-bank/src/db.rs
@@ -601,9 +601,7 @@ pub async fn incoming_history(
authorization_pub: None,
authorization_sig: None,
},
- IncomingType::wad => {
- unimplemented!("WAD is not yet supported")
- }
+ IncomingType::map => unimplemented!("MAP are never listed in the history"),
})
},
)