commit f86929453485b03617e253d5ab4c30fb811635df
parent e5031ab9f90c5b7fea7c23f6ec55dbd80619bdf5
Author: Antoine A <>
Date: Wed, 24 Nov 2021 15:28:29 +0100
Pass simple bank test
Diffstat:
7 files changed, 698 insertions(+), 300 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -3,6 +3,12 @@
version = 3
[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
name = "argh"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -32,6 +38,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38de00daab4eac7d753e97697066238d67ce9d7e2d823ab4f72fe14af29f3f33"
[[package]]
+name = "async-compression"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443ccbb270374a2b1055fc72da40e1f237809cd6bb0e97e66d264cd138473a6"
+dependencies = [
+ "flate2",
+ "futures-core",
+ "memchr",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -194,6 +213,15 @@ dependencies = [
]
[[package]]
+name = "crc32fast"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3825b1e8580894917dc4468cb634a1b4e9745fddc854edad72d9c04644c0319f"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
name = "criterion"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -409,12 +437,34 @@ dependencies = [
]
[[package]]
+name = "flate2"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f"
+dependencies = [
+ "cfg-if",
+ "crc32fast",
+ "libc",
+ "miniz_oxide",
+]
+
+[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
+name = "form_urlencoded"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
+dependencies = [
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
name = "futures-channel"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -622,6 +672,12 @@ dependencies = [
]
[[package]]
+name = "matches"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
+
+[[package]]
name = "memchr"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -637,6 +693,16 @@ dependencies = [
]
[[package]]
+name = "miniz_oxide"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
+dependencies = [
+ "adler",
+ "autocfg",
+]
+
+[[package]]
name = "mio"
version = "0.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -752,6 +818,12 @@ dependencies = [
]
[[package]]
+name = "percent-encoding"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
+
+[[package]]
name = "pin-project-lite"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1057,6 +1129,18 @@ dependencies = [
]
[[package]]
+name = "serde_urlencoded"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
name = "serde_with"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1379,10 +1463,13 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
name = "wire-gateway"
version = "0.1.0"
dependencies = [
+ "async-compression",
"base32",
"hyper",
+ "rand",
"serde",
"serde_json",
+ "serde_urlencoded",
"serde_with",
"thiserror",
"tokio",
diff --git a/script/test_bank.sh b/script/test_bank.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+
+set -eu
+
+# Cleanup to run whenever we exit
+function cleanup()
+{
+ for n in `jobs -p`
+ do
+ kill $n 2> /dev/null || true
+ done
+ wait
+}
+
+# Install cleanup handler (except for kill -9)
+trap cleanup EXIT
+
+echo "OK"
+
+BANK_ENDPOINT=http://localhost:8080/
+
+echo -n "Making wire transfer to exchange ..."
+
+taler-exchange-wire-gateway-client \
+ -b $BANK_ENDPOINT \
+ -S 0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00 \
+ -D payto://x-taler-bank/localhost:8899/user \
+ -a TESTKUDOS:4 > /dev/null
+echo " OK"
+
+echo -n "Requesting exchange incoming transaction list ..."
+
+taler-exchange-wire-gateway-client -b $BANK_ENDPOINT -i | grep TESTKUDOS:4 > /dev/null
+
+echo " OK"
+
+echo -n "Making wire transfer from exchange..."
+
+taler-exchange-wire-gateway-client \
+ -b $BANK_ENDPOINT \
+ -S 0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00 \
+ -C payto://x-taler-bank/$BANK_ENDPOINT/merchant \
+ -a TESTKUDOS:2 > /dev/null
+echo " OK"
+
+
+echo -n "Requesting exchange's outgoing transaction list..."
+
+taler-exchange-wire-gateway-client -b $BANK_ENDPOINT -o | grep TESTKUDOS:2 > /dev/null
+
+echo " OK"
+
+echo "All tests passed"
+
+exit 0
diff --git a/wire-gateway/Cargo.toml b/wire-gateway/Cargo.toml
@@ -16,7 +16,13 @@ serde = { version = "1.0.130", features = ["derive"] }
serde_with = "1.11.0"
# JSON serialization
serde_json = "1.0.71"
+# Url query serialization
+serde_urlencoded = "0.7.0"
# Crockford’s base32
base32 = "0.4.0"
# Error macros
thiserror = "1.0.30"
+# Async friendly compression
+async-compression = { version = "0.3.8", features = ["tokio", "zlib"] }
+# Rng
+rand = { version = "0.8.4", features = ["getrandom"] }
diff --git a/wire-gateway/src/api_common.rs b/wire-gateway/src/api_common.rs
@@ -0,0 +1,317 @@
+use std::{
+ fmt::Display,
+ num::ParseIntError,
+ str::FromStr,
+ time::{Duration, SystemTime},
+};
+
+use serde::{de::Error, ser::SerializeStruct, Deserialize, Deserializer};
+use serde_json::Value;
+
+/// <https://docs.taler.net/core/api-common.html#tsref-type-ErrorDetail>
+#[derive(Debug, Clone, serde::Serialize)]
+struct ErrorDetail {
+ code: i64,
+ hint: Option<String>,
+ detail: Option<String>,
+ parameter: Option<String>,
+ path: Option<String>,
+ offset: Option<String>,
+ index: Option<String>,
+ object: Option<String>,
+ currency: Option<String>,
+ type_expected: Option<String>,
+ type_actual: Option<String>,
+}
+
+/// <https://docs.taler.net/core/api-common.html#tsref-type-Timestamp>
+#[derive(Debug, Clone, Copy)]
+pub enum Timestamp {
+ Never,
+ Time(SystemTime),
+}
+
+#[derive(serde::Serialize, serde::Deserialize)]
+struct TimestampImpl {
+ t_ms: Value,
+}
+
+impl Timestamp {
+ pub fn now() -> Self {
+ Self::Time(SystemTime::now())
+ }
+}
+
+impl<'de> Deserialize<'de> for Timestamp {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let tmp = TimestampImpl::deserialize(deserializer)?;
+ match tmp.t_ms {
+ Value::Number(ms) => {
+ if let Some(since_epoch_ms) = ms.as_u64() {
+ Ok(Self::Time(
+ SystemTime::UNIX_EPOCH + Duration::from_millis(since_epoch_ms),
+ ))
+ } else {
+ Err(todo!())
+ }
+ }
+ Value::String(str) if str == "never" => Ok(Self::Never),
+ it => Err(todo!()),
+ }
+ }
+}
+
+impl serde::Serialize for Timestamp {
+ fn serialize<S>(&self, se: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ let mut se_struct = se.serialize_struct("Timestamp", 1)?;
+ match self {
+ Timestamp::Never => se_struct.serialize_field("t_ms", "never")?,
+ Timestamp::Time(time) => se_struct.serialize_field(
+ "t_ms",
+ &(time
+ .duration_since(SystemTime::UNIX_EPOCH)
+ .unwrap()
+ .as_secs() as u128
+ * 1000),
+ )?,
+ };
+
+ se_struct.end()
+ }
+}
+
+/// <https://docs.taler.net/core/api-common.html#tsref-type-SafeUint64>
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
+pub struct SafeUint64(u64);
+
+#[derive(Debug, thiserror::Error)]
+#[error("{0} unsafe, {0} > (2^53 - 1)")]
+pub struct UnsafeUint64(u64);
+
+impl TryFrom<u64> for SafeUint64 {
+ type Error = UnsafeUint64;
+
+ fn try_from(nb: u64) -> Result<Self, Self::Error> {
+ if nb < (1 << 53) - 1 {
+ Ok(SafeUint64(nb))
+ } else {
+ Err(UnsafeUint64(nb))
+ }
+ }
+}
+
+impl<'de> Deserialize<'de> for SafeUint64 {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ Ok(SafeUint64::try_from(u64::deserialize(deserializer)?).map_err(D::Error::custom)?)
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum ParseSafeUint64Error {
+ #[error(transparent)]
+ Unsafe(#[from] UnsafeUint64),
+ #[error(transparent)]
+ Format(#[from] ParseIntError),
+}
+
+impl FromStr for SafeUint64 {
+ type Err = ParseSafeUint64Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ Ok(SafeUint64::try_from(s.parse::<u64>()?)?)
+ }
+}
+
+/// <https://docs.taler.net/core/api-common.html#tsref-type-Amount>
+#[derive(
+ Debug, Clone, PartialEq, Eq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay,
+)]
+pub struct Amount {
+ currency: String,
+ value: u64,
+ fraction: u32,
+}
+
+impl Amount {
+ pub fn new(currency: impl Into<String>, value: u64, fraction: u32) -> Self {
+ Self {
+ currency: currency.into(),
+ value,
+ fraction,
+ }
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum ParseAmountError {
+ #[error("Invalid amount format")]
+ FormatAmount,
+ #[error("Amount overflow")]
+ AmountOverflow,
+ #[error(transparent)]
+ Format(#[from] ParseIntError),
+}
+
+impl FromStr for Amount {
+ type Err = ParseAmountError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let (currency, amount) = s
+ .trim()
+ .split_once(':')
+ .ok_or(ParseAmountError::FormatAmount)?;
+ if currency.len() > 12 {
+ return Err(ParseAmountError::FormatAmount);
+ }
+ let (value, fraction) = amount.split_once('.').unwrap_or((amount, ""));
+
+ let value: u64 = value.parse().map_err(|_| ParseAmountError::FormatAmount)?;
+ if value > 1 << 52 {
+ return Err(ParseAmountError::FormatAmount);
+ }
+
+ if fraction.len() > 8 {
+ return Err(ParseAmountError::FormatAmount);
+ }
+ let fraction: u32 = if fraction.is_empty() {
+ 0
+ } else {
+ fraction
+ .parse::<u32>()
+ .map_err(|_| ParseAmountError::FormatAmount)?
+ * 10_u32.pow((8 - fraction.len()) as u32)
+ };
+
+ Ok(Self {
+ currency: currency.to_string(),
+ value,
+ fraction,
+ })
+ }
+}
+
+impl Display for Amount {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_fmt(format_args!(
+ "{}:{}.{:08}",
+ self.currency, self.value, self.fraction
+ ))
+ }
+}
+
+#[test]
+fn test_amount() {
+ const TALER_AMOUNT_FRAC_BASE: u32 = 100000000;
+ // https://git.taler.net/exchange.git/tree/src/util/test_amount.c
+
+ const INVALID_AMOUNTS: [&str; 6] = [
+ "EUR:4a", // non-numeric,
+ "EUR:4.4a", // non-numeric
+ "EUR:4.a4", // non-numeric
+ ":4.a4", // no currency
+ "EUR:4.123456789", // precision to high
+ "EUR:1234567890123456789012345678901234567890123456789012345678901234567890", // value to big
+ ];
+
+ for str in INVALID_AMOUNTS {
+ let amount = Amount::from_str(str);
+ assert!(amount.is_err(), "invalid {} got {:?}", str, amount);
+ }
+
+ let valid_amounts: Vec<(&str, Amount)> = vec![
+ ("EUR:4", Amount::new("EUR", 4, 0)), // without fraction
+ (
+ "eur:0.02",
+ Amount::new("eur", 0, TALER_AMOUNT_FRAC_BASE / 100 * 2),
+ ), // leading zero fraction
+ (
+ " eur:4.12",
+ Amount::new("eur", 4, TALER_AMOUNT_FRAC_BASE / 100 * 12),
+ ), // leading space and fraction
+ (
+ " *LOCAL:4444.1000",
+ Amount::new("*LOCAL", 4444, TALER_AMOUNT_FRAC_BASE / 10),
+ ), // local currency
+ ];
+ for (str, goal) in valid_amounts {
+ let amount = Amount::from_str(str);
+ assert!(amount.is_ok(), "Valid {} got {:?}", str, amount);
+ assert_eq!(
+ *amount.as_ref().unwrap(),
+ goal,
+ "Expected {:?} got {:?} for {}",
+ goal,
+ amount,
+ str
+ );
+ let amount = amount.unwrap();
+ let str = amount.to_string();
+ assert_eq!(amount, Amount::from_str(&str).unwrap(), "{:?}", str);
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum ParseBase32Error {
+ #[error("Invalid Crockford’s base32 format")]
+ Format,
+ #[error("Invalid length: expected {0} got {1}")]
+ Length(usize, usize),
+}
+
+#[derive(Debug, Clone)]
+pub struct Base32<const L: usize>([u8; L]);
+
+impl<const L: usize> FromStr for Base32<L> {
+ type Err = ParseBase32Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let bytes =
+ base32::decode(base32::Alphabet::Crockford, s).ok_or(ParseBase32Error::Format)?;
+ let exact: [u8; L] = bytes
+ .try_into()
+ .map_err(|vec: Vec<u8>| ParseBase32Error::Length(L, vec.len()))?;
+ Ok(Self(exact))
+ }
+}
+
+impl<const L: usize> Display for Base32<L> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_str(&base32::encode(base32::Alphabet::Crockford, &self.0))
+ }
+}
+
+impl<const L: usize> serde::Serialize for Base32<L> {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ serializer.serialize_str(&self.to_string())
+ }
+}
+
+impl<'de, const L: usize> Deserialize<'de> for Base32<L> {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ Ok(Base32::from_str(&String::deserialize(deserializer)?).map_err(D::Error::custom)?)
+ }
+}
+
+/// EdDSA and ECDHE public keys always point on Curve25519
+/// and represented using the standard 256 bits Ed25519 compact format,
+/// converted to Crockford Base32.
+pub type EddsaPublicKey = Base32<32>;
+/// 64-byte hash code
+pub type HashCode = Base32<64>;
+/// 32-bytes hash code
+pub type ShortHashCode = Base32<32>;
diff --git a/wire-gateway/src/api_wire.rs b/wire-gateway/src/api_wire.rs
@@ -0,0 +1,86 @@
+use crate::api_common::{Amount, EddsaPublicKey, HashCode, SafeUint64, ShortHashCode, Timestamp};
+
+/// <https://docs.taler.net/core/api-wire.html#tsref-type-TransferResponse>
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+pub struct TransferResponse {
+ pub timestamp: Timestamp,
+ pub row_id: SafeUint64,
+}
+
+/// <https://docs.taler.net/core/api-wire.html#tsref-type-TransferRequest>
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+pub struct TransferRequest {
+ pub request_uid: HashCode,
+ pub amount: Amount,
+ pub exchange_base_url: String,
+ pub wtid: ShortHashCode,
+ pub credit_account: String,
+}
+
+/// <https://docs.taler.net/core/api-wire.html#tsref-type-OutgoingHistory>
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+pub struct OutgoingHistory {
+ pub outgoing_transactions: Vec<OutgoingBankTransaction>,
+}
+
+/// <https://docs.taler.net/core/api-wire.html#tsref-type-OutgoingBankTransaction>
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+pub struct OutgoingBankTransaction {
+ pub row_id: SafeUint64,
+ pub date: Timestamp,
+ pub amount: Amount,
+ pub credit_account: String,
+ pub debit_account: String,
+ pub wtid: ShortHashCode,
+ pub exchange_base_url: String,
+}
+
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+pub struct IncomingHistory {
+ pub incoming_transactions: Vec<IncomingBankTransaction>,
+}
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+#[serde(tag = "type")]
+pub enum IncomingBankTransaction {
+ #[serde(rename = "RESERVE")]
+ IncomingReserveTransaction {
+ row_id: SafeUint64,
+ date: Timestamp,
+ amount: Amount,
+ credit_account: String,
+ debit_account: String,
+ reserve_pub: EddsaPublicKey,
+ },
+ #[serde(rename = "WAD")]
+ IncomingWadTransaction {
+ row_id: SafeUint64,
+ date: Timestamp,
+ amount: Amount,
+ credit_account: String,
+ debit_account: String,
+ origin_exchange_url: String,
+ // TODO wad_id: WadId,
+ },
+}
+
+/// <https://docs.taler.net/core/api-wire.html#tsref-type-AddIncomingRequest>
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+pub struct AddIncomingRequest {
+ pub amount: Amount,
+ pub reserve_pub: EddsaPublicKey,
+ pub debit_account: String,
+}
+
+/// <https://docs.taler.net/core/api-wire.html#tsref-type-AddIncomingResponse>
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+pub struct AddIncomingResponse {
+ pub row_id: SafeUint64,
+ pub timestamp: Timestamp,
+}
+
+#[derive(Debug, Clone, serde::Deserialize)]
+pub struct HistoryParams {
+ pub start: Option<u64>,
+ pub delta: i64,
+ pub long_pool_ms: Option<u64>,
+}
diff --git a/wire-gateway/src/error_codes.rs b/wire-gateway/src/error_codes.rs
@@ -23,6 +23,8 @@
/// Error codes used by GNU Taler
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[allow(non_camel_case_types)]
+#[allow(dead_code)]
pub enum ErrorCode {
/**
Special code to indicate success (no error).
diff --git a/wire-gateway/src/main.rs b/wire-gateway/src/main.rs
@@ -1,12 +1,27 @@
-use common_types::{Amount, HashCode, SafeUint64, ShortHashCode, TimeStamp};
+use api_common::{Amount, SafeUint64, ShortHashCode, Timestamp};
+use api_wire::{OutgoingBankTransaction, OutgoingHistory};
+use async_compression::tokio::bufread::ZlibDecoder;
use hyper::{
+ header,
+ http::request::Parts,
service::{make_service_fn, service_fn},
Body, Error, Method, Request, Response, Server, StatusCode,
};
+use tokio::{io::AsyncReadExt, sync::Mutex};
+
+use crate::api_wire::{
+ AddIncomingRequest, AddIncomingResponse, HistoryParams, IncomingBankTransaction,
+ IncomingHistory, TransferRequest, TransferResponse,
+};
+
+mod error_codes;
#[tokio::main]
async fn main() {
- let state = ServerState {};
+ let state = ServerState {
+ incoming: Mutex::new(Vec::new()),
+ outgoing: Mutex::new(Vec::new()),
+ };
let state: &'static ServerState = Box::leak(Box::new(state));
let addr = ([0, 0, 0, 0], 8080).into();
@@ -26,323 +41,153 @@ async fn main() {
}
}
-struct ServerState;
-
-/// https://docs.taler.net/core/api-common.html#tsref-type-ErrorDetail
-#[derive(Debug, Clone, serde::Serialize)]
-struct ErrorDetail {
- code: i64,
- hint: Option<String>,
- detail: Option<String>,
- parameter: Option<String>,
- path: Option<String>,
- offset: Option<String>,
- index: Option<String>,
- object: Option<String>,
- currency: Option<String>,
- type_expected: Option<String>,
- type_actual: Option<String>,
+struct IncomingTransaction {
+ row_id: SafeUint64,
+ date: Timestamp,
+ amount: Amount,
+ reserve_pub: ShortHashCode,
+ debit_account: String,
}
-pub mod common_types {
- use std::{
- num::ParseIntError,
- str::FromStr,
- time::{Duration, SystemTime},
- };
-
- /// https://docs.taler.net/core/api-common.html#tsref-type-Timestamp
- #[derive(Debug, Clone, Copy)]
- pub enum TimeStamp {
- Never,
- Time(SystemTime),
- }
-
- #[derive(Debug, thiserror::Error)]
- pub enum ParseTimeStampError {
- #[error("Unknown timestamp {0}")]
- Unknown(String),
- #[error(transparent)]
- Format(#[from] ParseIntError),
- }
-
- impl FromStr for TimeStamp {
- type Err = ParseTimeStampError;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- if s.is_ascii() {
- if s == "never" {
- Ok(Self::Never)
- } else {
- Err(ParseTimeStampError::Unknown(s.to_string()))
- }
- } else {
- let since_epoch_ms: u64 = s.parse()?;
- Ok(Self::Time(
- SystemTime::UNIX_EPOCH + Duration::from_millis(since_epoch_ms),
- ))
- }
- }
- }
-
- /// https://docs.taler.net/core/api-common.html#tsref-type-SafeUint64
- #[derive(Debug, Clone)]
- pub struct SafeUint64(u64);
-
- #[derive(Debug, thiserror::Error)]
- pub enum ParseSafeUint64Error {
- #[error("{0} unsafe, {0} > (2^53 - 1)")]
- Unsafe(u64),
- #[error(transparent)]
- Format(#[from] ParseIntError),
- }
-
- impl FromStr for SafeUint64 {
- type Err = ParseSafeUint64Error;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- let nb: u64 = s.parse()?;
- if nb < (2 << 52) {
- Ok(SafeUint64(nb))
- } else {
- Err(ParseSafeUint64Error::Unsafe(nb))
- }
- }
- }
-
- /// TODO https://docs.taler.net/core/api-common.html#tsref-type-Amount
- #[derive(Debug, Clone, PartialEq, Eq)]
- pub struct Amount {
- currency: String,
- value: u64,
- fraction: u32,
- }
-
- impl Amount {
- pub fn new(currency: impl Into<String>, value: u64, fraction: u32) -> Self {
- Self {
- currency: currency.into(),
- value,
- fraction,
- }
- }
- }
-
- #[derive(Debug, thiserror::Error)]
- pub enum ParseAmountError {
- #[error("Invalid amount format")]
- FormatAmount,
- #[error("Amount overflow")]
- AmountOverflow,
- #[error(transparent)]
- Format(#[from] ParseIntError),
- }
-
- impl FromStr for Amount {
- type Err = ParseAmountError;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- let (currency, amount) = s
- .trim()
- .split_once(':')
- .ok_or(ParseAmountError::FormatAmount)?;
- if currency.len() > 12 {
- return Err(ParseAmountError::FormatAmount);
- }
- let (value, fraction) = amount.split_once('.').unwrap_or((amount, ""));
-
- let value: u64 = value.parse().map_err(|_| ParseAmountError::FormatAmount)?;
- if value > 1 << 52 {
- return Err(ParseAmountError::FormatAmount);
- }
-
- if fraction.len() > 8 {
- return Err(ParseAmountError::FormatAmount);
- }
- let fraction: u32 = if fraction.is_empty() {
- 0
- } else {
- fraction
- .parse::<u32>()
- .map_err(|_| ParseAmountError::FormatAmount)?
- * 10_u32.pow((8 - fraction.len()) as u32)
- };
-
- Ok(Self {
- currency: currency.to_string(),
- value,
- fraction,
- })
- }
- }
-
- #[test]
- fn test_amount() {
- const TALER_AMOUNT_FRAC_BASE: u32 = 100000000;
- // https://git.taler.net/exchange.git/tree/src/util/test_amount.c
-
- const INVALID_AMOUNTS: [&str; 6] = [
- "EUR:4a", // non-numeric,
- "EUR:4.4a", // non-numeric
- "EUR:4.a4", // non-numeric
- ":4.a4", // no currency
- "EUR:4.123456789", // precision to high
- "EUR:1234567890123456789012345678901234567890123456789012345678901234567890", // value to big
- ];
-
- for str in INVALID_AMOUNTS {
- let amount = Amount::from_str(str);
- assert!(amount.is_err(), "invalid {} got {:?}", str, amount);
- }
-
- let valid_amounts: Vec<(&str, Amount)> = vec![
- ("EUR:4", Amount::new("EUR", 4, 0)), // without fraction
- (
- "eur:0.02",
- Amount::new("eur", 0, TALER_AMOUNT_FRAC_BASE / 100 * 2),
- ), // leading zero fraction
- (
- " eur:4.12",
- Amount::new("eur", 4, TALER_AMOUNT_FRAC_BASE / 100 * 12),
- ), // leading space and fraction
- (
- " *LOCAL:4444.1000",
- Amount::new("*LOCAL", 4444, TALER_AMOUNT_FRAC_BASE / 10),
- ), // local currency
- ];
- for (str, goal) in valid_amounts {
- let amount = Amount::from_str(str);
- assert!(amount.is_ok(), "Valid {} got {:?}", str, amount);
- assert_eq!(
- *amount.as_ref().unwrap(),
- goal,
- "Expected {:?} got {:?} for {}",
- goal,
- amount,
- str
- );
- }
- }
-
- #[derive(Debug, thiserror::Error)]
- pub enum ParseHashError {
- #[error("Invalid Crockford’s base32 format")]
- Base32,
- #[error("Invalid length: expected {0} got {1}")]
- Length(usize, usize),
- }
-
- /// 64-byte hash code
- #[derive(Debug, Clone)]
- pub struct HashCode([u8; 64]);
-
- impl FromStr for HashCode {
- type Err = ParseHashError;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- let bytes =
- base32::decode(base32::Alphabet::Crockford, s).ok_or(ParseHashError::Base32)?;
- let exact: [u8; 64] = bytes
- .try_into()
- .map_err(|vec: Vec<u8>| ParseHashError::Length(64, vec.len()))?;
- Ok(Self(exact))
- }
- }
+struct OutgoingTransaction {
+ row_id: SafeUint64,
+ date: Timestamp,
+ amount: Amount,
+ wtid: ShortHashCode,
+ credit_account: String,
+}
- /// 32-bytes hash code
- #[derive(Debug, Clone)]
- pub struct ShortHashCode([u8; 32]);
+struct ServerState {
+ incoming: Mutex<Vec<IncomingTransaction>>,
+ outgoing: Mutex<Vec<OutgoingTransaction>>,
+}
- impl FromStr for ShortHashCode {
- type Err = ParseHashError;
+pub mod api_common;
+pub mod api_wire;
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- let bytes =
- base32::decode(base32::Alphabet::Crockford, s).ok_or(ParseHashError::Base32)?;
- let exact: [u8; 32] = bytes
- .try_into()
- .map_err(|vec: Vec<u8>| ParseHashError::Length(32, vec.len()))?;
- Ok(Self(exact))
- }
- }
+struct ServerError {
+ status: StatusCode,
+ detail: error_codes::ErrorCode,
}
-/// https://docs.taler.net/core/api-wire.html#tsref-type-TransferResponse
-#[derive(Debug, Clone, serde::Deserialize)]
-struct TransferResponse {
- #[serde(with = "serde_with::rust::display_fromstr")]
- timestamp: TimeStamp,
- #[serde(with = "serde_with::rust::display_fromstr")]
- row_id: SafeUint64,
+async fn parse_json<J: serde::de::DeserializeOwned>(parts: &Parts, body: Body) -> J {
+ let bytes = hyper::body::to_bytes(body).await.unwrap();
+ let mut buf = Vec::new();
+ let decompressed = if parts
+ .headers
+ .get(header::CONTENT_ENCODING)
+ .map(|it| it == "deflate")
+ .unwrap_or(false)
+ {
+ let mut decoder = ZlibDecoder::new(bytes.as_ref());
+ decoder.read_to_end(&mut buf).await.unwrap();
+ &buf
+ } else {
+ bytes.as_ref()
+ };
+
+ serde_json::from_slice(&decompressed).unwrap()
}
-/// https://docs.taler.net/core/api-wire.html#tsref-type-TransferRequest
-#[derive(Debug, Clone, serde::Deserialize)]
-struct TransferRequest {
- #[serde(with = "serde_with::rust::display_fromstr")]
- request_uid: HashCode,
- #[serde(with = "serde_with::rust::display_fromstr")]
- amount: Amount,
- exchange_base_url: String,
- #[serde(with = "serde_with::rust::display_fromstr")]
- wtid: ShortHashCode,
- credit_account: String,
+async fn json_response<J: serde::Serialize>(status: StatusCode, json: &J) -> Response<Body> {
+ let json = serde_json::to_vec(json).unwrap();
+ /* let mut encoder = ZlibEncoder::new(Vec::new());
+ encoder.write_all(&json).await.unwrap();
+ encoder.shutdown().await.unwrap();
+ let encoded = encoder.into_inner();*/
+ Response::builder()
+ .status(status)
+ .header(header::CONTENT_TYPE, "application/json")
+ //.header(header::CONTENT_ENCODING, "deflate")
+ .body(Body::from(json))
+ .unwrap()
}
async fn router(
req: Request<Body>,
state: &'static ServerState,
) -> Result<Response<Body>, Response<Body>> {
- dbg!(&req);
- let response = match req.uri().path() {
- "/transfer" if req.method() == Method::POST => Response::default(),
- "/history/incoming" if req.method() == Method::GET => Response::default(),
- "/history/outgoing" if req.method() == Method::GET => Response::default(),
+ let (parts, body) = req.into_parts();
+ let response = match parts.uri.path() {
+ "/transfer" if parts.method == Method::POST => {
+ let request: TransferRequest = parse_json(&parts, body).await;
+ let mut guard = state.outgoing.lock().await;
+ let row_id = SafeUint64::try_from(guard.len() as u64 + 1).unwrap();
+ let timestamp = Timestamp::now();
+ guard.push(OutgoingTransaction {
+ row_id,
+ date: timestamp,
+ amount: request.amount,
+ wtid: request.wtid,
+ credit_account: request.credit_account,
+ });
+ json_response(StatusCode::OK, &TransferResponse { timestamp, row_id }).await
+ }
+ "/history/incoming" if parts.method == Method::GET => {
+ let params: HistoryParams =
+ serde_urlencoded::from_str(parts.uri.query().unwrap_or("")).unwrap();
+ let guard = state.incoming.lock().await;
+ let transactions: Vec<IncomingBankTransaction> = guard
+ .iter()
+ .map(|tx| IncomingBankTransaction::IncomingReserveTransaction {
+ row_id: tx.row_id,
+ date: tx.date,
+ amount: tx.amount.clone(),
+ credit_account: String::new(),
+ debit_account: tx.debit_account.clone(),
+ reserve_pub: tx.reserve_pub.clone(),
+ })
+ .collect();
+ json_response(
+ StatusCode::OK,
+ &IncomingHistory {
+ incoming_transactions: transactions,
+ },
+ )
+ .await
+ }
+ "/history/outgoing" if parts.method == Method::GET => {
+ let params: HistoryParams =
+ serde_urlencoded::from_str(parts.uri.query().unwrap_or("")).unwrap();
+ let guard = state.outgoing.lock().await;
+ let transactions: Vec<OutgoingBankTransaction> = guard
+ .iter()
+ .map(|tx| OutgoingBankTransaction {
+ row_id: tx.row_id,
+ date: tx.date,
+ amount: tx.amount.clone(),
+ credit_account: tx.credit_account.clone(),
+ wtid: tx.wtid.clone(),
+ debit_account: String::new(),
+ exchange_base_url: String::new(),
+ })
+ .collect();
+ json_response(
+ StatusCode::OK,
+ &OutgoingHistory {
+ outgoing_transactions: transactions,
+ },
+ )
+ .await
+ }
+ "/admin/add-incoming" if parts.method == Method::POST => {
+ let request: AddIncomingRequest = parse_json(&parts, body).await;
+ let mut guard = state.incoming.lock().await;
+ let row_id = SafeUint64::try_from(guard.len() as u64 + 1).unwrap();
+ let timestamp = Timestamp::now();
+ guard.push(IncomingTransaction {
+ row_id,
+ date: timestamp,
+ amount: request.amount,
+ reserve_pub: request.reserve_pub,
+ debit_account: request.debit_account,
+ });
+ json_response(StatusCode::OK, &AddIncomingResponse { timestamp, row_id }).await
+ }
_ => Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::empty())
.unwrap(),
};
return Ok(response);
- /*
- let mut parts = req.uri().path().splitn(4, '/').skip(1);
- let prefix = parts.next().unwrap();
- let first = parts.next().unwrap_or("");
-
- if prefix != "api" {
- return Err(StatusCode::NOT_FOUND.into());
- }
-
- match parts.next() {
- None => match first {
- "pros" => match *req.method() {
- Method::GET => get_pros(Ctx::new(req, state)).await,
- Method::PUT => put_pros(Ctx::new(req, state)).await,
- _ => Err(StatusCode::NOT_FOUND.into()),
- },
- "recommendations" => match *req.method() {
- Method::POST => post_recommendation(Ctx::new(req, state)).await,
- Method::GET => get_recommendation(Ctx::new(req, state)).await,
- _ => Err(StatusCode::NOT_FOUND.into()),
- },
- "login" if req.method() == Method::POST => admin::login(Ctx::new(req, state)).await,
- "logout" if req.method() == Method::POST => {
- admin::logout(&Ctx::new(req, state), false).await
- }
- "logout_all" if req.method() == Method::POST => {
- admin::logout(&Ctx::new(req, state), true).await
- }
- "admin" if req.method() == Method::GET => admin::admin(&Ctx::new(req, state)).await,
- "mail" if req.method() == Method::POST => mail::send_mail(Ctx::new(req, state)).await,
- "mail_template" if req.method() == Method::POST => {
- mail::send_mail_template(Ctx::new(req, state)).await
- }
- "rate" if req.method() == Method::GET => limit(&req, state).await,
- _ => Err(StatusCode::NOT_FOUND.into()),
- },
- Some(second) => match first {
- "img" if req.method() == Method::GET => img(state, second).await,
- _ => Err(StatusCode::NOT_FOUND.into()),
- },
- }*/
}