taler-rust

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

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:
MCargo.lock | 441++++++++++---------------------------------------------------------------------
MCargo.toml | 4++++
Mcommon/taler-api/Cargo.toml | 9++++-----
Mcommon/taler-api/tests/api.rs | 70++++++++++++++++++++++++++++++++++++++--------------------------------
Mcommon/taler-common/Cargo.toml | 2+-
Mcommon/test-utils/Cargo.toml | 10+++++++++-
Mcommon/test-utils/src/lib.rs | 62+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Amagnet-bank.conf | 9+++++++++
Mwire-gateway/magnet-bank/Cargo.toml | 18++++++++++++++++--
Awire-gateway/magnet-bank/db/schema.sql | 309+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mwire-gateway/magnet-bank/src/config.rs | 14++++++++++++++
Awire-gateway/magnet-bank/src/constant.rs | 17+++++++++++++++++
Awire-gateway/magnet-bank/src/db.rs | 976+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mwire-gateway/magnet-bank/src/magnet/error.rs | 38+++++++++++++++++++++++++++++++++++++-
Mwire-gateway/magnet-bank/src/magnet/oauth.rs | 2+-
Mwire-gateway/magnet-bank/src/main.rs | 28++++++++++++++++++++++------
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, &timestamp) + .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 + }, + &timestamp + ) + .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(()) }