[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-rust] branch master updated (9fd9f82 -> 1f583d2)
From: |
gnunet |
Subject: |
[taler-rust] branch master updated (9fd9f82 -> 1f583d2) |
Date: |
Tue, 14 Jan 2025 13:35:02 +0100 |
This is an automated email from the git hooks/post-receive script.
antoine pushed a change to branch master
in repository taler-rust.
from 9fd9f82 common: optimize subject parsing
new 96b8c24 magnet-bank: add schema, db logic, dbinit and reduce
dependencies
new 1f583d2 magnet-bank: add api and serve cmd
The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
Cargo.lock | 441 ++--------
Cargo.toml | 4 +
common/taler-api/Cargo.toml | 9 +-
common/taler-api/src/lib.rs | 102 ++-
common/taler-api/tests/api.rs | 70 +-
common/taler-api/tests/common/mod.rs | 4 +
common/taler-common/Cargo.toml | 2 +-
common/taler-common/src/config.rs | 42 +-
common/test-utils/Cargo.toml | 10 +-
common/test-utils/src/lib.rs | 62 +-
magnet-bank.conf | 23 +
wire-gateway/magnet-bank/Cargo.toml | 17 +-
wire-gateway/magnet-bank/db/schema.sql | 309 +++++++
wire-gateway/magnet-bank/src/config.rs | 68 ++
.../magnet-bank/src/constant.rs | 7 +-
wire-gateway/magnet-bank/src/db.rs | 976 +++++++++++++++++++++
.../magnet-bank}/src/lib.rs | 12 +-
wire-gateway/magnet-bank/src/magnet/error.rs | 40 +-
wire-gateway/magnet-bank/src/magnet/oauth.rs | 2 +-
wire-gateway/magnet-bank/src/main.rs | 60 +-
wire-gateway/magnet-bank/src/wire_gateway.rs | 142 +++
21 files changed, 1936 insertions(+), 466 deletions(-)
create mode 100644 magnet-bank.conf
create mode 100644 wire-gateway/magnet-bank/db/schema.sql
copy common/test-utils/src/lib.rs => wire-gateway/magnet-bank/src/constant.rs
(86%)
create mode 100644 wire-gateway/magnet-bank/src/db.rs
copy {common/test-utils => wire-gateway/magnet-bank}/src/lib.rs (83%)
create mode 100644 wire-gateway/magnet-bank/src/wire_gateway.rs
diff --git a/Cargo.lock b/Cargo.lock
index f956390..44356ba 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -127,12 +127,6 @@ dependencies = [
"num-traits",
]
-[[package]]
-name = "atomic-waker"
-version = "1.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
-
[[package]]
name = "auto-future"
version = "1.0.0"
@@ -264,12 +258,9 @@ checksum =
"8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bitflags"
-version = "2.6.0"
+version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
-dependencies = [
- "serde",
-]
+checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be"
[[package]]
name = "block-buffer"
@@ -312,9 +303,9 @@ checksum =
"37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
-version = "1.2.7"
+version = "1.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7"
+checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b"
dependencies = [
"shlex",
]
@@ -367,9 +358,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.5.24"
+version = "4.5.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9560b07a799281c7e0958b9296854d6fafd4c5f31444a7e5bb1ad6dde5ccf1bd"
+checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783"
dependencies = [
"clap_builder",
"clap_derive",
@@ -377,9 +368,9 @@ dependencies = [
[[package]]
name = "clap_builder"
-version = "4.5.24"
+version = "4.5.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "874e0dd3eb68bf99058751ac9712f622e61e6f393a94f7128fa26e3f02f5c7cd"
+checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121"
dependencies = [
"anstream",
"anstyle",
@@ -769,15 +760,6 @@ dependencies = [
"zeroize",
]
-[[package]]
-name = "encoding_rs"
-version = "0.8.35"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
-dependencies = [
- "cfg-if",
-]
-
[[package]]
name = "equivalent"
version = "1.0.1"
@@ -838,17 +820,6 @@ version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
-[[package]]
-name = "flume"
-version = "0.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
-dependencies = [
- "futures-core",
- "futures-sink",
- "spin",
-]
-
[[package]]
name = "fnv"
version = "1.0.7"
@@ -901,17 +872,6 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
-[[package]]
-name = "futures-executor"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
-dependencies = [
- "futures-core",
- "futures-task",
- "futures-util",
-]
-
[[package]]
name = "futures-intrusive"
version = "0.5.0"
@@ -1002,25 +962,6 @@ dependencies = [
"subtle",
]
-[[package]]
-name = "h2"
-version = "0.4.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e"
-dependencies = [
- "atomic-waker",
- "bytes",
- "fnv",
- "futures-core",
- "futures-sink",
- "http 1.2.0",
- "indexmap 2.7.0",
- "slab",
- "tokio",
- "tokio-util",
- "tracing",
-]
-
[[package]]
name = "half"
version = "2.4.1"
@@ -1174,7 +1115,6 @@ dependencies = [
"bytes",
"futures-channel",
"futures-util",
- "h2",
"http 1.2.0",
"http-body",
"httparse",
@@ -1186,23 +1126,6 @@ dependencies = [
"want",
]
-[[package]]
-name = "hyper-rustls"
-version = "0.27.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2"
-dependencies = [
- "futures-util",
- "http 1.2.0",
- "hyper",
- "hyper-util",
- "rustls",
- "rustls-pki-types",
- "tokio",
- "tokio-rustls",
- "tower-service",
-]
-
[[package]]
name = "hyper-tls"
version = "0.6.0"
@@ -1468,9 +1391,9 @@ checksum =
"d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "jiff"
-version = "0.1.21"
+version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed0ce60560149333a8e41ca7dc78799c47c5fd435e2bc18faf6a054382eec037"
+checksum = "5c258647f65892e500c2478ef2c71ba008e7dc1774a8289345adbbb502a4def1"
dependencies = [
"log",
"portable-atomic",
@@ -1480,9 +1403,9 @@ dependencies = [
[[package]]
name = "js-sys"
-version = "0.3.76"
+version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -1493,9 +1416,6 @@ name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
-dependencies = [
- "spin",
-]
[[package]]
name = "libc"
@@ -1521,22 +1441,6 @@ dependencies = [
"libdeflate-sys",
]
-[[package]]
-name = "libm"
-version = "0.2.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
-
-[[package]]
-name = "libsqlite3-sys"
-version = "0.30.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
-dependencies = [
- "pkg-config",
- "vcpkg",
-]
-
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
@@ -1596,8 +1500,11 @@ dependencies = [
"serde_urlencoded",
"sha1",
"spki",
+ "sqlx",
+ "taler-api",
"taler-common",
- "thiserror 2.0.10",
+ "test-utils",
+ "thiserror 2.0.11",
"tokio",
"tracing",
"tracing-subscriber",
@@ -1697,49 +1604,12 @@ dependencies = [
"winapi",
]
-[[package]]
-name = "num-bigint-dig"
-version = "0.8.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
-dependencies = [
- "byteorder",
- "lazy_static",
- "libm",
- "num-integer",
- "num-iter",
- "num-traits",
- "rand",
- "smallvec",
- "zeroize",
-]
-
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
-[[package]]
-name = "num-integer"
-version = "0.1.46"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
-dependencies = [
- "num-traits",
-]
-
-[[package]]
-name = "num-iter"
-version = "0.1.45"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
-dependencies = [
- "autocfg",
- "num-integer",
- "num-traits",
-]
-
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -1747,7 +1617,6 @@ source =
"registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
- "libm",
]
[[package]]
@@ -1898,17 +1767,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
-[[package]]
-name = "pkcs1"
-version = "0.7.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
-dependencies = [
- "der",
- "pkcs8",
- "spki",
-]
-
[[package]]
name = "pkcs8"
version = "0.10.2"
@@ -2004,9 +1862,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
-version = "1.0.92"
+version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
+checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
dependencies = [
"unicode-ident",
]
@@ -2131,15 +1989,12 @@ checksum =
"43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da"
dependencies = [
"base64",
"bytes",
- "encoding_rs",
"futures-core",
"futures-util",
- "h2",
"http 1.2.0",
"http-body",
"http-body-util",
"hyper",
- "hyper-rustls",
"hyper-tls",
"hyper-util",
"ipnet",
@@ -2155,7 +2010,6 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"sync_wrapper",
- "system-configuration",
"tokio",
"tokio-native-tls",
"tower",
@@ -2187,41 +2041,6 @@ dependencies = [
"subtle",
]
-[[package]]
-name = "ring"
-version = "0.17.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
-dependencies = [
- "cc",
- "cfg-if",
- "getrandom",
- "libc",
- "spin",
- "untrusted",
- "windows-sys 0.52.0",
-]
-
-[[package]]
-name = "rsa"
-version = "0.9.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519"
-dependencies = [
- "const-oid",
- "digest",
- "num-bigint-dig",
- "num-integer",
- "num-traits",
- "pkcs1",
- "pkcs8",
- "rand_core",
- "signature",
- "spki",
- "subtle",
- "zeroize",
-]
-
[[package]]
name = "rust-multipart-rfc7578_2"
version = "0.6.1"
@@ -2266,20 +2085,6 @@ dependencies = [
"windows-sys 0.59.0",
]
-[[package]]
-name = "rustls"
-version = "0.23.20"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b"
-dependencies = [
- "once_cell",
- "ring",
- "rustls-pki-types",
- "rustls-webpki",
- "subtle",
- "zeroize",
-]
-
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
@@ -2295,17 +2100,6 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37"
-[[package]]
-name = "rustls-webpki"
-version = "0.102.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
-dependencies = [
- "ring",
- "rustls-pki-types",
- "untrusted",
-]
-
[[package]]
name = "rustversion"
version = "1.0.19"
@@ -2553,15 +2347,6 @@ dependencies = [
"windows-sys 0.52.0",
]
-[[package]]
-name = "spin"
-version = "0.9.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
-dependencies = [
- "lock_api",
-]
-
[[package]]
name = "spki"
version = "0.7.3"
@@ -2580,9 +2365,7 @@ checksum =
"4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f"
dependencies = [
"sqlx-core",
"sqlx-macros",
- "sqlx-mysql",
"sqlx-postgres",
- "sqlx-sqlite",
]
[[package]]
@@ -2605,20 +2388,18 @@ dependencies = [
"indexmap 2.7.0",
"log",
"memchr",
+ "native-tls",
"once_cell",
"percent-encoding",
- "rustls",
- "rustls-pemfile",
"serde",
"serde_json",
"sha2",
"smallvec",
- "thiserror 2.0.10",
+ "thiserror 2.0.11",
"tokio",
"tokio-stream",
"tracing",
"url",
- "webpki-roots",
]
[[package]]
@@ -2658,47 +2439,6 @@ dependencies = [
"url",
]
-[[package]]
-name = "sqlx-mysql"
-version = "0.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233"
-dependencies = [
- "atoi",
- "base64",
- "bitflags",
- "byteorder",
- "bytes",
- "crc",
- "digest",
- "dotenvy",
- "either",
- "futures-channel",
- "futures-core",
- "futures-io",
- "futures-util",
- "generic-array",
- "hex",
- "hkdf",
- "hmac",
- "itoa",
- "log",
- "md-5",
- "memchr",
- "once_cell",
- "percent-encoding",
- "rand",
- "rsa",
- "sha1",
- "sha2",
- "smallvec",
- "sqlx-core",
- "stringprep",
- "thiserror 2.0.10",
- "tracing",
- "whoami",
-]
-
[[package]]
name = "sqlx-postgres"
version = "0.8.3"
@@ -2731,33 +2471,11 @@ dependencies = [
"smallvec",
"sqlx-core",
"stringprep",
- "thiserror 2.0.10",
+ "thiserror 2.0.11",
"tracing",
"whoami",
]
-[[package]]
-name = "sqlx-sqlite"
-version = "0.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540"
-dependencies = [
- "atoi",
- "flume",
- "futures-channel",
- "futures-core",
- "futures-executor",
- "futures-intrusive",
- "futures-util",
- "libsqlite3-sys",
- "log",
- "percent-encoding",
- "serde_urlencoded",
- "sqlx-core",
- "tracing",
- "url",
-]
-
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
@@ -2789,9 +2507,9 @@ checksum =
"13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
-version = "2.0.95"
+version = "2.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a"
+checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
dependencies = [
"proc-macro2",
"quote",
@@ -2818,27 +2536,6 @@ dependencies = [
"syn",
]
-[[package]]
-name = "system-configuration"
-version = "0.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
-dependencies = [
- "bitflags",
- "core-foundation",
- "system-configuration-sys",
-]
-
-[[package]]
-name = "system-configuration-sys"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
-dependencies = [
- "core-foundation-sys",
- "libc",
-]
-
[[package]]
name = "taler-api"
version = "0.1.0"
@@ -2856,7 +2553,7 @@ dependencies = [
"sqlx",
"taler-common",
"test-utils",
- "thiserror 2.0.10",
+ "thiserror 2.0.11",
"tokio",
"tracing",
"tracing-subscriber",
@@ -2880,7 +2577,7 @@ dependencies = [
"serde_with",
"sqlx",
"tempfile",
- "thiserror 2.0.10",
+ "thiserror 2.0.11",
"tracing",
"url",
]
@@ -2907,8 +2604,12 @@ dependencies = [
"axum-test",
"serde",
"serde_json",
+ "sqlx",
"taler-common",
+ "tempfile",
"tokio",
+ "tracing",
+ "tracing-subscriber",
]
[[package]]
@@ -2922,11 +2623,11 @@ dependencies = [
[[package]]
name = "thiserror"
-version = "2.0.10"
+version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3ac7f54ca534db81081ef1c1e7f6ea8a3ef428d2fc069097c079443d24124d3"
+checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
dependencies = [
- "thiserror-impl 2.0.10",
+ "thiserror-impl 2.0.11",
]
[[package]]
@@ -2942,9 +2643,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
-version = "2.0.10"
+version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e9465d30713b56a37ede7185763c3492a91be2f5fa68d958c44e41ab9248beb"
+checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
dependencies = [
"proc-macro2",
"quote",
@@ -3065,16 +2766,6 @@ dependencies = [
"tokio",
]
-[[package]]
-name = "tokio-rustls"
-version = "0.26.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37"
-dependencies = [
- "rustls",
- "tokio",
-]
-
[[package]]
name = "tokio-stream"
version = "0.1.17"
@@ -3086,19 +2777,6 @@ dependencies = [
"tokio",
]
-[[package]]
-name = "tokio-util"
-version = "0.7.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
-dependencies = [
- "bytes",
- "futures-core",
- "futures-sink",
- "pin-project-lite",
- "tokio",
-]
-
[[package]]
name = "tower"
version = "0.5.2"
@@ -3255,12 +2933,6 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
-[[package]]
-name = "untrusted"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
-
[[package]]
name = "url"
version = "2.5.4"
@@ -3293,9 +2965,9 @@ checksum =
"06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
-version = "1.11.0"
+version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
+checksum = "b913a3b5fe84142e269d63cc62b64319ccaf89b748fc31fe025177f767a756c4"
[[package]]
name = "valuable"
@@ -3348,20 +3020,21 @@ checksum =
"b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasm-bindgen"
-version = "0.2.99"
+version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"once_cell",
+ "rustversion",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
-version = "0.2.99"
+version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
@@ -3373,9 +3046,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.49"
+version = "0.4.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2"
+checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
dependencies = [
"cfg-if",
"js-sys",
@@ -3386,9 +3059,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.99"
+version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -3396,9 +3069,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.99"
+version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
@@ -3409,29 +3082,23 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.99"
+version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
[[package]]
name = "web-sys"
-version = "0.3.76"
+version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc"
+checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
dependencies = [
"js-sys",
"wasm-bindgen",
]
-[[package]]
-name = "webpki-roots"
-version = "0.26.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e"
-dependencies = [
- "rustls-pki-types",
-]
-
[[package]]
name = "whoami"
version = "1.5.2"
diff --git a/Cargo.toml b/Cargo.toml
index a801397..25d0252 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -24,3 +24,7 @@ tracing = "0.1"
tracing-subscriber = "0.3"
clap = { version = "4.5", features = ["derive"] }
jiff = { version = "0.1", default-features = false, features = ["std"] }
+tempfile = "3.15"
+taler-common = { path = "common/taler-common" }
+taler-api = { path = "common/taler-api" }
+test-utils = { path = "common/test-utils" }
diff --git a/common/taler-api/Cargo.toml b/common/taler-api/Cargo.toml
index 559c62d..cd63520 100644
--- a/common/taler-api/Cargo.toml
+++ b/common/taler-api/Cargo.toml
@@ -9,9 +9,8 @@ tracing-test = "0.2"
dashmap = "6.1"
sqlx = { workspace = true, features = [
"postgres",
- "runtime-tokio-rustls",
- "tls-rustls-ring",
- "migrate",
+ "runtime-tokio-native-tls",
+ "tls-native-tls",
] }
http-body-util = "0.1.2"
libdeflater = "1.22.0"
@@ -24,10 +23,10 @@ serde_json.workspace = true
axum.workspace = true
url.workspace = true
thiserror.workspace = true
-taler-common = { path = "../taler-common" }
+taler-common.workspace = true
[dev-dependencies]
-test-utils = { path = "../test-utils" }
+test-utils.workspace = true
criterion.workspace = true
fastrand.workspace = true
diff --git a/common/taler-api/src/lib.rs b/common/taler-api/src/lib.rs
index 97e2ed1..c932093 100644
--- a/common/taler-api/src/lib.rs
+++ b/common/taler-api/src/lib.rs
@@ -1,6 +1,6 @@
/*
This file is part of TALER
- Copyright (C) 2024 Taler Systems SA
+ Copyright (C) 2024-2025 Taler Systems SA
TALER is free software; you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
Software
@@ -15,6 +15,10 @@
*/
use std::{
+ fs::Permissions,
+ io::ErrorKind,
+ net::SocketAddr,
+ os::unix::fs::PermissionsExt,
sync::{
atomic::{AtomicU32, Ordering},
Arc,
@@ -35,6 +39,7 @@ use axum::{
use constants::{MAX_BODY_LENGTH, MAX_PAGE_SIZE, MAX_TIMEOUT_MS,
WIRE_GATEWAY_API_VERSION};
use error::{failure, failure_code, ApiError, ApiResult};
use http_body_util::BodyExt;
+use listenfd::ListenFd;
use serde::de::DeserializeOwned;
use taler_common::{
api_params::{History, HistoryParams, Page, TransferParams},
@@ -46,7 +51,10 @@ use taler_common::{
error_code::ErrorCode,
types::amount::Amount,
};
-use tokio::{net::TcpListener, signal};
+use tokio::{
+ net::{TcpListener, UnixListener},
+ signal,
+};
use tracing::info;
pub mod auth;
@@ -173,6 +181,7 @@ where
}
pub trait WireGatewayImpl: Send + Sync {
+ fn name(&self) -> &str;
fn currency(&self) -> &str;
fn implementation(&self) -> Option<&str>;
fn transfer(
@@ -231,9 +240,66 @@ pub fn standard_layer(router: Router, auth: AuthMethod) ->
Router {
))
}
+pub enum Serve {
+ Tcp(SocketAddr),
+ Unix {
+ path: String,
+ permission: Permissions,
+ },
+}
+
+impl Serve {
+ /// Resolve listener from a config and environement
+ fn resolve(&self) -> Result<Listener, std::io::Error> {
+ // Check if systemd is passing a socket
+ let mut listenfd = ListenFd::from_env();
+ if let Ok(Some(unix)) = listenfd.take_unix_listener(0) {
+ info!(
+ "Server listening on activated unix socket {:?}",
+ unix.local_addr()
+ );
+ Ok(Listener::Unix(UnixListener::from_std(unix)?))
+ } else if let Ok(Some(tcp)) = listenfd.take_tcp_listener(0) {
+ info!(
+ "Server listening on activated TCP socket {:?}",
+ tcp.local_addr()
+ );
+ Ok(Listener::Tcp(TcpListener::from_std(tcp)?))
+ } else {
+ match self {
+ Serve::Tcp(socket_addr) => {
+ info!("Server listening on {socket_addr}");
+ let listener = std::net::TcpListener::bind(socket_addr)?;
+ Ok(Listener::Tcp(TcpListener::from_std(listener)?))
+ }
+ Serve::Unix { path, permission } => {
+ info!(
+ "Server listening on unxis domain socket {path} {:o}",
+ permission.mode()
+ );
+ if let Err(e) = std::fs::remove_file(path) {
+ let kind = e.kind();
+ if kind != ErrorKind::NotFound {
+ return Err(e);
+ }
+ }
+ let listener =
std::os::unix::net::UnixListener::bind(path)?;
+ std::fs::set_permissions(path, permission.clone())?;
+ Ok(Listener::Unix(UnixListener::from_std(listener)?))
+ }
+ }
+ }
+ }
+}
+
+enum Listener {
+ Tcp(TcpListener),
+ Unix(UnixListener),
+}
+
pub async fn server(
- tcp_listener: TcpListener,
mut router: Router,
+ serve: Serve,
auth: AuthMethod,
lifetime_counter: Option<AtomicU32>,
) -> Result<(), std::io::Error> {
@@ -248,13 +314,29 @@ pub async fn server(
))
}
let router = standard_layer(router, auth);
+ let listener = serve.resolve()?;
+
+ match listener {
+ Listener::Tcp(tcp_listener) => {
+ axum::serve(
+ tcp_listener,
+ router.layer(middleware::from_fn(logger_middleware)),
+ )
+ .with_graceful_shutdown(shutdown_signal(notify))
+ .await?;
+ }
+ Listener::Unix(unix_listener) => {
+ axum::serve(
+ unix_listener,
+ router.layer(middleware::from_fn(logger_middleware)),
+ )
+ .with_graceful_shutdown(shutdown_signal(notify))
+ .await?;
+ }
+ }
- axum::serve(
- tcp_listener,
- router.layer(middleware::from_fn(logger_middleware)),
- )
- .with_graceful_shutdown(shutdown_signal(notify))
- .await
+ info!("Server stopped");
+ Ok(())
}
struct LifetimeMiddlewareState {
@@ -334,7 +416,7 @@ pub fn wire_gateway_api<I: WireGatewayImpl + 'static>(wg:
Arc<I>) -> Router {
"/config",
get(|State(state): State<Arc<I>>| async move {
Json(WireConfig {
- name: "taler-wire-gateway",
+ name: state.name(),
version: WIRE_GATEWAY_API_VERSION,
currency: state.currency(),
implementation: state.implementation(),
diff --git a/common/taler-api/tests/api.rs b/common/taler-api/tests/api.rs
index f3a2a3e..6e1a5bc 100644
--- a/common/taler-api/tests/api.rs
+++ b/common/taler-api/tests/api.rs
@@ -28,6 +28,7 @@ use taler_common::{
};
use test_utils::{
axum_test::TestServer,
+ db_test_setup,
helpers::TestResponseHelper,
json,
routine::{routine_history, routine_pagination},
@@ -35,17 +36,21 @@ use test_utils::{
mod common;
-async fn setup(pool: PgPool) -> TestServer {
- TestServer::new(standard_layer(
- sample_wire_gateway_api(Some(pool), "EUR".to_string()).await,
+async fn setup() -> (TestServer, PgPool) {
+ let pool = db_test_setup().await;
+
+ let server = TestServer::new(standard_layer(
+ sample_wire_gateway_api(Some(pool.clone()), "EUR".to_string()).await,
AuthMethod::None,
))
- .unwrap()
+ .unwrap();
+
+ (server, pool)
}
-#[sqlx::test]
-async fn errors(pool: PgPool) {
- let server = setup(pool).await;
+#[tokio::test]
+async fn errors() {
+ let (server, _) = setup().await;
server
.get("/unknown")
.await
@@ -56,14 +61,15 @@ async fn errors(pool: PgPool) {
.assert_error(ErrorCode::GENERIC_METHOD_INVALID);
}
-#[sqlx::test]
-async fn config(pool: PgPool) {
- let server = setup(pool).await;
+#[tokio::test]
+async fn config() {
+ let (server, _) = setup().await;
server.get("/config").await.assert_status_ok();
}
-#[sqlx::test]
-async fn transfer(pool: PgPool) {
+#[tokio::test]
+async fn transfer() {
+ let (server, _) = setup().await;
let valid_request = json!({
"request_uid": HashCode::rand(),
"amount": "EUR:42",
@@ -71,7 +77,6 @@ async fn transfer(pool: PgPool) {
"wtid": ShortHashCode::rand(),
"credit_account": "payto://todo",
});
- let server = setup(pool).await;
// Check OK
let first = server
@@ -107,11 +112,11 @@ async fn transfer(pool: PgPool) {
.assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH);
}
-#[sqlx::test]
-async fn transfer_by_id(pool: PgPool) {
- let wtid = ShortHashCode::rand();
+#[tokio::test]
+async fn transfer_by_id() {
+ let (server, _) = setup().await;
- let server = setup(pool).await;
+ let wtid = ShortHashCode::rand();
let resp = server
.post("/transfer")
.json(&json!({
@@ -142,9 +147,9 @@ async fn transfer_by_id(pool: PgPool) {
.assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND);
}
-#[sqlx::test]
-async fn transfer_page(pool: PgPool) {
- let server = setup(pool).await;
+#[tokio::test]
+async fn transfer_page() {
+ let (server, _) = setup().await;
server.get("/transfers").await.assert_no_content();
server
.get("/transfers?status=success")
@@ -206,9 +211,9 @@ async fn transfer_page(pool: PgPool) {
.await;
}
-#[sqlx::test]
-async fn outgoing_history(pool: PgPool) {
- let server = setup(pool).await;
+#[tokio::test]
+async fn outgoing_history() {
+ let (server, _) = setup().await;
server.get("/history/outgoing").await.assert_no_content();
routine_pagination::<OutgoingHistory, _>(
@@ -237,9 +242,9 @@ async fn outgoing_history(pool: PgPool) {
.await;
}
-#[sqlx::test]
-async fn incoming_history(pool: PgPool) {
- let server = setup(pool).await;
+#[tokio::test]
+async fn incoming_history() {
+ let (server, _) = setup().await;
server.get("/history/incoming").await.assert_no_content();
routine_history(
@@ -350,13 +355,14 @@ async fn add_incoming_routine(server: TestServer, kind:
IncomingType) {
}.assertBadRequest()*/
}
-#[sqlx::test]
-async fn add_incoming_reserve(pool: PgPool) {
- let server = setup(pool).await;
+#[tokio::test]
+async fn add_incoming_reserve() {
+ let (server, _) = setup().await;
add_incoming_routine(server, IncomingType::reserve).await;
}
-#[sqlx::test]
-async fn add_incoming_kyc(pool: PgPool) {
- let server = setup(pool).await;
+
+#[tokio::test]
+async fn add_incoming_kyc() {
+ let (server, _) = setup().await;
add_incoming_routine(server, IncomingType::kyc).await;
}
diff --git a/common/taler-api/tests/common/mod.rs
b/common/taler-api/tests/common/mod.rs
index 8269f36..83f642b 100644
--- a/common/taler-api/tests/common/mod.rs
+++ b/common/taler-api/tests/common/mod.rs
@@ -47,6 +47,10 @@ pub struct SampleState {
}
impl WireGatewayImpl for SampleState {
+ fn name(&self) -> &str {
+ "taler-wire-gateway"
+ }
+
fn currency(&self) -> &str {
&self.currency
}
diff --git a/common/taler-common/Cargo.toml b/common/taler-common/Cargo.toml
index cd7c2c0..93888b6 100644
--- a/common/taler-common/Cargo.toml
+++ b/common/taler-common/Cargo.toml
@@ -9,7 +9,7 @@ rand = "0.8"
serde_urlencoded = "0.7"
glob = "0.3"
indexmap = "2.7"
-tempfile = "3.15"
+tempfile.workspace = true
jiff.workspace = true
serde.workspace = true
serde_json = { workspace = true, features = ["raw_value"] }
diff --git a/common/taler-common/src/config.rs
b/common/taler-common/src/config.rs
index f6e30b8..571169d 100644
--- a/common/taler-common/src/config.rs
+++ b/common/taler-common/src/config.rs
@@ -14,7 +14,7 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-use std::{fmt::Debug, str::FromStr};
+use std::{fmt::Debug, fs::Permissions, os::unix::fs::PermissionsExt,
str::FromStr};
use indexmap::IndexMap;
use url::Url;
@@ -584,6 +584,38 @@ impl<'cfg, 'arg> Section<'cfg, 'arg> {
self.value(ty, option, |it| it.parse::<T>().map_err(|e| e.to_string()))
}
+ pub fn map<T: Copy>(
+ &self,
+ ty: &'arg str,
+ option: &'arg str,
+ map: &[(&str, T)],
+ ) -> Value<'arg, T> {
+ self.value(ty, option, |value| {
+ map.iter()
+ .find_map(|(k, v)| (*k == value).then_some(*v))
+ .ok_or_else(|| {
+ let mut buf = "expected '".to_owned();
+ match map {
+ [] => unreachable!("you must provide at least one
mapping"),
+ [(unique, _)] => buf.push_str(unique),
+ [(first, _), other @ .., (second, _)] => {
+ buf.push_str(first);
+ for (k, _) in other {
+ buf.push_str("', '");
+ buf.push_str(k);
+ }
+ buf.push_str("' or '");
+ buf.push_str(second);
+ }
+ }
+ buf.push_str("' got '");
+ buf.push_str(value);
+ buf.push('\'');
+ buf
+ })
+ })
+ }
+
/** Access [option] as str */
pub fn str(&self, option: &'arg str) -> Value<'arg, String> {
self.value("string", option, |it| Ok::<_, &str>(it.to_owned()))
@@ -594,6 +626,14 @@ impl<'cfg, 'arg> Section<'cfg, 'arg> {
self.value("path", option, |it| self.config.pathsub(it, 0))
}
+ pub fn unix_mode(&self, option: &'arg str) -> Value<'arg, Permissions> {
+ self.value("unix mode", option, |it| {
+ u32::from_str_radix(it, 8)
+ .map(Permissions::from_mode)
+ .map_err(|_| format!("'{it}' not a valid number"))
+ })
+ }
+
/** Access [option] as a number */
pub fn number<T: FromStr>(&self, option: &'arg str) -> Value<'arg, T> {
self.value("number", option, |it| {
diff --git a/common/test-utils/Cargo.toml b/common/test-utils/Cargo.toml
index d591647..7a780b4 100644
--- a/common/test-utils/Cargo.toml
+++ b/common/test-utils/Cargo.toml
@@ -9,4 +9,12 @@ axum.workspace = true
tokio.workspace = true
serde_json.workspace = true
serde.workspace = true
-taler-common = { path = "../taler-common" }
+taler-common.workspace = true
+tracing.workspace = true
+tracing-subscriber.workspace = true
+tempfile.workspace = true
+sqlx = { workspace = true, features = [
+ "postgres",
+ "runtime-tokio-native-tls",
+ "tls-native-tls",
+] }
diff --git a/common/test-utils/src/lib.rs b/common/test-utils/src/lib.rs
index d856785..a836cf1 100644
--- a/common/test-utils/src/lib.rs
+++ b/common/test-utils/src/lib.rs
@@ -1,6 +1,6 @@
/*
This file is part of TALER
- Copyright (C) 2024 Taler Systems SA
+ Copyright (C) 2024-2025 Taler Systems SA
TALER is free software; you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
Software
@@ -14,7 +14,67 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+use std::sync::atomic::{AtomicUsize, Ordering};
+
+use sqlx::{postgres::PgPoolOptions, PgPool};
+
pub use axum_test;
+use tracing::Level;
+use tracing_subscriber::{util::SubscriberInitExt, FmtSubscriber};
pub mod helpers;
pub mod json;
pub mod routine;
+
+pub async fn db_test_setup() -> PgPool {
+ setup_tracing();
+ db_pool().await
+}
+
+static MASTER_POOL: tokio::sync::OnceCell<PgPool> =
tokio::sync::OnceCell::const_new();
+
+const DB: &str = "postgres:/taler_rust_check";
+static NB_DB: AtomicUsize = AtomicUsize::new(0);
+
+async fn db_pool() -> PgPool {
+ let master = MASTER_POOL
+ .get_or_init(|| async {
+ PgPoolOptions::new()
+ .min_connections(0)
+ .max_connections(20)
+ .test_before_acquire(true)
+ .connect(DB)
+ .await
+ .expect("pg pool")
+ })
+ .await;
+ let idx = NB_DB.fetch_add(1, Ordering::Relaxed);
+ // Cleanup test db
+ let name = format!("taler_rust_test_{idx}");
+ let mut conn = master.acquire().await.unwrap();
+ sqlx::raw_sql(&format!("DROP DATABASE IF EXISTS {name}"))
+ .execute(&mut *conn)
+ .await
+ .unwrap();
+ sqlx::raw_sql(&format!("CREATE DATABASE {name}"))
+ .execute(&mut *conn)
+ .await
+ .unwrap();
+ drop(conn);
+
+ PgPoolOptions::new()
+ .min_connections(0)
+ .max_connections(5)
+ .test_before_acquire(true)
+ .connect(&format!("postgresql:/{name}"))
+ .await
+ .expect("pg pool")
+}
+
+fn setup_tracing() {
+ FmtSubscriber::builder()
+ .with_max_level(Level::TRACE)
+ .with_writer(std::io::stderr)
+ .finish()
+ .try_init()
+ .ok();
+}
diff --git a/magnet-bank.conf b/magnet-bank.conf
new file mode 100644
index 0000000..31a111a
--- /dev/null
+++ b/magnet-bank.conf
@@ -0,0 +1,23 @@
+[magnet-bank]
+API_URL = "https://mobil.magnetbank.hu"
+CONSUMER_KEY = "Consumer"
+CONSUMER_SECRET = "qikgjxc5y06tiil7qgrmh09l7rfi5a8e"
+KEYS_FILE = keys.json
+
+# How "magnet-bank serve" serves its API, this can either be tcp or unix
+SERVE = tcp
+
+# Port on which the HTTP server listens, e.g. 9967. Only used if SERVE is tcp.
+PORT = 8080
+
+# Which IP address should we bind to? E.g. ``127.0.0.1`` or ``::1``for
loopback. Only used if SERVE is tcp.
+BIND_TO = 0.0.0.0
+
+# Which unix domain path should we bind to? Only used if SERVE is unix.
+# UNIXPATH = libeufin-bank.sock
+
+# What should be the file access permissions for UNIXPATH? Only used if SERVE
is unix.
+# UNIXPATH_MODE = 660
+
+[magnet-bank-postgres]
+CONFIG = postgres:/magnet-bank
\ No newline at end of file
diff --git a/wire-gateway/magnet-bank/Cargo.toml
b/wire-gateway/magnet-bank/Cargo.toml
index 3e02d28..3ef5b8d 100644
--- a/wire-gateway/magnet-bank/Cargo.toml
+++ b/wire-gateway/magnet-bank/Cargo.toml
@@ -5,7 +5,10 @@ edition = "2021"
[dependencies]
rand_core = { version = "*" }
-reqwest = { version = "0.12", features = ["json"] }
+reqwest = { version = "0.12", default-features = false, features = [
+ "json",
+ "native-tls",
+] }
hmac = "0.12"
sha1 = "0.10"
p256 = { version = "0.13.2", features = ["alloc", "ecdsa"] }
@@ -16,12 +19,22 @@ percent-encoding = "2.3"
serde_urlencoded = "0.7.1"
anyhow = "1.0"
passterm = "2.0"
-taler-common = { path = "../../common/taler-common" }
+sqlx = { workspace = true, features = [
+ "postgres",
+ "runtime-tokio-native-tls",
+ "tls-native-tls",
+] }
serde_json = { workspace = true, features = ["raw_value"] }
jiff = { workspace = true, features = ["serde"] }
+taler-common.workspace = true
+taler-api.workspace = true
clap.workspace = true
serde.workspace = true
thiserror.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
tokio.workspace = true
+
+
+[dev-dependencies]
+test-utils.workspace = true
\ No newline at end of file
diff --git a/wire-gateway/magnet-bank/db/schema.sql
b/wire-gateway/magnet-bank/db/schema.sql
new file mode 100644
index 0000000..677de4f
--- /dev/null
+++ b/wire-gateway/magnet-bank/db/schema.sql
@@ -0,0 +1,309 @@
+--
+-- This file is part of TALER
+-- Copyright (C) 2025 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/>
+
+BEGIN;
+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 TABLE tx_in(
+ tx_in_id INT8 PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
+ magnet_code INT8 UNIQUE,
+ amount taler_amount NOT NULL,
+ subject TEXT NOT NULL,
+ debit_payto TEXT NOT NULL,
+ created INT8 NOT NULL
+);
+COMMENT ON TABLE tx_in IS 'Incoming transactions';
+
+CREATE TABLE tx_out(
+ tx_out_id INT8 PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
+ magnet_code INT8 UNIQUE,
+ amount taler_amount NOT NULL,
+ subject TEXT NOT NULL,
+ credit_payto TEXT NOT NULL,
+ created INT8 NOT NULL
+);
+COMMENT ON TABLE tx_in IS 'Outgoing transactions';
+
+CREATE TYPE incoming_type AS ENUM
+ ('reserve' ,'kyc', 'wad');
+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,
+ metadata BYTEA NOT NULL,
+ origin_exchange_url TEXT,
+ 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
+ )
+);
+COMMENT ON TABLE tx_in IS 'Incoming talerable transactions';
+
+CREATE UNIQUE INDEX taler_in_unique_reserve_pub ON taler_in (metadata) WHERE
type = 'reserve';
+
+CREATE TABLE taler_out(
+ tx_out_id INT8 PRIMARY KEY REFERENCES tx_out(tx_out_id) ON DELETE CASCADE,
+ wtid BYTEA NOT NULL UNIQUE CHECK (LENGTH(wtid)=32),
+ exchange_base_url TEXT NOT NULL
+);
+COMMENT ON TABLE tx_in IS 'Outgoing talerable transactions';
+
+CREATE TYPE transfer_status AS ENUM(
+ 'pending',
+ 'transient_failure',
+ 'permanent_failure',
+ 'success',
+ 'late_failure'
+);
+COMMENT ON TYPE transfer_status IS 'Status of an initiated outgoing
transaction';
+
+CREATE TABLE initiated(
+ initiated_id INT8 PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
+ amount taler_amount NOT NULL,
+ subject TEXT NOT NULL,
+ credit_payto TEXT NOT NULL,
+ status transfer_status NOT NULL DEFAULT 'pending',
+ status_msg TEXT,
+ magnet_code INT8 UNIQUE,
+ last_submitted INT8,
+ submission_counter INT2 NOT NULL DEFAULT 0,
+ tx_out_id INT8 UNIQUE REFERENCES tx_out(tx_out_id) ON DELETE CASCADE,
+ created INT8 NOT NULL
+);
+COMMENT ON TABLE tx_in IS 'Initiated outgoing transactions';
+
+CREATE TABLE transfer(
+ initiated_id INT8 PRIMARY KEY REFERENCES initiated(initiated_id) ON DELETE
CASCADE,
+ request_uid BYTEA UNIQUE NOT NULL CHECK (LENGTH(request_uid)=64),
+ wtid BYTEA UNIQUE NOT NULL CHECK (LENGTH(wtid)=32),
+ exchange_base_url TEXT NOT NULL
+);
+COMMENT ON TABLE transfer IS 'Wire Gateway transfers';
+
+CREATE FUNCTION register_tx_in(
+ IN in_code INT8,
+ IN in_amount taler_amount,
+ IN in_subject TEXT,
+ IN in_debit_payto TEXT,
+ IN in_timestamp INT8,
+ IN in_type incoming_type,
+ IN in_metadata BYTEA,
+ -- Success return
+ OUT out_tx_row_id INT8,
+ OUT out_timestamp INT8,
+ OUT out_new BOOLEAN
+)
+LANGUAGE plpgsql AS $$
+BEGIN
+-- Check for idempotence
+SELECT tx_in_id, created
+INTO out_tx_row_id, out_timestamp
+FROM tx_in
+WHERE (in_code IS NOT NULL AND magnet_code = in_code) -- Magnet transaction
+ OR (in_code IS NULL AND amount = in_amount AND debit_payto = in_debit_payto
AND subject = in_subject); -- Admin transaction
+
+out_new = NOT found;
+IF out_new THEN
+ -- Insert new incoming transaction
+ INSERT INTO tx_in (
+ magnet_code,
+ amount,
+ subject,
+ debit_payto,
+ created
+ ) VALUES (
+ in_code,
+ in_amount,
+ in_subject,
+ in_debit_payto,
+ in_timestamp
+ )
+ RETURNING tx_in_id, created
+ INTO out_tx_row_id, out_timestamp;
+ -- Notify new incoming transaction registration
+ PERFORM pg_notify('tx_in', out_tx_row_id || '');
+ IF in_type IS NOT NULL THEN
+ -- Insert new incoming talerable transaction
+ INSERT INTO taler_in (
+ tx_in_id,
+ type,
+ metadata
+ ) VALUES (
+ out_tx_row_id,
+ in_type,
+ in_metadata
+ );
+ -- Notify new incoming talerable transaction registration
+ PERFORM pg_notify('taler_in', out_tx_row_id || '');
+ END IF;
+END IF;
+END $$;
+COMMENT ON FUNCTION register_tx_in IS 'Register an incoming transaction
idempotently';
+
+CREATE FUNCTION register_tx_out(
+ IN in_code INT8,
+ IN in_amount taler_amount,
+ IN in_subject TEXT,
+ IN in_credit_payto TEXT,
+ IN in_timestamp INT8,
+ IN in_wtid BYTEA,
+ IN in_origin_exchange_url TEXT,
+ -- Success return
+ OUT out_tx_row_id INT8,
+ OUT out_timestamp INT8,
+ OUT out_new BOOLEAN
+)
+LANGUAGE plpgsql AS $$
+BEGIN
+-- Check for idempotence
+SELECT tx_out_id, created
+INTO out_tx_row_id, out_timestamp
+FROM tx_out WHERE magnet_code = in_code;
+
+out_new = NOT found;
+IF out_new THEN
+ -- Insert new outgoing transaction
+ INSERT INTO tx_out (
+ magnet_code,
+ amount,
+ subject,
+ credit_payto,
+ created
+ ) VALUES (
+ in_code,
+ in_amount,
+ in_subject,
+ in_credit_payto,
+ in_timestamp
+ )
+ RETURNING tx_out_id, created
+ INTO out_tx_row_id, out_timestamp;
+ -- Notify new outgoing transaction registration
+ PERFORM pg_notify('tx_out', out_tx_row_id || '');
+
+ IF in_wtid IS NOT NULL THEN
+ -- Insert new outgoing talerable transaction
+ INSERT INTO taler_out (
+ tx_out_id,
+ wtid,
+ exchange_base_url
+ ) VALUES (
+ out_tx_row_id,
+ in_wtid,
+ in_origin_exchange_url
+ );
+ -- Notify new outgoing talerable transaction registration
+ PERFORM pg_notify('taler_out', out_tx_row_id || '');
+ END IF;
+END IF;
+END $$;
+COMMENT ON FUNCTION register_tx_out IS 'Register an outgoing transaction
idempotently';
+
+CREATE FUNCTION taler_transfer(
+ IN in_request_uid BYTEA,
+ IN in_wtid BYTEA,
+ IN in_subject TEXT,
+ IN in_amount taler_amount,
+ IN in_exchange_base_url TEXT,
+ IN in_credit_payto TEXT,
+ IN in_timestamp INT8,
+ -- Error return
+ OUT out_request_uid_reuse BOOLEAN,
+ OUT out_wtid_reuse BOOLEAN,
+ -- Success return
+ OUT out_tx_row_id INT8,
+ OUT out_timestamp INT8
+)
+LANGUAGE plpgsql AS $$
+BEGIN
+-- Check for idempotence and conflict
+SELECT (amount != in_amount
+ OR credit_payto != in_credit_payto
+ OR exchange_base_url != in_exchange_base_url
+ OR wtid != in_wtid)
+ ,transfer.initiated_id, created
+INTO out_request_uid_reuse, out_tx_row_id, out_timestamp
+FROM transfer JOIN initiated USING (initiated_id)
+WHERE request_uid = in_request_uid;
+IF FOUND THEN
+ out_wtid_reuse=FALSE;
+ RETURN;
+END IF;
+out_request_uid_reuse=FALSE;
+-- Check for wtid reuse
+out_wtid_reuse = EXISTS(SELECT FROM transfer WHERE wtid=in_wtid);
+IF out_wtid_reuse THEN
+ RETURN;
+END IF;
+-- Insert an initiated outgoing transaction
+INSERT INTO initiated (
+ amount,
+ subject,
+ credit_payto,
+ created
+) VALUES (
+ in_amount,
+ in_subject,
+ in_credit_payto,
+ in_timestamp
+) RETURNING initiated_id, created
+INTO out_tx_row_id, out_timestamp;
+-- Insert a transfer operation
+INSERT INTO transfer (
+ initiated_id,
+ request_uid,
+ wtid,
+ exchange_base_url
+) VALUES (
+ out_tx_row_id,
+ in_request_uid,
+ in_wtid,
+ in_exchange_base_url
+);
+PERFORM pg_notify('transfer', out_tx_row_id || '');
+END $$;
+
+CREATE FUNCTION initiated_status_update(
+ IN in_initiated_id INT8,
+ IN in_status transfer_status,
+ IN in_status_msg TEXT
+)
+RETURNS void
+LANGUAGE plpgsql AS $$
+DECLARE
+current_status transfer_status;
+BEGIN
+ -- Check current status
+ SELECT status INTO current_status FROM initiated
+ WHERE initiated_id = in_initiated_id;
+ IF FOUND THEN
+ -- Update unsettled transaction status
+ IF current_status = 'success' AND in_status = 'permanent_failure' THEN
+ UPDATE initiated
+ SET status = 'late_failure', status_msg = in_status_msg
+ WHERE initiated_id = in_initiated_id;
+ ELSIF current_status NOT IN ('success', 'permanent_failure',
'late_failure') THEN
+ UPDATE initiated
+ SET status = in_status, status_msg = in_status_msg
+ WHERE initiated_id = in_initiated_id;
+ END IF;
+ END IF;
+END $$;
+
+COMMIT;
diff --git a/wire-gateway/magnet-bank/src/config.rs
b/wire-gateway/magnet-bank/src/config.rs
index f5e729d..bedc47b 100644
--- a/wire-gateway/magnet-bank/src/config.rs
+++ b/wire-gateway/magnet-bank/src/config.rs
@@ -14,11 +14,79 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+use std::net::{IpAddr, SocketAddr};
+
+use base64::{prelude::BASE64_STANDARD, Engine};
use reqwest::Url;
+use sqlx::postgres::PgConnectOptions;
+use taler_api::{auth::AuthMethod, Serve};
use taler_common::config::{Config, ValueError};
use crate::magnet::Token;
+pub struct DbConfig {
+ pub cfg: PgConnectOptions,
+}
+
+impl DbConfig {
+ pub fn parse(cfg: &Config) -> Result<Self, ValueError> {
+ let sect = cfg.section("magnet-bank-postgres");
+ Ok(Self {
+ cfg: sect.postgres("CONFIG").require()?,
+ })
+ }
+}
+
+pub struct WireGatewayConfig {
+ pub serve: Serve,
+ pub auth: AuthMethod,
+}
+
+impl WireGatewayConfig {
+ pub fn parse(cfg: &Config) -> Result<Self, ValueError> {
+ let sect = cfg.section("magnet-bank");
+
+ let parse_tcp = || {
+ let port = sect.number("PORT").require()?;
+ let ip: IpAddr = sect.parse("IP addr", "BIND_TO").require()?;
+ Ok(Serve::Tcp(SocketAddr::new(ip, port)))
+ };
+ let parse_unix = || {
+ let path = sect.path("UNIXPATH").require()?;
+ let permission = sect.unix_mode("UNIXPATH_MODE").require()?;
+ Ok(Serve::Unix { path, permission })
+ };
+ let serve = sect
+ .map::<&dyn Fn() -> Result<Serve, ValueError>>(
+ "serve",
+ "SERVE",
+ &[("tcp", &parse_tcp), ("unix", &parse_unix)],
+ )
+ .require()?()?;
+
+ let parse_basic = || {
+ let username = sect.str("USERNAME").require()?;
+ let password = sect.str("PASSWORD").require()?;
+ Ok(AuthMethod::Basic(
+ BASE64_STANDARD.encode(format!("{username}:{password}")),
+ ))
+ };
+ let parse_bearer = ||
Ok(AuthMethod::Bearer(sect.str("AUTH_TOKEN").require()?));
+ let auth = sect
+ .map::<&dyn Fn() -> Result<AuthMethod, ValueError>>(
+ "auth_method",
+ "AUTH_METHOD",
+ &[
+ ("none", &|| Ok(AuthMethod::None)),
+ ("basic", &parse_basic),
+ ("bearer", &parse_bearer),
+ ],
+ )
+ .require()?()?;
+ Ok(Self { serve, auth })
+ }
+}
+
pub struct MagnetConfig {
pub api_url: Url,
pub consumer: Token,
diff --git a/common/test-utils/src/lib.rs
b/wire-gateway/magnet-bank/src/constant.rs
similarity index 86%
copy from common/test-utils/src/lib.rs
copy to wire-gateway/magnet-bank/src/constant.rs
index d856785..c8d1680 100644
--- a/common/test-utils/src/lib.rs
+++ b/wire-gateway/magnet-bank/src/constant.rs
@@ -1,6 +1,6 @@
/*
This file is part of TALER
- Copyright (C) 2024 Taler Systems SA
+ Copyright (C) 2025 Taler Systems SA
TALER is free software; you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
Software
@@ -14,7 +14,4 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-pub use axum_test;
-pub mod helpers;
-pub mod json;
-pub mod routine;
+pub const CURRENCY: &str = "HUF";
diff --git a/wire-gateway/magnet-bank/src/db.rs
b/wire-gateway/magnet-bank/src/db.rs
new file mode 100644
index 0000000..9c1bf69
--- /dev/null
+++ b/wire-gateway/magnet-bank/src/db.rs
@@ -0,0 +1,976 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero 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 Affero General Public License for more
details.
+
+ You should have received a copy of the GNU Affero General Public License
along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+
+use std::fmt::Display;
+
+use sqlx::{postgres::PgRow, PgConnection, PgExecutor, PgPool, QueryBuilder,
Row};
+use taler_api::{
+ db::{history, page, BindHelper, IncomingType, TypeHelper},
+ subject::{IncomingSubject, OutgoingSubject},
+};
+use taler_common::{
+ api_params::{History, Page},
+ api_wire::{
+ IncomingBankTransaction, OutgoingBankTransaction, TransferListStatus,
TransferRequest,
+ TransferState, TransferStatus,
+ },
+ types::{amount::Amount, payto::Payto, timestamp::Timestamp},
+};
+
+use crate::constant::CURRENCY;
+
+#[derive(Debug, Clone)]
+pub struct TxIn {
+ pub code: u64,
+ pub amount: Amount,
+ pub subject: String,
+ pub debit_payto: Payto,
+ pub timestamp: Timestamp,
+}
+
+#[derive(Debug, Clone)]
+pub struct TxOut {
+ pub code: u64,
+ pub amount: Amount,
+ pub subject: String,
+ pub credit_payto: Payto,
+ pub timestamp: Timestamp,
+}
+
+#[derive(Debug, Clone)]
+pub struct TxInAdmin {
+ pub amount: Amount,
+ pub subject: String,
+ pub debit_payto: Payto,
+ pub timestamp: Timestamp,
+ pub metadata: IncomingSubject,
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct RegisteredTx {
+ pub new: bool,
+ pub row_id: u64,
+ pub timestamp: Timestamp,
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct Initiated {
+ pub id: u64,
+ pub amount: Amount,
+ pub subject: String,
+ pub creditor: Payto,
+}
+
+impl Display for Initiated {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "{} {} {} '{}'",
+ self.id, self.amount, self.creditor, self.subject
+ )
+ }
+}
+
+pub async fn db_init(db: &PgPool, reset: bool) -> sqlx::Result<()> {
+ let mut tx = db.begin().await?;
+ if reset {
+ sqlx::raw_sql("DROP SCHEMA public CASCADE;CREATE SCHEMA public;")
+ .execute(&mut *tx)
+ .await?;
+ }
+ // TODO migrations
+ sqlx::raw_sql(include_str!("../db/schema.sql"))
+ .execute(&mut *tx)
+ .await?;
+ tx.commit().await?;
+ Ok(())
+}
+
+pub async fn register_tx_in_admin(db: &PgPool, tx: &TxInAdmin) ->
sqlx::Result<RegisteredTx> {
+ sqlx::query(
+ "
+ SELECT out_new, out_tx_row_id, out_timestamp
+ FROM register_tx_in(NULL, ($1, $2)::taler_amount, $3, $4, $5, $6,
$7)
+ ",
+ )
+ .bind_amount(&tx.amount)
+ .bind(&tx.subject)
+ .bind(tx.debit_payto.raw())
+ .bind_timestamp(&tx.timestamp)
+ .bind(tx.metadata.ty())
+ .bind(tx.metadata.key())
+ .try_map(|r: PgRow| {
+ Ok(RegisteredTx {
+ new: r.try_get(0)?,
+ row_id: r.try_get_u64(1)?,
+ timestamp: r.try_get_timestamp(2)?,
+ })
+ })
+ .fetch_one(db)
+ .await
+}
+
+pub async fn register_tx_in(
+ db: &mut PgConnection,
+ tx: &TxIn,
+ subject: &Option<IncomingSubject>,
+) -> sqlx::Result<RegisteredTx> {
+ sqlx::query(
+ "
+ SELECT out_new, out_tx_row_id, out_timestamp
+ FROM register_tx_in($1, ($2, $3)::taler_amount, $4, $5, $6, $7, $8)
+ ",
+ )
+ .bind(tx.code as i64)
+ .bind_amount(&tx.amount)
+ .bind(&tx.subject)
+ .bind(tx.debit_payto.raw())
+ .bind_timestamp(&tx.timestamp)
+ .bind(subject.as_ref().map(|it| it.ty()))
+ .bind(subject.as_ref().map(|it| it.key()))
+ .try_map(|r: PgRow| {
+ Ok(RegisteredTx {
+ new: r.try_get(0)?,
+ row_id: r.try_get_u64(1)?,
+ timestamp: r.try_get_timestamp(2)?,
+ })
+ })
+ .fetch_one(db)
+ .await
+}
+
+pub async fn register_tx_out(
+ db: &mut PgConnection,
+ tx: &TxOut,
+ subject: &Option<OutgoingSubject>,
+) -> sqlx::Result<RegisteredTx> {
+ sqlx::query(
+ "
+ SELECT out_new, out_tx_row_id, out_timestamp
+ FROM register_tx_out($1, ($2, $3)::taler_amount, $4, $5, $6, $7,
$8)
+ ",
+ )
+ .bind(tx.code as i64)
+ .bind_amount(&tx.amount)
+ .bind(&tx.subject)
+ .bind(tx.credit_payto.raw())
+ .bind_timestamp(&tx.timestamp)
+ .bind(subject.as_ref().map(|it| it.0.as_ref()))
+ .bind(subject.as_ref().map(|it| it.1.as_str()))
+ .try_map(|r: PgRow| {
+ Ok(RegisteredTx {
+ new: r.try_get(0)?,
+ row_id: r.try_get_u64(1)?,
+ timestamp: r.try_get_timestamp(2)?,
+ })
+ })
+ .fetch_one(db)
+ .await
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum TransferResult {
+ Success { id: u64, timestamp: Timestamp },
+ RequestUidReuse,
+ WtidReuse,
+}
+
+pub async fn make_transfer<'a>(
+ db: impl PgExecutor<'a>,
+ req: &TransferRequest,
+ timestamp: &Timestamp,
+) -> sqlx::Result<TransferResult> {
+ let subject = format!("{} {}", req.wtid, req.exchange_base_url);
+ sqlx::query(
+ "
+ SELECT out_request_uid_reuse, out_wtid_reuse, out_tx_row_id,
out_timestamp
+ FROM taler_transfer($1, $2, $3, ($4, $5)::taler_amount, $6, $7, $8)
+ ",
+ )
+ .bind(req.request_uid.as_ref())
+ .bind(req.wtid.as_ref())
+ .bind(&subject)
+ .bind_amount(&req.amount)
+ .bind(req.exchange_base_url.as_str())
+ .bind(req.credit_account.raw())
+ .bind_timestamp(timestamp)
+ .try_map(|r: PgRow| {
+ Ok(if r.try_get(0)? {
+ TransferResult::RequestUidReuse
+ } else if r.try_get(1)? {
+ TransferResult::WtidReuse
+ } else {
+ TransferResult::Success {
+ id: r.try_get_u64(2)?,
+ timestamp: r.try_get_timestamp(3)?,
+ }
+ })
+ })
+ .fetch_one(db)
+ .await
+}
+
+pub async fn transfer_page<'a>(
+ db: impl PgExecutor<'a>,
+ status: &Option<TransferState>,
+ params: &Page,
+) -> sqlx::Result<Vec<TransferListStatus>> {
+ page(
+ db,
+ "initiated_id",
+ params,
+ || {
+ let mut builder = QueryBuilder::new(
+ "
+ SELECT
+ initiated_id,
+ status,
+ (amount).val as amount_val,
+ (amount).frac as amount_frac,
+ credit_payto,
+ created
+ FROM transfer
+ JOIN initiated USING (initiated_id)
+ WHERE
+ ",
+ );
+ if let Some(status) = status {
+ builder.push(" status = ").push_bind(status).push(" AND ");
+ }
+ builder
+ },
+ |r: PgRow| {
+ Ok(TransferListStatus {
+ 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_payto(4)?,
+ timestamp: r.try_get_timestamp(5)?,
+ })
+ },
+ )
+ .await
+}
+
+pub async fn outgoing_history(
+ db: &PgPool,
+ params: &History,
+) -> sqlx::Result<Vec<OutgoingBankTransaction>> {
+ history(
+ db,
+ "tx_out_id",
+ params,
+ || tokio::sync::watch::channel(0).1,
+ || {
+ QueryBuilder::new(
+ "
+ SELECT
+ tx_out_id,
+ (amount).val as amount_val,
+ (amount).frac as amount_frac,
+ credit_payto,
+ created,
+ exchange_base_url,
+ wtid
+ FROM taler_out
+ JOIN tx_out USING (tx_out_id)
+ WHERE
+ ",
+ )
+ },
+ |r: PgRow| {
+ Ok(OutgoingBankTransaction {
+ row_id: r.try_get_safeu64(0)?,
+ amount: r.try_get_amount_i(1, CURRENCY)?,
+ credit_account: r.try_get_payto(3)?,
+ date: r.try_get_timestamp(4)?,
+ exchange_base_url: r.try_get_url(5)?,
+ wtid: r.try_get_base32(6)?,
+ })
+ },
+ )
+ .await
+}
+
+pub async fn incoming_history(
+ db: &PgPool,
+ params: &History,
+) -> sqlx::Result<Vec<IncomingBankTransaction>> {
+ history(
+ db,
+ "tx_in_id",
+ params,
+ || tokio::sync::watch::channel(0).1,
+ || {
+ QueryBuilder::new(
+ "
+ SELECT
+ tx_in_id,
+ (amount).val as amount_val,
+ (amount).frac as amount_frac,
+ debit_payto,
+ created,
+ type,
+ metadata
+ FROM taler_in
+ JOIN tx_in USING (tx_in_id)
+ WHERE
+ ",
+ )
+ },
+ |r: PgRow| {
+ Ok(match r.try_get(5)? {
+ IncomingType::reserve => IncomingBankTransaction::Reserve {
+ row_id: r.try_get_safeu64(0)?,
+ amount: r.try_get_amount_i(1, CURRENCY)?,
+ debit_account: r.try_get_payto(3)?,
+ date: r.try_get_timestamp(4)?,
+ reserve_pub: r.try_get_base32(6)?,
+ },
+ IncomingType::kyc => IncomingBankTransaction::Kyc {
+ row_id: r.try_get_safeu64(0)?,
+ amount: r.try_get_amount_i(1, CURRENCY)?,
+ debit_account: r.try_get_payto(3)?,
+ date: r.try_get_timestamp(4)?,
+ account_pub: r.try_get_base32(6)?,
+ },
+ IncomingType::wad => {
+ unimplemented!("WAD is not yet supported")
+ }
+ })
+ },
+ )
+ .await
+}
+
+pub async fn transfer_by_id<'a>(
+ db: impl PgExecutor<'a>,
+ id: u64,
+) -> sqlx::Result<Option<TransferStatus>> {
+ sqlx::query(
+ "
+ SELECT
+ status,
+ status_msg,
+ (amount).val as amount_val,
+ (amount).frac as amount_frac,
+ exchange_base_url,
+ wtid,
+ credit_payto,
+ created
+ FROM transfer
+ JOIN initiated USING (initiated_id)
+ WHERE initiated_id = $1
+ ",
+ )
+ .bind(id as i64)
+ .try_map(|r: PgRow| {
+ Ok(TransferStatus {
+ status: r.try_get(1)?,
+ status_msg: r.try_get(2)?,
+ amount: r.try_get_amount_i(3, CURRENCY)?,
+ origin_exchange_url: r.try_get(5)?,
+ wtid: r.try_get_base32(6)?,
+ credit_account: r.try_get_payto(7)?,
+ timestamp: r.try_get_timestamp(8)?,
+ })
+ })
+ .fetch_optional(db)
+ .await
+}
+
+pub async fn pending_batch<'a>(
+ db: impl PgExecutor<'a>,
+ start: &Timestamp,
+) -> sqlx::Result<Vec<Initiated>> {
+ sqlx::query(
+ "
+ SELECT initiated_id, (amount).val, (amount).frac, subject,
credit_payto
+ FROM initiated
+ WHERE magnet_code IS NULL AND (last_submitted IS NULL OR
last_submitted < $1)
+ LIMIT 100
+ ",
+ )
+ .bind_timestamp(start)
+ .try_map(|r: PgRow| {
+ Ok(Initiated {
+ id: r.try_get_u64(0)?,
+ amount: r.try_get_amount_i(1, CURRENCY)?,
+ subject: r.try_get(3)?,
+ creditor: r.try_get_payto(4)?,
+ })
+ })
+ .fetch_all(db)
+ .await
+}
+
+/** Update status of a sucessfull submitted initiated transaction */
+pub async fn initiated_submit_success<'a>(
+ db: impl PgExecutor<'a>,
+ id: u64,
+ timestamp: &Timestamp,
+ magnet_code: u64,
+) -> sqlx::Result<()> {
+ sqlx::query(
+ "
+ UPDATE initiated
+ SET status='pending', submission_counter=submission_counter+1,
last_submitted=$1, magnet_code=$2
+ WHERE initiated_id=$3
+ "
+ ).bind_timestamp(timestamp)
+ .bind(magnet_code as i64)
+ .bind(id as i64)
+ .execute(db).await?;
+ Ok(())
+}
+
+/** Update status of a sucessfull submitted initiated transaction */
+pub async fn initiated_submit_failure<'a>(
+ db: impl PgExecutor<'a>,
+ id: u64,
+ timestamp: &Timestamp,
+ msg: &str,
+) -> sqlx::Result<()> {
+ sqlx::query(
+ "
+ UPDATE initiated
+ SET status='pending', submission_counter=submission_counter+1,
last_submitted=$1, status_msg=$2
+ WHERE initiated_id=$3
+ ",
+ )
+ .bind_timestamp(timestamp)
+ .bind(msg)
+ .bind(id as i64)
+ .execute(db)
+ .await?;
+ Ok(())
+}
+
+#[cfg(test)]
+mod test {
+
+ use sqlx::{postgres::PgRow, PgConnection, PgPool};
+ use taler_api::{
+ db::TypeHelper,
+ subject::{IncomingSubject, OutgoingSubject},
+ };
+ use taler_common::{
+ api_common::{EddsaPublicKey, HashCode, ShortHashCode},
+ api_params::{History, Page},
+ api_wire::TransferRequest,
+ types::{amount::amount, payto::payto, timestamp::Timestamp, url},
+ };
+
+ use crate::{
+ constant::CURRENCY,
+ db::{
+ self, make_transfer, register_tx_in, register_tx_in_admin,
register_tx_out,
+ RegisteredTx, TransferResult, TxIn, TxOut,
+ },
+ };
+
+ use super::TxInAdmin;
+
+ async fn setup() -> (PgConnection, PgPool) {
+ let pool = test_utils::db_test_setup().await;
+ db::db_init(&pool, false).await.expect("dbinit");
+ let conn = pool.acquire().await.expect("aquire conn").leak();
+ (conn, pool)
+ }
+
+ #[tokio::test]
+ async fn tx_in() {
+ let (mut db, pool) = setup().await;
+
+ async fn routine(
+ db: &mut PgConnection,
+ first: &Option<IncomingSubject>,
+ second: &Option<IncomingSubject>,
+ ) {
+ let (id, code) =
+ sqlx::query("SELECT count(*) + 1, COALESCE(max(magnet_code),
0) + 20 FROM tx_in")
+ .try_map(|r: PgRow| Ok((r.try_get_u64(0)?,
r.try_get_u64(1)?)))
+ .fetch_one(&mut *db)
+ .await
+ .unwrap();
+ let tx = TxIn {
+ code: code,
+ amount: amount("EUR:10"),
+ subject: "subject".to_owned(),
+ debit_payto: payto("payto://"),
+ timestamp: Timestamp::now_stable(),
+ };
+ // Insert
+ assert_eq!(
+ register_tx_in(db, &tx, &first)
+ .await
+ .expect("register tx in"),
+ RegisteredTx {
+ new: true,
+ row_id: id,
+ timestamp: tx.timestamp
+ }
+ );
+ // Idempotent
+ assert_eq!(
+ register_tx_in(
+ db,
+ &TxIn {
+ timestamp: Timestamp::now(),
+ ..tx.clone()
+ },
+ &first
+ )
+ .await
+ .expect("register tx in"),
+ RegisteredTx {
+ new: false,
+ row_id: id,
+ timestamp: tx.timestamp
+ }
+ );
+ // Many
+ assert_eq!(
+ register_tx_in(
+ db,
+ &TxIn {
+ code: code + 1,
+ ..tx
+ },
+ &second
+ )
+ .await
+ .expect("register tx in"),
+ RegisteredTx {
+ new: true,
+ row_id: id + 1,
+ timestamp: tx.timestamp
+ }
+ );
+ }
+
+ // Empty db
+ assert_eq!(
+ db::incoming_history(&pool, &History::default())
+ .await
+ .unwrap(),
+ Vec::new()
+ );
+
+ // Regular transaction
+ routine(&mut db, &None, &None).await;
+
+ // Reserve transaction
+ routine(
+ &mut db,
+ &Some(IncomingSubject::Reserve(EddsaPublicKey::rand())),
+ &Some(IncomingSubject::Reserve(EddsaPublicKey::rand())),
+ )
+ .await;
+
+ // Kyc transaction
+ routine(
+ &mut db,
+ &Some(IncomingSubject::Kyc(EddsaPublicKey::rand())),
+ &Some(IncomingSubject::Kyc(EddsaPublicKey::rand())),
+ )
+ .await;
+
+ // History
+ assert_eq!(
+ db::incoming_history(&pool, &History::default())
+ .await
+ .unwrap()
+ .len(),
+ 4
+ );
+ }
+
+ #[tokio::test]
+ async fn tx_in_admin() {
+ let (mut db, pool) = setup().await;
+
+ // Empty db
+ assert_eq!(
+ db::incoming_history(&pool, &History::default())
+ .await
+ .unwrap(),
+ Vec::new()
+ );
+
+ let tx = TxInAdmin {
+ amount: amount("EUR:10"),
+ subject: "subject".to_owned(),
+ debit_payto: payto("payto://"),
+ timestamp: Timestamp::now_stable(),
+ metadata: IncomingSubject::Reserve(EddsaPublicKey::rand()),
+ };
+ // Insert
+ assert_eq!(
+ register_tx_in_admin(&pool, &tx)
+ .await
+ .expect("register tx in"),
+ RegisteredTx {
+ new: true,
+ row_id: 1,
+ timestamp: tx.timestamp
+ }
+ );
+ // Idempotent
+ assert_eq!(
+ register_tx_in_admin(
+ &pool,
+ &TxInAdmin {
+ timestamp: Timestamp::now(),
+ ..tx.clone()
+ }
+ )
+ .await
+ .expect("register tx in"),
+ RegisteredTx {
+ new: false,
+ row_id: 1,
+ timestamp: tx.timestamp
+ }
+ );
+ // Many
+ assert_eq!(
+ register_tx_in_admin(
+ &pool,
+ &TxInAdmin {
+ subject: "Other".to_owned(),
+ metadata: IncomingSubject::Reserve(EddsaPublicKey::rand()),
+ ..tx.clone()
+ }
+ )
+ .await
+ .expect("register tx in"),
+ RegisteredTx {
+ new: true,
+ row_id: 2,
+ timestamp: tx.timestamp
+ }
+ );
+
+ // History
+ assert_eq!(
+ db::incoming_history(&pool, &History::default())
+ .await
+ .unwrap()
+ .len(),
+ 2
+ );
+ }
+
+ #[tokio::test]
+ async fn tx_out() {
+ let (mut db, pool) = setup().await;
+
+ async fn routine(
+ db: &mut PgConnection,
+ first: &Option<OutgoingSubject>,
+ second: &Option<OutgoingSubject>,
+ ) {
+ let (id, code) =
+ sqlx::query("SELECT count(*) + 1, COALESCE(max(magnet_code),
0) + 20 FROM tx_out")
+ .try_map(|r: PgRow| Ok((r.try_get_u64(0)?,
r.try_get_u64(1)?)))
+ .fetch_one(&mut *db)
+ .await
+ .unwrap();
+ let tx = TxOut {
+ code: code,
+ amount: amount("EUR:10"),
+ subject: "subject".to_owned(),
+ credit_payto: payto("payto://"),
+ timestamp: Timestamp::now_stable(),
+ };
+ // Insert
+ assert_eq!(
+ register_tx_out(db, &tx, &first)
+ .await
+ .expect("register tx out"),
+ RegisteredTx {
+ new: true,
+ row_id: id,
+ timestamp: tx.timestamp
+ }
+ );
+ // Idempotent
+ assert_eq!(
+ register_tx_out(
+ db,
+ &TxOut {
+ timestamp: Timestamp::now(),
+ ..tx.clone()
+ },
+ &first
+ )
+ .await
+ .expect("register tx out"),
+ RegisteredTx {
+ new: false,
+ row_id: id,
+ timestamp: tx.timestamp
+ }
+ );
+ // Many
+ assert_eq!(
+ register_tx_out(
+ db,
+ &TxOut {
+ code: code + 1,
+ ..tx.clone()
+ },
+ &second
+ )
+ .await
+ .expect("register tx out"),
+ RegisteredTx {
+ new: true,
+ row_id: id + 1,
+ timestamp: tx.timestamp
+ }
+ );
+ }
+
+ // Empty db
+ assert_eq!(
+ db::outgoing_history(&pool, &History::default())
+ .await
+ .unwrap(),
+ Vec::new()
+ );
+
+ // Regular transaction
+ routine(&mut db, &None, &None).await;
+
+ // Talerable transaction
+ routine(
+ &mut db,
+ &Some(OutgoingSubject(
+ ShortHashCode::rand(),
+ url("https://exchange.com"),
+ )),
+ &Some(OutgoingSubject(
+ ShortHashCode::rand(),
+ url("https://exchange.com"),
+ )),
+ )
+ .await;
+
+ // History
+ assert_eq!(
+ db::outgoing_history(&pool, &History::default())
+ .await
+ .unwrap()
+ .len(),
+ 2
+ );
+ }
+
+ #[tokio::test]
+ async fn transfer() {
+ let (mut db, _) = setup().await;
+
+ // Empty db
+ assert_eq!(db::transfer_by_id(&mut db, 0).await.unwrap(), None);
+ assert_eq!(
+ db::transfer_page(&mut db, &None, &Page::default())
+ .await
+ .unwrap(),
+ Vec::new()
+ );
+
+ let req = TransferRequest {
+ request_uid: HashCode::rand(),
+ amount: amount("EUR:10"),
+ exchange_base_url: url("https://exchange.test.com/"),
+ wtid: ShortHashCode::rand(),
+ credit_account: payto("payto://"),
+ };
+ let timestamp = Timestamp::now_stable();
+ // Insert
+ assert_eq!(
+ make_transfer(&mut db, &req, ×tamp)
+ .await
+ .expect("transfer"),
+ TransferResult::Success {
+ id: 1,
+ timestamp: timestamp
+ }
+ );
+ // Idempotent
+ assert_eq!(
+ make_transfer(&mut db, &req, &Timestamp::now())
+ .await
+ .expect("transfer"),
+ TransferResult::Success {
+ id: 1,
+ timestamp: timestamp
+ }
+ );
+ // Request UID reuse
+ assert_eq!(
+ make_transfer(
+ &mut db,
+ &TransferRequest {
+ wtid: ShortHashCode::rand(),
+ ..req.clone()
+ },
+ &Timestamp::now()
+ )
+ .await
+ .expect("transfer"),
+ TransferResult::RequestUidReuse
+ );
+ // wtid reuse
+ assert_eq!(
+ make_transfer(
+ &mut db,
+ &TransferRequest {
+ request_uid: HashCode::rand(),
+ ..req.clone()
+ },
+ &Timestamp::now()
+ )
+ .await
+ .expect("transfer"),
+ TransferResult::WtidReuse
+ );
+ // Many
+ assert_eq!(
+ make_transfer(
+ &mut db,
+ &TransferRequest {
+ request_uid: HashCode::rand(),
+ wtid: ShortHashCode::rand(),
+ ..req
+ },
+ ×tamp
+ )
+ .await
+ .expect("transfer"),
+ TransferResult::Success {
+ id: 2,
+ timestamp: timestamp
+ }
+ );
+
+ // Get
+ //assert!(db::transfer_by_id(&mut db, 1).await.unwrap().is_some());
+ //assert!(db::transfer_by_id(&mut db, 2).await.unwrap().is_some());
+ assert_eq!(
+ db::transfer_page(&mut db, &None, &Page::default())
+ .await
+ .unwrap()
+ .len(),
+ 2
+ );
+ }
+
+ #[tokio::test]
+ async fn status() {
+ let (mut db, _) = setup().await;
+
+ // Unknown transfer
+ db::initiated_submit_failure(&mut db, 1, &Timestamp::now(), "msg")
+ .await
+ .unwrap();
+ db::initiated_submit_success(&mut db, 1, &Timestamp::now(), 12)
+ .await
+ .unwrap();
+ }
+
+ #[tokio::test]
+ async fn batch() {
+ let (mut db, _) = setup().await;
+ let start = Timestamp::now();
+
+ // Empty db
+ let pendings = db::pending_batch(&mut db, &start)
+ .await
+ .expect("pending_batch");
+ assert_eq!(pendings.len(), 0);
+
+ // Some transfers
+ for i in 0..3 {
+ make_transfer(
+ &mut db,
+ &TransferRequest {
+ request_uid: HashCode::rand(),
+ amount: amount(format!("{CURRENCY}:{}", i + 1)),
+ exchange_base_url: url("https://exchange.test.com/"),
+ wtid: ShortHashCode::rand(),
+ credit_account: payto("payto://"),
+ },
+ &Timestamp::now(),
+ )
+ .await
+ .expect("transfer");
+ }
+ let pendings = db::pending_batch(&mut db, &start)
+ .await
+ .expect("pending_batch");
+ assert_eq!(pendings.len(), 3);
+
+ // Max 100 txs in batch
+ for i in 0..100 {
+ make_transfer(
+ &mut db,
+ &TransferRequest {
+ request_uid: HashCode::rand(),
+ amount: amount(format!("{CURRENCY}:{}", i + 1)),
+ exchange_base_url: url("https://exchange.test.com/"),
+ wtid: ShortHashCode::rand(),
+ credit_account: payto("payto://"),
+ },
+ &Timestamp::now(),
+ )
+ .await
+ .expect("transfer");
+ }
+ let pendings = db::pending_batch(&mut db, &start)
+ .await
+ .expect("pending_batch");
+ assert_eq!(pendings.len(), 100);
+
+ // Skip uploaded
+ for i in 0..=10 {
+ db::initiated_submit_success(&mut db, i, &Timestamp::now(), i)
+ .await
+ .expect("status success");
+ }
+ let pendings = db::pending_batch(&mut db, &start)
+ .await
+ .expect("pending_batch");
+ assert_eq!(pendings.len(), 93);
+
+ // Skip tried since start
+ for i in 0..=10 {
+ db::initiated_submit_failure(&mut db, 10 + i, &Timestamp::now(),
"failure")
+ .await
+ .expect("status failure");
+ }
+ let pendings = db::pending_batch(&mut db, &start)
+ .await
+ .expect("pending_batch");
+ assert_eq!(pendings.len(), 83);
+ let pendings = db::pending_batch(&mut db, &Timestamp::now())
+ .await
+ .expect("pending_batch");
+ assert_eq!(pendings.len(), 93);
+ }
+}
diff --git a/common/test-utils/src/lib.rs b/wire-gateway/magnet-bank/src/lib.rs
similarity index 83%
copy from common/test-utils/src/lib.rs
copy to wire-gateway/magnet-bank/src/lib.rs
index d856785..0d8c518 100644
--- a/common/test-utils/src/lib.rs
+++ b/wire-gateway/magnet-bank/src/lib.rs
@@ -1,6 +1,6 @@
/*
This file is part of TALER
- Copyright (C) 2024 Taler Systems SA
+ Copyright (C) 2025 Taler Systems SA
TALER is free software; you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
Software
@@ -14,7 +14,9 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-pub use axum_test;
-pub mod helpers;
-pub mod json;
-pub mod routine;
+pub mod config;
+pub mod constant;
+pub mod db;
+pub mod keys;
+pub mod magnet;
+pub mod wire_gateway;
diff --git a/wire-gateway/magnet-bank/src/magnet/error.rs
b/wire-gateway/magnet-bank/src/magnet/error.rs
index 3c24144..6844432 100644
--- a/wire-gateway/magnet-bank/src/magnet/error.rs
+++ b/wire-gateway/magnet-bank/src/magnet/error.rs
@@ -41,7 +41,7 @@ pub struct MagnetError {
#[derive(Error, Debug)]
pub enum ApiError {
#[error("transport: {0}")]
- Transport(#[from] reqwest::Error),
+ Transport(FmtSource<reqwest::Error>),
#[error("magnet {0}")]
Magnet(#[from] MagnetError),
#[error("JSON body: {0}")]
@@ -54,6 +54,42 @@ pub enum ApiError {
StatusCause(StatusCode, String),
}
+#[derive(Debug)]
+pub struct FmtSource<E: std::error::Error>(E);
+
+fn fmt_with_source(
+ f: &mut std::fmt::Formatter<'_>,
+ mut e: &dyn std::error::Error,
+) -> std::fmt::Result {
+ loop {
+ write!(f, "{}", &e)?;
+ if let Some(source) = e.source() {
+ write!(f, ": ")?;
+ e = source;
+ } else {
+ return Ok(());
+ }
+ }
+}
+
+impl<E: std::error::Error> std::fmt::Display for FmtSource<E> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ fmt_with_source(f, &self.0)
+ }
+}
+
+impl<E: std::error::Error> From<E> for FmtSource<E> {
+ fn from(value: E) -> Self {
+ Self(value)
+ }
+}
+
+impl From<reqwest::Error> for ApiError {
+ fn from(value: reqwest::Error) -> Self {
+ Self::Transport(FmtSource(value))
+ }
+}
+
pub type ApiResult<R> = std::result::Result<R, ApiError>;
/** Handle error from magnet API calls */
@@ -105,7 +141,7 @@ async fn magnet_url<T: DeserializeOwned>(response:
reqwest::Result<Response>) ->
serde_urlencoded::from_str(&body).map_err(ApiError::Form)
}
-pub trait MagnetBuilder {
+pub(crate) trait MagnetBuilder {
async fn magnet_call_encoded<T: DeserializeOwned>(self) -> ApiResult<T>;
async fn magnet_call<T: DeserializeOwned>(self) -> ApiResult<T>;
async fn magnet_empty(self) -> ApiResult<()>;
diff --git a/wire-gateway/magnet-bank/src/magnet/oauth.rs
b/wire-gateway/magnet-bank/src/magnet/oauth.rs
index 3eefec6..c1600d9 100644
--- a/wire-gateway/magnet-bank/src/magnet/oauth.rs
+++ b/wire-gateway/magnet-bank/src/magnet/oauth.rs
@@ -19,9 +19,9 @@ use std::{borrow::Cow, time::SystemTime};
use base64::{prelude::BASE64_STANDARD, Engine as _};
use hmac::{Hmac, Mac};
use percent_encoding::NON_ALPHANUMERIC;
+use rand_core::RngCore;
use reqwest::header::HeaderValue;
use sha1::Sha1;
-use rand_core::RngCore;
use super::Token;
diff --git a/wire-gateway/magnet-bank/src/main.rs
b/wire-gateway/magnet-bank/src/main.rs
index c9bd3f0..e4d9db6 100644
--- a/wire-gateway/magnet-bank/src/main.rs
+++ b/wire-gateway/magnet-bank/src/main.rs
@@ -14,18 +14,22 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-use std::{future::Future, path::PathBuf};
+use std::{future::Future, path::PathBuf, sync::Arc};
use clap::Parser;
-use config::MagnetConfig;
-use taler_common::config::{parser::ConfigSource, Config};
+use magnet_bank::{
+ config::{DbConfig, MagnetConfig, WireGatewayConfig},
+ db, keys,
+ wire_gateway::MagnetWireGateway,
+};
+use sqlx::PgPool;
+use taler_common::{
+ config::{parser::ConfigSource, Config},
+ types::payto::payto,
+};
use tracing::{error, Level};
use tracing_subscriber::{util::SubscriberInitExt as _, FmtSubscriber};
-mod config;
-mod keys;
-mod magnet;
-
#[derive(clap::Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
@@ -45,17 +49,25 @@ struct Args {
#[derive(clap::Subcommand, Debug)]
enum Command {
- /// Setup Magnet Bank auth token and account settings for Wire Gateway use
+ /// Setup magnet-bank auth token and account settings for Wire Gateway use
Setup,
+ /// Initialize magnet-bank database
+ Dbinit {
+ #[clap(long, short)]
+ reset: bool,
+ },
+ /// Run magnet-bank HTTP server
+ Serve,
}
fn setup(level: Option<tracing::Level>, app: impl Future<Output = Result<(),
anyhow::Error>>) {
// Setup logger
let level = level.unwrap_or(Level::INFO);
- let guard = FmtSubscriber::builder()
+ FmtSubscriber::builder()
.with_max_level(level)
+ .with_writer(std::io::stderr)
.finish()
- .set_default();
+ .init();
// Setup async runtime
let runtime = tokio::runtime::Builder::new_multi_thread()
@@ -69,16 +81,38 @@ fn setup(level: Option<tracing::Level>, app: impl
Future<Output = Result<(), any
error!("{}", err);
std::process::exit(1);
}
- drop(guard);
}
async fn app(args: Args) -> Result<(), anyhow::Error> {
let source = ConfigSource::new("magnet-bank", "magnet-bank",
"magnet-bank");
let cfg = Config::from_file(source, args.config)?;
- let cfg = MagnetConfig::parse(&cfg)?;
+
match args.cmd {
- Command::Setup => keys::setup(cfg).await?,
+ Command::Setup => {
+ let cfg = MagnetConfig::parse(&cfg)?;
+ keys::setup(cfg).await?
+ }
+ Command::Dbinit { reset } => {
+ let db = DbConfig::parse(&cfg)?;
+ let pool = PgPool::connect_with(db.cfg).await?;
+ db::db_init(&pool, reset).await?;
+ }
+ Command::Serve => {
+ let db = DbConfig::parse(&cfg)?;
+ let pool = PgPool::connect_with(db.cfg).await?;
+ let cfg = WireGatewayConfig::parse(&cfg)?;
+ taler_api::server(
+ taler_api::wire_gateway_api(Arc::new(MagnetWireGateway {
+ pool,
+ payto: payto("payto://todo"),
+ })),
+ cfg.serve,
+ cfg.auth,
+ None,
+ )
+ .await?;
+ }
}
Ok(())
}
diff --git a/wire-gateway/magnet-bank/src/wire_gateway.rs
b/wire-gateway/magnet-bank/src/wire_gateway.rs
new file mode 100644
index 0000000..a73e571
--- /dev/null
+++ b/wire-gateway/magnet-bank/src/wire_gateway.rs
@@ -0,0 +1,142 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2025 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero 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 Affero General Public License for more
details.
+
+ You should have received a copy of the GNU Affero General Public License
along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+
+use taler_api::{
+ error::{failure, ApiResult},
+ subject::IncomingSubject,
+ WireGatewayImpl,
+};
+use taler_common::{
+ api_common::{safe_u64, SafeU64},
+ api_params::{History, Page},
+ api_wire::{
+ AddIncomingRequest, AddIncomingResponse, AddKycauthRequest,
AddKycauthResponse,
+ IncomingHistory, OutgoingHistory, TransferList, TransferRequest,
TransferResponse,
+ TransferState, TransferStatus,
+ },
+ error_code::ErrorCode,
+ types::{
+ payto::{payto, Payto},
+ timestamp::Timestamp,
+ },
+};
+
+use crate::{
+ constant::CURRENCY,
+ db::{self, TxInAdmin},
+};
+
+pub struct MagnetWireGateway {
+ pub pool: sqlx::PgPool,
+ pub payto: Payto,
+}
+
+impl WireGatewayImpl for MagnetWireGateway {
+ fn name(&self) -> &str {
+ "magnet-bank"
+ }
+
+ fn currency(&self) -> &str {
+ CURRENCY
+ }
+
+ fn implementation(&self) -> Option<&str> {
+ None
+ }
+
+ async fn transfer(&self, req: TransferRequest) ->
ApiResult<TransferResponse> {
+ let result = db::make_transfer(&self.pool, &req,
&Timestamp::now()).await?;
+ match result {
+ db::TransferResult::Success { id, timestamp } =>
Ok(TransferResponse {
+ timestamp,
+ row_id: SafeU64::try_from(id).unwrap(),
+ }),
+ db::TransferResult::RequestUidReuse => Err(failure(
+ ErrorCode::BANK_TRANSFER_REQUEST_UID_REUSED,
+ "request_uid used already",
+ )),
+ db::TransferResult::WtidReuse => unimplemented!(),
+ }
+ }
+
+ async fn transfer_page(
+ &self,
+ page: Page,
+ status: Option<TransferState>,
+ ) -> ApiResult<TransferList> {
+ Ok(TransferList {
+ transfers: db::transfer_page(&self.pool, &status, &page).await?,
+ debit_account: payto("payto://todo"),
+ })
+ }
+
+ async fn transfer_by_id(&self, id: u64) ->
ApiResult<Option<TransferStatus>> {
+ Ok(db::transfer_by_id(&self.pool, id).await?)
+ }
+
+ async fn outgoing_history(&self, params: History) ->
ApiResult<OutgoingHistory> {
+ Ok(OutgoingHistory {
+ outgoing_transactions: db::outgoing_history(&self.pool,
¶ms).await?,
+ debit_account: self.payto.clone(),
+ })
+ }
+
+ async fn incoming_history(&self, params: History) ->
ApiResult<IncomingHistory> {
+ Ok(IncomingHistory {
+ incoming_transactions: db::incoming_history(&self.pool,
¶ms).await?,
+ credit_account: self.payto.clone(),
+ })
+ }
+
+ async fn add_incoming_reserve(
+ &self,
+ req: AddIncomingRequest,
+ ) -> ApiResult<AddIncomingResponse> {
+ let result = db::register_tx_in_admin(
+ &self.pool,
+ &TxInAdmin {
+ amount: req.amount,
+ subject: format!("Admin incoming {}", req.reserve_pub),
+ debit_payto: req.debit_account,
+ timestamp: Timestamp::now(),
+ metadata: IncomingSubject::Reserve(req.reserve_pub),
+ },
+ )
+ .await?;
+ Ok(AddIncomingResponse {
+ row_id: safe_u64(result.row_id),
+ timestamp: result.timestamp,
+ })
+ }
+
+ async fn add_incoming_kyc(&self, req: AddKycauthRequest) ->
ApiResult<AddKycauthResponse> {
+ let result = db::register_tx_in_admin(
+ &self.pool,
+ &TxInAdmin {
+ amount: req.amount,
+ subject: format!("Admin incoming KYC:{}", req.account_pub),
+ debit_payto: req.debit_account,
+ timestamp: Timestamp::now(),
+ metadata: IncomingSubject::Kyc(req.account_pub),
+ },
+ )
+ .await?;
+ Ok(AddKycauthResponse {
+ row_id: safe_u64(result.row_id),
+ timestamp: result.timestamp,
+ })
+ }
+}
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [taler-rust] branch master updated (9fd9f82 -> 1f583d2),
gnunet <=