commit 96b8c243dfaba804f39aa58240ee9f272c5c3ae6
parent 9fd9f8217e102c24ca980ec92f689403a64f3c2a
Author: Antoine A <>
Date: Mon, 13 Jan 2025 13:28:51 +0100
magnet-bank: add schema, db logic, dbinit and reduce dependencies
Diffstat:
16 files changed, 1572 insertions(+), 437 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -128,12 +128,6 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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",
@@ -770,15 +761,6 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -839,17 +821,6 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -902,17 +873,6 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1003,25 +963,6 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1174,7 +1115,6 @@ dependencies = [
"bytes",
"futures-channel",
"futures-util",
- "h2",
"http 1.2.0",
"http-body",
"httparse",
@@ -1187,23 +1127,6 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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"
@@ -1522,22 +1442,6 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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",
@@ -1698,56 +1605,18 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
- "libm",
]
[[package]]
@@ -1899,17 +1768,6 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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",
@@ -2188,41 +2042,6 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2267,20 +2086,6 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2296,17 +2101,6 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2554,15 +2348,6 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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]]
@@ -2659,47 +2440,6 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2731,34 +2471,12 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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",
@@ -2819,27 +2537,6 @@ dependencies = [
]
[[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"
dependencies = [
@@ -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",
@@ -3066,16 +2767,6 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3087,19 +2778,6 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3256,12 +2934,6 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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,30 +3082,24 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git 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
@@ -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/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-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/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
@@ -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
@@ -0,0 +1,8 @@
+[magnet-bank]
+API_URL = "https://mobil.magnetbank.hu"
+CONSUMER_KEY = "Consumer"
+CONSUMER_SECRET = "qikgjxc5y06tiil7qgrmh09l7rfi5a8e"
+KEYS_FILE = keys.json
+
+[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
@@ -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
@@ -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
@@ -15,10 +15,24 @@
*/
use reqwest::Url;
+use sqlx::postgres::PgConnectOptions;
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 MagnetConfig {
pub api_url: Url,
pub consumer: Token,
diff --git a/wire-gateway/magnet-bank/src/constant.rs b/wire-gateway/magnet-bank/src/constant.rs
@@ -0,0 +1,17 @@
+/*
+ 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/>
+*/
+
+pub const CURRENCY: &str = "HUF";
diff --git a/wire-gateway/magnet-bank/src/db.rs 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, true).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/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 */
diff --git 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
@@ -17,12 +17,15 @@
use std::{future::Future, path::PathBuf};
use clap::Parser;
-use config::MagnetConfig;
+use config::{DbConfig, MagnetConfig};
+use sqlx::PgPool;
use taler_common::config::{parser::ConfigSource, Config};
use tracing::{error, Level};
use tracing_subscriber::{util::SubscriberInitExt as _, FmtSubscriber};
mod config;
+mod constant;
+mod db;
mod keys;
mod magnet;
@@ -47,15 +50,21 @@ struct Args {
enum Command {
/// Setup Magnet Bank auth token and account settings for Wire Gateway use
Setup,
+ /// Initialize magnet-bank database
+ Dbinit {
+ #[clap(long, short)]
+ reset: bool,
+ },
}
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 +78,23 @@ 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?;
+ }
}
Ok(())
}