diff --git a/.cargo/config.toml b/.cargo/config.toml index 70f9eae..72817e9 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,5 @@ [registries.crates-io] protocol = "sparse" + +[build] +rustflags = ["--cfg", "tokio_unstable"] diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index aaeff3b..fb9eee7 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -36,7 +36,7 @@ jobs: strategy: matrix: rust: [ - 1.70.0, + 1.74.0, nightly ] @@ -58,7 +58,7 @@ jobs: strategy: matrix: rust: [ - 1.70.0, + 1.74.0, nightly ] diff --git a/Cargo.lock b/Cargo.lock index 40ccad4..4f5b80e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,29 +12,34 @@ dependencies = [ "actix-session", "actix-web", "actix-web-actors", + "backoff", "bincode", + "blake3", + "bytes", "bytestring", + "console-subscriber", "env_logger", "handlebars", "hex", - "http", - "httpmock", - "hyper", + "http-body-util", + "hyper 1.2.0", "hyper-rustls", - "indexmap 2.0.2", + "hyper-util", + "indexmap 2.2.5", "log", "md-5", "monero", "qrcode", "rand", "rand_chacha", + "rayon", "serde", "serde_json", "sled", "sqlite", "strum", - "tempfile", "test-case", + "testing-utils", "thiserror", "tokio", ] @@ -44,21 +49,18 @@ name = "acceptxmr-server" version = "0.1.0" dependencies = [ "acceptxmr", - "actix", - "actix-files", - "actix-session", - "actix-web", - "actix-web-actors", - "actix-web-httpauth", - "anyhow", - "bytestring", + "axum 0.7.4", + "bytes", "clap", + "console-subscriber", "dotenv", "env_logger", "futures", - "http", - "hyper", + "futures-util", + "http-body-util", + "hyper 1.2.0", "hyper-rustls", + "hyper-util", "log", "monero", "rand", @@ -72,20 +74,26 @@ dependencies = [ "serde_with", "serde_yaml", "test-case", + "testing-utils", "thiserror", "tokio", + "tokio-rustls", + "tower", + "tower-http", + "utoipa", + "utoipa-swagger-ui", ] [[package]] name = "actix" -version = "0.13.1" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cba56612922b907719d4a01cf11c8d5b458e7d3dba946d0435f20f58d6795ed2" +checksum = "fb72882332b6d6282f428b77ba0358cb2687e61a6f6df6a6d3871e8a177c2d4f" dependencies = [ "actix-macros", "actix-rt", "actix_derive", - "bitflags 2.4.1", + "bitflags 2.4.2", "bytes", "crossbeam-channel", "futures-core", @@ -103,11 +111,11 @@ dependencies = [ [[package]] name = "actix-codec" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617a8268e3537fe1d8c9ead925fca49ef6400927ee7bc26750e90ecee14ce4b8" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.2", "bytes", "futures-core", "futures-sink", @@ -120,16 +128,15 @@ dependencies = [ [[package]] name = "actix-files" -version = "0.6.2" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d832782fac6ca7369a70c9ee9a20554623c5e51c76e190ad151780ebea1cf689" +checksum = "bf0bdd6ff79de7c9a021f5d9ea79ce23e108d8bfc9b49b5b4a2cf6fad5a35212" dependencies = [ "actix-http", "actix-service", "actix-utils", "actix-web", - "askama_escape", - "bitflags 1.3.2", + "bitflags 2.4.2", "bytes", "derive_more", "futures-core", @@ -139,22 +146,22 @@ dependencies = [ "mime_guess", "percent-encoding", "pin-project-lite", + "v_htmlescape", ] [[package]] name = "actix-http" -version = "3.4.0" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92ef85799cba03f76e4f7c10f533e66d87c9a7e7055f3391f09000ad8351bc9" +checksum = "d223b13fd481fc0d1f83bb12659ae774d9e3601814c68a0bc539731698cca743" dependencies = [ "actix-codec", "actix-rt", "actix-service", - "actix-tls", "actix-utils", "ahash", - "base64 0.21.4", - "bitflags 2.4.1", + "base64 0.21.7", + "bitflags 2.4.2", "brotli", "bytes", "bytestring", @@ -162,8 +169,8 @@ dependencies = [ "encoding_rs", "flate2", "futures-core", - "h2", - "http", + "h2 0.3.24", + "http 0.2.12", "httparse", "httpdate", "itoa", @@ -188,17 +195,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.38", + "syn 2.0.52", ] [[package]] name = "actix-router" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" +checksum = "d22475596539443685426b6bdadb926ad0ecaefdfc5fb05e5e3441f15463c511" dependencies = [ "bytestring", - "http", + "http 0.2.12", "regex", "serde", "tracing", @@ -226,7 +233,7 @@ dependencies = [ "futures-core", "futures-util", "mio", - "socket2 0.5.4", + "socket2 0.5.6", "tokio", "tracing", ] @@ -244,42 +251,20 @@ dependencies = [ [[package]] name = "actix-session" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e6a28f813a6671e1847d005cad0be36ae4d016287690f765c303379837c13d6" +checksum = "b671404ec72194d8af58c2bdaf51e3c477a0595056bd5010148405870dda8df2" dependencies = [ "actix-service", "actix-utils", "actix-web", "anyhow", - "async-trait", "derive_more", "serde", "serde_json", "tracing", ] -[[package]] -name = "actix-tls" -version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72616e7fbec0aa99c6f3164677fa48ff5a60036d0799c98cab894a44f3e0efc3" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "impl-more", - "pin-project-lite", - "rustls", - "rustls-webpki 0.101.6", - "tokio", - "tokio-rustls", - "tokio-util", - "tracing", - "webpki-roots 0.25.2", -] - [[package]] name = "actix-utils" version = "3.0.1" @@ -292,9 +277,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.4.0" +version = "4.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4a5b5e29603ca8c94a77c65cf874718ceb60292c5a5c3e5f4ace041af462b9" +checksum = "43a6556ddebb638c2358714d853257ed226ece6023ef9364f23f0c70737ea984" dependencies = [ "actix-codec", "actix-http", @@ -303,7 +288,6 @@ dependencies = [ "actix-rt", "actix-server", "actix-service", - "actix-tls", "actix-utils", "actix-web-codegen", "ahash", @@ -326,16 +310,16 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.5.4", + "socket2 0.5.6", "time", "url", ] [[package]] name = "actix-web-actors" -version = "4.2.0" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf6e9ccc371cfddbed7aa842256a4abc7a6dcac9f3fce392fe1d0f68cfd136b2" +checksum = "420b001bb709d8510c3e2659dae046e54509ff9528018d09c78381e765a1f9fa" dependencies = [ "actix", "actix-codec", @@ -358,22 +342,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.38", -] - -[[package]] -name = "actix-web-httpauth" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d613edf08a42ccc6864c941d30fe14e1b676a77d16f1dbadc1174d065a0a775" -dependencies = [ - "actix-utils", - "actix-web", - "base64 0.21.4", - "futures-core", - "futures-util", - "log", - "pin-project-lite", + "syn 2.0.52", ] [[package]] @@ -384,7 +353,7 @@ checksum = "7c7db3d5a9718568e4cf4a537cfd7070e6e6ff7481510d0237fb529ac850f6d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.52", ] [[package]] @@ -414,9 +383,9 @@ dependencies = [ [[package]] name = "aes" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", @@ -439,14 +408,15 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.3" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", "getrandom", "once_cell", "version_check", + "zerocopy", ] [[package]] @@ -490,9 +460,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.4" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" dependencies = [ "anstyle", "anstyle-parse", @@ -504,43 +474,55 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" [[package]] name = "anstyle-parse" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" + +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "ascii-canvas" @@ -551,12 +533,6 @@ dependencies = [ "term", ] -[[package]] -name = "askama_escape" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" - [[package]] name = "assert-json-diff" version = "2.0.2" @@ -567,6 +543,16 @@ dependencies = [ "serde_json", ] +[[package]] +name = "async-attributes" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" +dependencies = [ + "quote", + "syn 1.0.109", +] + [[package]] name = "async-channel" version = "1.9.0" @@ -578,32 +564,45 @@ dependencies = [ "futures-core", ] +[[package]] +name = "async-channel" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3" +dependencies = [ + "concurrent-queue", + "event-listener 5.2.0", + "event-listener-strategy 0.5.0", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-executor" -version = "1.5.4" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1da3ae8dabd9c00f453a329dfe1fb28da3c0a72e2478cdcd93171740c20499" +checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" dependencies = [ - "async-lock", + "async-lock 3.3.0", "async-task", "concurrent-queue", "fastrand 2.0.1", - "futures-lite", + "futures-lite 2.2.0", "slab", ] [[package]] name = "async-global-executor" -version = "2.3.1" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ - "async-channel", + "async-channel 2.2.0", "async-executor", - "async-io", - "async-lock", + "async-io 2.3.2", + "async-lock 3.3.0", "blocking", - "futures-lite", + "futures-lite 2.2.0", "once_cell", ] @@ -613,20 +612,39 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" dependencies = [ - "async-lock", + "async-lock 2.8.0", "autocfg", "cfg-if", "concurrent-queue", - "futures-lite", + "futures-lite 1.13.0", "log", "parking", - "polling", - "rustix 0.37.25", + "polling 2.8.0", + "rustix 0.37.27", "slab", - "socket2 0.4.9", + "socket2 0.4.10", "waker-fn", ] +[[package]] +name = "async-io" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" +dependencies = [ + "async-lock 3.3.0", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.2.0", + "parking", + "polling 3.5.0", + "rustix 0.38.31", + "slab", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "async-lock" version = "2.8.0" @@ -636,6 +654,17 @@ dependencies = [ "event-listener 2.5.3", ] +[[package]] +name = "async-lock" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" +dependencies = [ + "event-listener 4.0.3", + "event-listener-strategy 0.4.0", + "pin-project-lite", +] + [[package]] name = "async-object-pool" version = "0.1.4" @@ -651,33 +680,33 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" dependencies = [ - "async-io", - "async-lock", + "async-io 1.13.0", + "async-lock 2.8.0", "async-signal", "blocking", "cfg-if", - "event-listener 3.0.0", - "futures-lite", - "rustix 0.38.19", - "windows-sys", + "event-listener 3.1.0", + "futures-lite 1.13.0", + "rustix 0.38.31", + "windows-sys 0.48.0", ] [[package]] name = "async-signal" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2a5415b7abcdc9cd7d63d6badba5288b2ca017e3fbd4173b8f405449f1a2399" +checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5" dependencies = [ - "async-io", - "async-lock", + "async-io 2.3.2", + "async-lock 2.8.0", "atomic-waker", "cfg-if", "futures-core", "futures-io", - "rustix 0.38.19", + "rustix 0.38.31", "signal-hook-registry", "slab", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -686,16 +715,17 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" dependencies = [ - "async-channel", + "async-attributes", + "async-channel 1.9.0", "async-global-executor", - "async-io", - "async-lock", + "async-io 1.13.0", + "async-lock 2.8.0", "async-process", "crossbeam-utils", "futures-channel", "futures-core", "futures-io", - "futures-lite", + "futures-lite 1.13.0", "gloo-timers", "kv-log-macro", "log", @@ -707,21 +737,43 @@ dependencies = [ "wasm-bindgen-futures", ] +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "async-task" -version = "4.4.1" +version = "4.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9441c6b2fe128a7c2bf680a44c34d0df31ce09e5b7e401fcca3faa483dbc921" +checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" [[package]] name = "async-trait" -version = "0.1.74" +version = "0.1.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.52", ] [[package]] @@ -736,6 +788,136 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core 0.3.4", + "bitflags 1.3.2", + "bytes", + "futures-util", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.28", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1236b4b292f6c4d6dc34604bb5120d85c3fe1d1aa596bd5cc52ca054d13e7b9e" +dependencies = [ + "async-trait", + "axum-core 0.4.3", + "axum-macros", + "base64 0.21.7", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.2.0", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 0.2.12", + "http-body 0.4.6", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "backoff" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" +dependencies = [ + "futures-core", + "getrandom", + "instant", + "pin-project-lite", + "rand", + "tokio", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -768,15 +950,15 @@ checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" [[package]] name = "base64" -version = "0.21.4" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "basic-cookies" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb53b6b315f924c7f113b162e53b3901c05fc9966baf84d201dfcc7432a4bb38" +checksum = "67bd8fd42c16bdb08688243dc5f0cc117a3ca9efeeaba3a345a18a6159ad96f7" dependencies = [ "lalrpop", "lalrpop-util", @@ -825,9 +1007,22 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + +[[package]] +name = "blake3" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cca6d3674597c30ddf2c587bf8d9d65c9a84d2326d941cc79c9842dfe0ef52" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] [[package]] name = "block-buffer" @@ -840,16 +1035,16 @@ dependencies = [ [[package]] name = "blocking" -version = "1.4.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c36a4d0d48574b3dd360b4b7d95cc651d2b6557b6402848a27d4b228a473e2a" +checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" dependencies = [ - "async-channel", - "async-lock", + "async-channel 2.2.0", + "async-lock 3.3.0", "async-task", "fastrand 2.0.1", "futures-io", - "futures-lite", + "futures-lite 2.2.0", "piper", "tracing", ] @@ -867,9 +1062,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "2.5.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da74e2b81409b1b743f8f0c62cc6254afefb8b8e50bbfe3735550f7aeefa3448" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -877,15 +1072,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" [[package]] name = "bytemuck" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" +checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" [[package]] name = "byteorder" @@ -901,24 +1096,18 @@ checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "bytestring" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "238e4886760d98c4f899360c834fa93e62cf7f721ac3c2da375cbdf4b8679aae" +checksum = "74d80203ea6b29df88012294f62733de21cfeab47f17b41af3a38bc30a03ee72" dependencies = [ "bytes", ] -[[package]] -name = "castaway" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" - [[package]] name = "cc" -version = "1.0.83" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" dependencies = [ "jobserver", "libc", @@ -930,23 +1119,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "checked_int_cast" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cc5e6b5ab06331c33589842070416baa137e8b0eb912b008cfd4a78ada7919" - [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", "serde", - "windows-targets", + "windows-targets 0.52.4", ] [[package]] @@ -961,30 +1144,30 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.6" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" +checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.4.6" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim", + "strsim 0.11.0", ] [[package]] name = "clap_lex" -version = "0.5.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "color_quant" @@ -1000,93 +1183,138 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "concurrent-queue" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" +checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" dependencies = [ "crossbeam-utils", ] [[package]] -name = "convert_case" -version = "0.4.0" +name = "console-api" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +checksum = "fd326812b3fd01da5bb1af7d340d0d555fd3d4b641e7f1dfcf5962a902952787" +dependencies = [ + "futures-core", + "prost", + "prost-types", + "tonic", + "tracing-core", +] [[package]] -name = "cookie" -version = "0.16.2" +name = "console-subscriber" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +checksum = "7481d4c57092cd1c19dd541b92bdce883de840df30aa5d03fd48a3935c01842e" dependencies = [ - "aes-gcm", - "base64 0.20.0", - "hkdf", - "hmac", - "percent-encoding", - "rand", - "sha2", - "subtle", + "console-api", + "crossbeam-channel", + "crossbeam-utils", + "futures-task", + "hdrhistogram", + "humantime", + "prost-types", + "serde", + "serde_json", + "thread_local", + "tokio", + "tokio-stream", + "tonic", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "aes-gcm", + "base64 0.20.0", + "hkdf", + "hmac", + "percent-encoding", + "rand", + "sha2", + "subtle", "time", "version_check", ] [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.9" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.8" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" dependencies = [ - "cfg-if", "crossbeam-utils", ] [[package]] -name = "crossbeam-epoch" -version = "0.9.15" +name = "crossbeam-deque" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ - "autocfg", - "cfg-if", + "crossbeam-epoch", "crossbeam-utils", - "memoffset", - "scopeguard", ] [[package]] -name = "crossbeam-utils" -version = "0.8.16" +name = "crossbeam-epoch" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "cfg-if", + "crossbeam-utils", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + [[package]] name = "crunchy" version = "0.2.2" @@ -1113,42 +1341,11 @@ dependencies = [ "cipher", ] -[[package]] -name = "curl" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22" -dependencies = [ - "curl-sys", - "libc", - "openssl-probe", - "openssl-sys", - "schannel", - "socket2 0.4.9", - "winapi", -] - -[[package]] -name = "curl-sys" -version = "0.4.68+curl-8.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4a0d18d88360e374b16b2273c832b5e57258ffc1d4aa4f96b108e0738d5752f" -dependencies = [ - "cc", - "libc", - "libnghttp2-sys", - "libz-sys", - "openssl-sys", - "pkg-config", - "vcpkg", - "windows-sys", -] - [[package]] name = "curve25519-dalek" -version = "4.1.1" +version = "4.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" +checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348" dependencies = [ "cfg-if", "cpufeatures", @@ -1163,20 +1360,20 @@ dependencies = [ [[package]] name = "curve25519-dalek-derive" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.52", ] [[package]] name = "darling" -version = "0.20.3" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" dependencies = [ "darling_core", "darling_macro", @@ -1184,34 +1381,40 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.3" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", - "strsim", - "syn 2.0.38", + "strsim 0.10.0", + "syn 2.0.52", ] [[package]] name = "darling_macro" -version = "0.20.3" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", - "syn 2.0.38", + "syn 2.0.52", ] +[[package]] +name = "data-encoding" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" + [[package]] name = "deranged" -version = "0.3.9" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", "serde", @@ -1230,12 +1433,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "diff" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" - [[package]] name = "digest" version = "0.10.7" @@ -1276,9 +1473,9 @@ checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" [[package]] name = "either" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "ena" @@ -1298,17 +1495,27 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_filter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_logger" -version = "0.10.0" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" dependencies = [ + "anstream", + "anstyle", + "env_filter", "humantime", - "is-terminal", "log", - "regex", - "termcolor", ] [[package]] @@ -1319,12 +1526,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.5" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1335,15 +1542,57 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "3.0.0" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29e56284f00d94c1bc7fd3c77027b4623c88c1f53d8d2394c6199f2921dea325" +checksum = "2b5fb89194fa3cad959b833185b3063ba881dbfc7030680b314250779fb4cc91" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] +[[package]] +name = "event-listener-strategy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +dependencies = [ + "event-listener 4.0.3", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291" +dependencies = [ + "event-listener 5.2.0", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -1361,9 +1610,9 @@ checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fiat-crypto" -version = "0.2.1" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0870c84016d4b481be5c9f323c24f65e31e901ae618f0e80f4308fb00de1d2d" +checksum = "1676f435fc1dadde4d03e43f5d62b259e1ce5f40bd4ffb21db2b42ebe59c1382" [[package]] name = "fixed-hash" @@ -1401,9 +1650,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -1420,9 +1669,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -1435,9 +1684,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -1445,15 +1694,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -1462,9 +1711,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-lite" @@ -1481,34 +1730,47 @@ dependencies = [ "waker-fn", ] +[[package]] +name = "futures-lite" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445ba825b27408685aaecefd65178908c36c6e96aaf6d8599419d46e624192ba" +dependencies = [ + "fastrand 2.0.1", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.52", ] [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -1543,9 +1805,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", @@ -1554,9 +1816,9 @@ dependencies = [ [[package]] name = "ghash" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" dependencies = [ "opaque-debug", "polyval", @@ -1564,9 +1826,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "gloo-timers" @@ -1582,17 +1844,36 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", - "http", - "indexmap 1.9.3", + "http 0.2.12", + "indexmap 2.2.5", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 1.1.0", + "indexmap 2.2.5", "slab", "tokio", "tokio-util", @@ -1601,9 +1882,9 @@ dependencies = [ [[package]] name = "handlebars" -version = "4.4.0" +version = "5.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39b3bc2a8f715298032cf5087e58573809374b08160aa7d750582bdb82d2683" +checksum = "ab283476b99e66691dee3f1640fea91487a8d81f50fb5ecc75538f8f8879a1e4" dependencies = [ "log", "pest", @@ -1622,9 +1903,22 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.1" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "hdrhistogram" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" +dependencies = [ + "base64 0.21.7", + "byteorder", + "flate2", + "nom", + "num-traits", +] [[package]] name = "heck" @@ -1634,9 +1928,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.3" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -1652,9 +1946,9 @@ checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" [[package]] name = "hkdf" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ "hmac", ] @@ -1670,9 +1964,20 @@ dependencies = [ [[package]] name = "http" -version = "0.2.9" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", @@ -1681,12 +1986,35 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" dependencies = [ "bytes", - "http", + "futures-core", + "http 1.1.0", + "http-body 1.0.0", "pin-project-lite", ] @@ -1696,6 +2024,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" +[[package]] +name = "http-range-header" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ce4ef31cda248bbdb6e6820603b82dfcd9e833db65a43e997a0ccec777d11fe" + [[package]] name = "httparse" version = "1.8.0" @@ -1710,20 +2044,20 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "httpmock" -version = "0.6.8" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b02e044d3b4c2f94936fb05f9649efa658ca788f44eb6b87554e2033fc8ce93" +checksum = "08ec9586ee0910472dec1a1f0f8acf52f0fdde93aea74d70d4a3107b4be0fd5b" dependencies = [ "assert-json-diff", "async-object-pool", + "async-std", "async-trait", - "base64 0.21.4", + "base64 0.21.7", "basic-cookies", "crossbeam-utils", "form_urlencoded", "futures-util", - "hyper", - "isahc", + "hyper 0.14.28", "lazy_static", "levenshtein", "log", @@ -1744,56 +2078,112 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.27" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", "futures-core", "futures-util", - "h2", - "http", - "http-body", + "h2 0.3.24", + "http 0.2.12", + "http-body 0.4.6", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.9", + "socket2 0.5.6", "tokio", "tower-service", "tracing", "want", ] +[[package]] +name = "hyper" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.2", + "http 1.1.0", + "http-body 1.0.0", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + [[package]] name = "hyper-rustls" -version = "0.24.1" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" +checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" dependencies = [ "futures-util", - "http", - "hyper", + "http 1.1.0", + "hyper 1.2.0", + "hyper-util", "log", "rustls", + "rustls-pki-types", "tokio", "tokio-rustls", - "webpki-roots 0.23.1", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper 0.14.28", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + +[[package]] +name = "hyper-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "hyper 1.2.0", + "pin-project-lite", + "socket2 0.5.6", + "tokio", + "tower", + "tower-service", + "tracing", ] [[package]] name = "iana-time-zone" -version = "0.1.57" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows", + "windows-core", ] [[package]] @@ -1813,9 +2203,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -1823,24 +2213,16 @@ dependencies = [ [[package]] name = "image" -version = "0.23.14" +version = "0.24.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" dependencies = [ "bytemuck", "byteorder", "color_quant", - "num-iter", - "num-rational", "num-traits", ] -[[package]] -name = "impl-more" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206ca75c9c03ba3d4ace2460e57b189f39f43de612c2f85836e65c929701bb2d" - [[package]] name = "indexmap" version = "1.9.3" @@ -1854,12 +2236,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.2" +version = "2.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" dependencies = [ "equivalent", - "hashbrown 0.14.1", + "hashbrown 0.14.3", "serde", ] @@ -1889,76 +2271,38 @@ checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi", "libc", - "windows-sys", -] - -[[package]] -name = "is-terminal" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" -dependencies = [ - "hermit-abi", - "rustix 0.38.19", - "windows-sys", -] - -[[package]] -name = "isahc" -version = "1.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9" -dependencies = [ - "async-channel", - "castaway", - "crossbeam-utils", - "curl", - "curl-sys", - "encoding_rs", - "event-listener 2.5.3", - "futures-lite", - "http", - "log", - "mime", - "once_cell", - "polling", - "slab", - "sluice", - "tracing", - "tracing-futures", - "url", - "waker-fn", + "windows-sys 0.48.0", ] [[package]] name = "itertools" -version = "0.10.5" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "jobserver" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -1974,33 +2318,33 @@ dependencies = [ [[package]] name = "lalrpop" -version = "0.19.12" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a1cbf952127589f2851ab2046af368fd20645491bb4b376f04b7f94d7a9837b" +checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" dependencies = [ "ascii-canvas", "bit-set", - "diff", "ena", - "is-terminal", "itertools", "lalrpop-util", "petgraph", + "pico-args", "regex", - "regex-syntax 0.6.29", + "regex-syntax 0.8.2", "string_cache", "term", "tiny-keccak", "unicode-xid", + "walkdir", ] [[package]] name = "lalrpop-util" -version = "0.19.12" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3c48237b9604c5a4702de6b824e02006c3214327564636aef27c1028a8fa0ed" +checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" dependencies = [ - "regex", + "regex-automata 0.4.6", ] [[package]] @@ -2023,30 +2367,19 @@ checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" [[package]] name = "libc" -version = "0.2.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" - -[[package]] -name = "libnghttp2-sys" -version = "0.1.8+1.55.1" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fae956c192dadcdb5dace96db71fa0b827333cce7c7b38dc71446f024d8a340" -dependencies = [ - "cc", - "libc", -] +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] -name = "libz-sys" -version = "1.1.12" +name = "libredox" +version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" dependencies = [ - "cc", + "bitflags 2.4.2", "libc", - "pkg-config", - "vcpkg", + "redox_syscall 0.4.1", ] [[package]] @@ -2057,15 +2390,15 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.4.10" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "local-channel" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a493488de5f18c8ffcba89eebb8532ffc562dc400490eb65b84893fae0b178" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" dependencies = [ "futures-core", "futures-sink", @@ -2074,15 +2407,15 @@ dependencies = [ [[package]] name = "local-waker" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" [[package]] name = "lock_api" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg", "scopeguard", @@ -2090,14 +2423,29 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" dependencies = [ "serde", "value-bag", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "md-5" version = "0.10.6" @@ -2110,18 +2458,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" - -[[package]] -name = "memoffset" -version = "0.9.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" -dependencies = [ - "autocfg", -] +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "mime" @@ -2139,32 +2478,38 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.8" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "monero" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf671c5fd0fa1f1dcc2495b6ca97a83f0aa6b2a737957e773d34c6c6bac0659" +checksum = "1b205707fd34b01a547f2fe77e687b40fed05966fb82e955b86ac55cd8ee31b5" dependencies = [ "base58-monero", "curve25519-dalek", @@ -2185,42 +2530,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" [[package]] -name = "num-integer" -version = "0.1.45" +name = "nom" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ - "autocfg", - "num-traits", + "memchr", + "minimal-lexical", ] [[package]] -name = "num-iter" -version = "0.1.43" +name = "nu-ansi-term" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ - "autocfg", - "num-integer", - "num-traits", + "overload", + "winapi", ] [[package]] -name = "num-rational" -version = "0.3.2" +name = "num-conv" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", ] @@ -2237,48 +2576,36 @@ dependencies = [ [[package]] name = "object" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" - -[[package]] -name = "openssl-probe" -version = "0.1.5" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] -name = "openssl-sys" -version = "0.9.93" +name = "overload" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parking" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e52c774a4c39359c1d1c52e43f73dd91a75a614652c825408eec30c95a9b2067" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" @@ -2298,7 +2625,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.8", + "parking_lot_core 0.9.9", ] [[package]] @@ -2317,15 +2644,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.3.5", + "redox_syscall 0.4.1", "smallvec", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -2336,25 +2663,25 @@ checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "pem" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3163d2912b7c3b52d651a055f2c7eec9ba5cd22d26ef75b8dd3a59980b185923" +checksum = "1b8fcc794035347fb64beda2d3b462595dd2753e3f268d89c5aae77e8cf2c310" dependencies = [ - "base64 0.21.4", + "base64 0.21.7", "serde", ] [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.4" +version = "2.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c022f1e7b65d6a24c0dbbd5fb344c66881bc01f3e5ae74a1c8100f2f985d98a4" +checksum = "56f8023d0fb78c8e03784ea1c7f3fa36e68a723138990b8d5a47d916b651e7a8" dependencies = [ "memchr", "thiserror", @@ -2363,9 +2690,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.4" +version = "2.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35513f630d46400a977c4cb58f78e1bfbe01434316e60c37d27b9ad6139c66d8" +checksum = "b0d24f72393fd16ab6ac5738bc33cdb6a9aa73f8b902e8fe29cf4e67d7dd1026" dependencies = [ "pest", "pest_generator", @@ -2373,22 +2700,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.4" +version = "2.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc9fc1b9e7057baba189b5c626e2d6f40681ae5b6eb064dc7c7834101ec8123a" +checksum = "fdc17e2a6c7d0a492f0158d7a4bd66cc17280308bbaff78d5bef566dca35ab80" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.52", ] [[package]] name = "pest_meta" -version = "2.7.4" +version = "2.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df74e9e7ec4053ceb980e7c0c8bd3594e977fde1af91daba9c928e8e8c6708d" +checksum = "934cd7631c050f4674352a6e835d5f6711ffbfb9345c2fc0107155ac495ae293" dependencies = [ "once_cell", "pest", @@ -2402,7 +2729,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 2.0.2", + "indexmap 2.2.5", ] [[package]] @@ -2414,24 +2741,30 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + [[package]] name = "pin-project" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.52", ] [[package]] @@ -2459,15 +2792,15 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "platforms" -version = "3.1.2" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4503fa043bf02cee09a9582e9554b4c6403b2ef55e4612e96561d294419429f8" +checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c" [[package]] name = "polling" @@ -2482,14 +2815,28 @@ dependencies = [ "libc", "log", "pin-project-lite", - "windows-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24f040dee2588b4963afb4e420540439d126f73fdacf4a9c486a96d840bac3c9" +dependencies = [ + "cfg-if", + "concurrent-queue", + "pin-project-lite", + "rustix 0.38.31", + "tracing", + "windows-sys 0.52.0", ] [[package]] name = "polyval" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", "cpufeatures", @@ -2541,28 +2888,59 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "prost-types" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" +dependencies = [ + "prost", +] + [[package]] name = "qrcode" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16d2f1455f3630c6e5107b4f2b94e74d76dea80736de0981fd27644216cff57f" +checksum = "166f136dfdb199f98186f3649cf7a0536534a61417a1a30221b492b4fb60ce3f" dependencies = [ - "checked_int_cast", "image", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -2597,11 +2975,31 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rayon" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "rcgen" -version = "0.11.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52c4f3084aa3bc7dfbba4eff4fab2a54db4324965d8872ab933565e6fbd83bc6" +checksum = "48406db8ac1f3cbc7dcdb56ec355343817958a356ff430259bb07baf7607e1e1" dependencies = [ "pem", "ring", @@ -2620,41 +3018,50 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.3.5" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "redox_users" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" dependencies = [ "getrandom", - "redox_syscall 0.2.16", + "libredox", "thiserror", ] [[package]] name = "regex" -version = "1.10.1" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaac441002f822bc9705a681810a4dd2963094b9ca0ddc41cb963a4c189189ea" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", - "regex-automata", + "regex-automata 0.4.6", "regex-syntax 0.8.2", ] [[package]] name = "regex-automata" -version = "0.4.2" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5011c7e263a695dc8ca064cddb722af1be54e517a280b12a5356f98366899e5d" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", @@ -2675,17 +3082,51 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "ring" -version = "0.16.20" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", + "cfg-if", + "getrandom", "libc", - "once_cell", "spin", "untrusted", - "web-sys", - "winapi", + "windows-sys 0.52.0", +] + +[[package]] +name = "rust-embed" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb78f46d0066053d16d4ca7b898e9343bc3530f71c61d5ad84cd404ada068745" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91ac2a3c6c0520a3fb3dd89321177c3c692937c4eb21893378219da10c44fc8" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.52", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f69089032567ffff4eada41c573fc43ff466c7db7c5688b2e7969584345581" +dependencies = [ + "sha2", + "walkdir", ] [[package]] @@ -2711,69 +3152,69 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.25" +version = "0.37.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4eb579851244c2c03e7c24f501c3432bed80b8f720af1d6e5b0e0f01555a035" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" dependencies = [ "bitflags 1.3.2", "errno", "io-lifetimes", "libc", "linux-raw-sys 0.3.8", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "rustix" -version = "0.38.19" +version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "745ecfa778e66b2b63c88a61cb36e0eea109e803b0b86bf9879fbc77c70e86ed" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "errno", "libc", - "linux-raw-sys 0.4.10", - "windows-sys", + "linux-raw-sys 0.4.13", + "windows-sys 0.52.0", ] [[package]] name = "rustls" -version = "0.21.7" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41" dependencies = [ "log", "ring", - "rustls-webpki 0.101.6", - "sct", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", ] [[package]] name = "rustls-pemfile" -version = "1.0.3" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +checksum = "f48172685e6ff52a556baa527774f61fcaa884f59daf3375c62a3f1cd2549dab" dependencies = [ - "base64 0.21.4", + "base64 0.21.7", + "rustls-pki-types", ] [[package]] -name = "rustls-webpki" -version = "0.100.3" +name = "rustls-pki-types" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6a5fc258f1c1276dfe3016516945546e2d5383911efc0fc4f1cdc5df3a4ae3" -dependencies = [ - "ring", - "untrusted", -] +checksum = "5ede67b28608b4c60685c7d54122d4400d90f62b40caee7700e700380a390fa8" [[package]] name = "rustls-webpki" -version = "0.101.6" +version = "0.102.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" dependencies = [ "ring", + "rustls-pki-types", "untrusted", ] @@ -2785,9 +3226,9 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "same-file" @@ -2798,31 +3239,12 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "schannel" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" -dependencies = [ - "windows-sys", -] - [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "sealed" version = "0.5.0" @@ -2832,7 +3254,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.52", ] [[package]] @@ -2847,15 +3269,15 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" [[package]] name = "serde" -version = "1.0.189" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] @@ -2871,26 +3293,36 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.189" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.52", ] [[package]] name = "serde_json" -version = "1.0.107" +version = "1.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" dependencies = [ "itoa", "ryu", "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_regex" version = "1.1.0" @@ -2915,16 +3347,17 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.3.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca3b16a3d82c4088f343b7480a93550b3eabe1a358569c2dfe38bbcead07237" +checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a" dependencies = [ - "base64 0.21.4", + "base64 0.21.7", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.0.2", + "indexmap 2.2.5", "serde", + "serde_derive", "serde_json", "serde_with_macros", "time", @@ -2932,23 +3365,23 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.3.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e6be15c453eb305019bfa438b1593c731f36a289a7853f7707ee29e870b3b3c" +checksum = "6561dc161a9224638a31d876ccdfefbc1df91d3f3a8342eddb35f055d48c7655" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.52", ] [[package]] name = "serde_yaml" -version = "0.9.25" +version = "0.9.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" +checksum = "8fd075d994154d4a774f95b51fb96bdc2832b0ea48425c92546073816cda1f2f" dependencies = [ - "indexmap 2.0.2", + "indexmap 2.2.5", "itoa", "ryu", "serde", @@ -2977,6 +3410,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -2988,9 +3430,9 @@ dependencies = [ [[package]] name = "similar" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aeaf503862c419d66959f5d7ca015337d864e9c49485d771b732e2a20453597" +checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" [[package]] name = "siphasher" @@ -3023,28 +3465,17 @@ dependencies = [ "parking_lot 0.11.2", ] -[[package]] -name = "sluice" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5" -dependencies = [ - "async-channel", - "futures-core", - "futures-io", -] - [[package]] name = "smallvec" -version = "1.11.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" [[package]] name = "socket2" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", @@ -3052,25 +3483,25 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.4" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] name = "spin" -version = "0.5.2" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "sqlite" -version = "0.31.1" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05439db7afa0ce0b38f6d1b4c691f368adde108df021e15e900fec6a1af92488" +checksum = "78a97f6ee5b054b7cfddc3b6613fdea4c77af2ef8f8aac3048c7d0be31e605e3" dependencies = [ "libc", "sqlite3-sys", @@ -3078,9 +3509,9 @@ dependencies = [ [[package]] name = "sqlite3-src" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfc95a51a1ee38839599371685b9d4a926abb51791f0bc3bf8c3bb7867e6e454" +checksum = "9448d45899495acd1a46b0b888ae9747c13551fb0d8f4a60ab485da52b040503" dependencies = [ "cc", "pkg-config", @@ -3088,9 +3519,9 @@ dependencies = [ [[package]] name = "sqlite3-sys" -version = "0.15.2" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2752c669433e40ebb08fde824146f50d9628aa0b66a3b7fc6be34db82a8063b" +checksum = "aed0b61828382e1103930d2d5df1972d493173319730b481192e63d929097321" dependencies = [ "libc", "sqlite3-src", @@ -3121,26 +3552,32 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strsim" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" + [[package]] name = "strum" -version = "0.25.0" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.25.3" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" dependencies = [ "heck", "proc-macro2", "quote", "rustversion", - "syn 2.0.38", + "syn 2.0.52", ] [[package]] @@ -3162,26 +3599,31 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.38" +version = "2.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "tempfile" -version = "3.8.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand 2.0.1", - "redox_syscall 0.3.5", - "rustix 0.38.19", - "windows-sys", + "rustix 0.38.31", + "windows-sys 0.52.0", ] [[package]] @@ -3195,78 +3637,92 @@ dependencies = [ "winapi", ] -[[package]] -name = "termcolor" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" -dependencies = [ - "winapi-util", -] - [[package]] name = "test-case" -version = "3.2.1" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8f1e820b7f1d95a0cdbf97a5df9de10e1be731983ab943e56703ac1b8e9d425" +checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8" dependencies = [ "test-case-macros", ] [[package]] name = "test-case-core" -version = "3.2.1" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54c25e2cb8f5fcd7318157634e8838aa6f7e4715c96637f969fabaccd1ef5462" +checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" dependencies = [ "cfg-if", - "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.52", ] [[package]] name = "test-case-macros" -version = "3.2.1" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37cfd7bbc88a0104e304229fba519bdc45501a30b760fb72240342f1289ad257" +checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ - "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.52", "test-case-core", ] +[[package]] +name = "testing-utils" +version = "0.1.0" +dependencies = [ + "acceptxmr", + "console-subscriber", + "env_logger", + "httpmock", + "log", + "serde_json", + "tempfile", + "tracing-subscriber", +] + [[package]] name = "thiserror" -version = "1.0.49" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.49" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.52", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", ] [[package]] name = "time" -version = "0.3.30" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" dependencies = [ "deranged", "itoa", + "num-conv", "powerfmt", "serde", "time-core", @@ -3281,10 +3737,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" dependencies = [ + "num-conv", "time-core", ] @@ -3314,9 +3771,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.33.0" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", @@ -3326,37 +3783,72 @@ dependencies = [ "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.4", + "socket2 0.5.6", "tokio-macros", - "windows-sys", + "tracing", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", ] [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.52", ] [[package]] name = "tokio-rustls" -version = "0.24.1" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" dependencies = [ "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", "tokio", + "tungstenite", ] [[package]] name = "tokio-util" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", @@ -3366,6 +3858,85 @@ dependencies = [ "tracing", ] +[[package]] +name = "tonic" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" +dependencies = [ + "async-stream", + "async-trait", + "axum 0.6.20", + "base64 0.21.7", + "bytes", + "h2 0.3.24", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.28", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "base64 0.21.7", + "bitflags 2.4.2", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + [[package]] name = "tower-service" version = "0.3.2" @@ -3374,9 +3945,9 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.39" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2ef2af84856a50c1d430afce2fdded0a4ec7eda868db86409b4543df0797f9" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "log", "pin-project-lite", @@ -3392,7 +3963,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.52", ] [[package]] @@ -3402,23 +3973,62 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", ] [[package]] -name = "tracing-futures" -version = "0.2.5" +name = "tracing-log" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ - "pin-project", + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", "tracing", + "tracing-core", + "tracing-log", ] [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.1.0", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "url", + "utf-8", +] [[package]] name = "typenum" @@ -3443,9 +4053,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" @@ -3455,9 +4065,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] @@ -3480,27 +4090,33 @@ dependencies = [ [[package]] name = "unsafe-libyaml" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" +checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b" [[package]] name = "untrusted" -version = "0.7.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8parse" version = "0.2.1" @@ -3508,16 +4124,62 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] -name = "value-bag" -version = "1.4.1" +name = "utoipa" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "272ebdfbc99111033031d2f10e018836056e4d2c8e2acda76450ec7974269fa7" +dependencies = [ + "indexmap 2.2.5", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3c9f4d08338c1bfa70dde39412a040a884c6f318b3d09aaaf3437a1e52027fc" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "utoipa-swagger-ui" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b39868d43c011961e04b41623e050aedf2cc93652562ff7935ce0f819aaf2da" +dependencies = [ + "axum 0.7.4", + "mime_guess", + "regex", + "rust-embed", + "serde", + "serde_json", + "utoipa", + "zip", +] + +[[package]] +name = "v_htmlescape" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3" +checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" [[package]] -name = "vcpkg" -version = "0.2.15" +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "value-bag" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +checksum = "8fec26a25bd6fca441cdd0f769fd7f891bae119f996de31f86a5eddccef54c1d" [[package]] name = "version_check" @@ -3539,9 +4201,9 @@ checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" [[package]] name = "walkdir" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", @@ -3564,9 +4226,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3574,24 +4236,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.52", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.37" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -3601,9 +4263,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3611,28 +4273,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.52", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-sys" -version = "0.3.64" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", @@ -3640,19 +4302,13 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.23.1" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338" +checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" dependencies = [ - "rustls-webpki 0.100.3", + "rustls-pki-types", ] -[[package]] -name = "webpki-roots" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" - [[package]] name = "winapi" version = "0.3.9" @@ -3685,12 +4341,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows" -version = "0.48.0" +name = "windows-core" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets", + "windows-targets 0.52.4", ] [[package]] @@ -3699,7 +4355,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.4", ] [[package]] @@ -3708,13 +4373,28 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", ] [[package]] @@ -3723,42 +4403,84 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + [[package]] name = "yasna" version = "0.5.2" @@ -3768,28 +4490,59 @@ dependencies = [ "time", ] +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "zeroize" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", + "flate2", +] [[package]] name = "zstd" -version = "0.12.4" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" +checksum = "bffb3309596d527cfcba7dfc6ed6052f1d39dfbd7c867aa2e865e4a449c10110" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "6.0.6" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581" +checksum = "43747c7422e2924c11144d5229878b98180ef8b06cca4ab5af37afc8a8d8ea3e" dependencies = [ - "libc", "zstd-sys", ] diff --git a/Cargo.toml b/Cargo.toml index edecbc2..2711a50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,8 +3,77 @@ resolver = "2" members = [ "server", - "library" + "library", + "testing-utils" ] +[workspace.lints.rust] +unsafe_code = "forbid" +missing_docs = "warn" +unreachable_pub = "warn" + +[workspace.lints.clippy] +pedantic = "warn" +cargo = "warn" +module_name_repetitions = "allow" +multiple_crate_versions = "allow" + +[workspace.dependencies] +acceptxmr = { path = "library" } +actix = "0.13" +actix-files = "0.6" +actix-session = "0.9" +actix-web = "4" +actix-web-actors = "4" +axum = { version = "0.7", default-features = false } +backoff = "0.4" +bincode = "^2.0.0-rc.3" +blake3 = "1" +bytes = "1" +bytestring = "1" +clap = "4" +console-subscriber = "0.2" +dotenv = "0.15" +env_logger = "0.11" +futures = "0.3" +futures-util = "0.3" +handlebars = "5" +hex = "0.4" +http-body-util = "0.1" +httpmock = "0.7" +hyper = "1" +hyper-rustls = { version = "0.26", default-features = false } +hyper-util = "0.1" +indexmap = "2" +log = "0.4" +md-5 = "0.10" +monero = "0.20" +qrcode = "0.13" +rand = "0.8" +rand_chacha = "0.3" +rayon = "1" +rcgen = "0.12" +rustls = "0.22" +rustls-pemfile = "2" +secrecy = "0.8" +serde = { version = "1.0", default-features = false } +serde_json = "1" +serde_with = "3" +serde_yaml = "0.9" +sled = "0.34" +sqlite = "0.33" +strum = "0.26" +tempfile = "3" +test-case = "3" +testing-utils = { path = "testing-utils" } +thiserror = "1" +tokio = "1" +tokio-rustls = "0.25" +tower = "0.4" +tower-http = "0.5" +tracing-subscriber = "0.3" +utoipa = "4" +utoipa-swagger-ui = "6" + [profile.release] lto = true diff --git a/acceptxmr.yaml b/acceptxmr.yaml index 32e9868..8ae90b6 100644 --- a/acceptxmr.yaml +++ b/acceptxmr.yaml @@ -45,6 +45,16 @@ internal-api: # Specify where static HTML/CSS/JS files can be found. static_dir: server/static/ +# When a connection fails to be made to a callback recipient or the recipient +# returns an error, the callback will be placed back into the queue and retried. +# If the queue fills up, the payment gateway will stop processing invoices until +# the queue begins to clear. +# +# Note that the queue is held in memory. When the server is stopped, the queue +# is lost and the queued callbacks will not be delivered. +callback: + queue-size: 1000 + # Remember to change the address below to your own. You will also need to set # your private viewkey using the PRIVATE_VIEWKEY environment variable. # @@ -62,6 +72,10 @@ wallet: daemon: url: http://xmr-node.cakewallet.com:18081/ login: null + # Timeout in seconds for RPC calls to the daemon. Defaults to 30s. + rpc-timeout: 30 + # Timeout in seconds for making an RPC connection to the daemon. Defaults to 20s. + connection-timeout: 20 database: path: AcceptXMR_DB/ diff --git a/library/Cargo.toml b/library/Cargo.toml index d50f042..e1e07ca 100644 --- a/library/Cargo.toml +++ b/library/Cargo.toml @@ -2,7 +2,7 @@ name = "acceptxmr" version = "0.13.0" edition = "2021" -rust-version = "1.70" +rust-version = "1.74" license = "MIT OR Apache-2.0" description = "Accept monero in your application." repository = "https://github.com/busyboredom/acceptxmr" @@ -10,6 +10,9 @@ readme = "README.md" keywords = ["crypto", "gateway", "monero", "payment", "xmr"] categories = ["cryptography::cryptocurrencies"] +[lints] +workspace = true + [lib] name = "acceptxmr" path = "src/lib.rs" @@ -19,24 +22,29 @@ all-features = true rustc-args = ["--cfg", "docsrs"] [dependencies] -bincode = { version = "^2.0.0-rc.3", optional = true } -hex = "0.4" -http = "0.2" -hyper = { version = "0.14", features = ["client", "http1", "http2", "tcp"] } -hyper-rustls = { version = "0.24", features = ["logging", "http1", "http2", "tls12", "webpki-tokio"], default-features = false } -indexmap = "2" -log = "0.4" -md-5 = "0.10" -monero = "0.19" -rand = "0.8" -rand_chacha = "0.3" -serde = { version = "1.0", default-features = false, features = ["derive", "alloc"], optional = true } -serde_json = "1" -sled = { version = "0.34", optional = true } -sqlite = { version = "0.31", optional = true } -strum = { version = "0.25", features = ["derive"] } -thiserror = "1" -tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] } +backoff = { workspace = true, features = ["tokio"] } +bincode = { workspace = true, optional = true } +blake3 = { workspace = true, features = ["std"] } +bytes.workspace = true +hex.workspace = true +http-body-util.workspace = true +hyper = { workspace = true, features = ["client", "http1", "http2"] } +hyper-rustls = { workspace = true, features = ["logging", "http1", "http2", "tls12", "webpki-tokio", "webpki-roots", "ring"] } +hyper-util = { workspace = true, features = ["client-legacy", "http1", "http2"] } +indexmap.workspace = true +log.workspace = true +md-5.workspace = true +monero.workspace = true +rand.workspace = true +rand_chacha.workspace = true +rayon.workspace = true +serde = { workspace = true, features = ["derive", "alloc"], optional = true } +serde_json.workspace = true +sled = { workspace = true, optional = true } +sqlite = { workspace = true, optional = true } +strum = { workspace = true, features = ["derive"] } +thiserror.workspace = true +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time", "tracing"] } [features] bincode = ["dep:bincode"] @@ -46,21 +54,21 @@ sled = ["bincode", "dep:sled"] sqlite = ["bincode", "dep:sqlite"] [dev-dependencies] -actix = "0.13" -actix-files = "0.6" -actix-session = { version = "0.8", features = ["cookie-session"] } -actix-web = "4" -actix-web-actors = "4" -bytestring = "1" -env_logger = "0.10" -handlebars = { version = "4", features = ["dir_source"] } -httpmock = "0.6" -qrcode = "0.12" -serde = "1" -tempfile = "3" -test-case = "3" +actix.workspace = true +actix-files.workspace = true +actix-session = { workspace = true, features = ["cookie-session"] } +actix-web.workspace = true +actix-web-actors.workspace = true +bytestring.workspace = true +console-subscriber.workspace = true +env_logger.workspace = true +handlebars = { workspace = true, features = ["dir_source"] } +qrcode.workspace = true +serde.workspace = true +test-case.workspace = true +testing-utils.workspace = true # This is a workaround to enable features in tests. -acceptxmr = { path = ".", features = ["sled", "in-memory", "sqlite"] } +acceptxmr = { workspace = true, features = ["sled", "in-memory", "sqlite"] } [[example]] name = "custom_storage" diff --git a/library/README.md b/library/README.md index 1530df2..82e34f7 100644 --- a/library/README.md +++ b/library/README.md @@ -1,7 +1,7 @@ [![BuildStatus](https://github.com/busyboredom/acceptxmr/workflows/CI/badge.svg)](https://img.shields.io/github/actions/workflow/status/busyboredom/acceptxmr/ci.yml?branch=main) [![Crates.io](https://img.shields.io/crates/v/acceptxmr.svg)](https://crates.io/crates/acceptxmr) [![Documentation](https://docs.rs/acceptxmr/badge.svg)](https://docs.rs/acceptxmr) -[![MSRV](https://img.shields.io/badge/MSRV-1.70.0-blue)](https://blog.rust-lang.org/2023/06/01/Rust-1.70.0.html) +[![MSRV](https://img.shields.io/badge/MSRV-1.74.0-blue)](https://blog.rust-lang.org/2023/11/16/Rust-1.74.0.html) # `AcceptXMR`: Accept Monero in Your Application `AcceptXMR` is a library for building payment gateways. diff --git a/library/examples/custom_storage/main.rs b/library/examples/custom_storage/main.rs index 6657b30..2f26711 100644 --- a/library/examples/custom_storage/main.rs +++ b/library/examples/custom_storage/main.rs @@ -1,9 +1,6 @@ -#![warn(clippy::pedantic)] +//! Use a custom storage layer. -use std::collections::{ - btree_map::{self, Entry}, - BTreeMap, -}; +use std::collections::{btree_map::Entry, BTreeMap}; use acceptxmr::{ storage::{HeightStorage, InvoiceStorage, OutputId, OutputKeyStorage, OutputPubKey, Storage}, @@ -33,6 +30,7 @@ async fn main() { ) .daemon_url("http://node.sethforprivacy.com:18089".to_string()) .build() + .await .unwrap(); info!("Payment gateway created."); @@ -41,9 +39,11 @@ async fn main() { // custom storage layer. let invoice_id = payment_gateway .new_invoice(1000, 2, 5, "Demo invoice".to_string()) + .await .unwrap(); let invoice = payment_gateway .get_invoice(invoice_id) + .await .unwrap() .expect("invoice not found"); @@ -53,9 +53,9 @@ async fn main() { ); } -// This example uses a BTreeMap for simplicity, but you can implement this trait -// on virtually any storage layer you choose. Postgres or MySQL, CSV files, -// whatever works best for your application. +/// This example storage layer uses a `BTreeMap` for simplicity, but you can +/// implement this trait on virtually any storage layer you choose. `Postgres` +/// or `MySQL`, CSV files, whatever works best for your application. pub struct MyCustomStorage { invoices: BTreeMap, output_keys: BTreeMap, @@ -82,7 +82,6 @@ impl Default for MyCustomStorage { impl InvoiceStorage for MyCustomStorage { type Error = MyCustomStorageError; - type Iter<'a> = MyCustomStorageIter<'a>; fn insert(&mut self, invoice: Invoice) -> Result<(), Self::Error> { if self.invoices.contains_key(&invoice.id()) { @@ -115,19 +114,17 @@ impl InvoiceStorage for MyCustomStorage { .is_some()) } - fn try_iter(&self) -> Result, Self::Error> { - let iter = self.invoices.values(); - Ok(MyCustomStorageIter(iter)) + fn try_for_each(&self, mut f: F) -> Result<(), Self::Error> + where + F: FnMut(Result) -> Result<(), Self::Error>, + { + self.invoices + .iter() + .try_for_each(|(_, invoice)| f(Ok(invoice.clone()))) } -} - -pub struct MyCustomStorageIter<'a>(btree_map::Values<'a, InvoiceId, Invoice>); - -impl<'a> Iterator for MyCustomStorageIter<'a> { - type Item = Result; - fn next(&mut self) -> Option { - self.0.next().map(|v| Ok(v.clone())) + fn is_empty(&self) -> Result { + Ok(self.invoices.is_empty()) } } diff --git a/library/examples/nojs/main.rs b/library/examples/nojs/main.rs index 8c0a3d7..59c9fab 100644 --- a/library/examples/nojs/main.rs +++ b/library/examples/nojs/main.rs @@ -1,4 +1,4 @@ -#![warn(clippy::pedantic)] +//! Serve a no-js frontend. use acceptxmr::{storage::stores::InMemory, InvoiceId, PaymentGateway, PaymentGatewayBuilder}; use actix_files::Files; @@ -15,7 +15,7 @@ use actix_web::{ web::{Data, Form}, App, HttpResponse, HttpServer, Result, }; -use handlebars::{no_escape, Handlebars}; +use handlebars::{no_escape, DirectorySourceOptions, Handlebars}; use log::{debug, error, info, LevelFilter}; use qrcode::{render::svg, EcLevel, QrCode}; use rand::{thread_rng, Rng}; @@ -46,6 +46,7 @@ async fn main() -> std::io::Result<()> { ) .daemon_url("http://xmr-node.cakewallet.com:18081".to_string()) .build() + .await .expect("failed to build payment gateway"); info!("Payment gateway created."); @@ -57,7 +58,7 @@ async fn main() -> std::io::Result<()> { // Watch for invoice updates and deal with them accordingly. let gateway_copy = payment_gateway.clone(); - std::thread::spawn(move || { + tokio::spawn(async move { // Watch all invoice updates. let mut subscriber = gateway_copy.subscribe_all(); loop { @@ -74,7 +75,7 @@ async fn main() -> std::io::Result<()> { "Invoice to index {} has been tracked for > 30 blocks. Removing invoice now", invoice.index() ); - if let Err(e) = gateway_copy.remove_invoice(invoice.id()) { + if let Err(e) = gateway_copy.remove_invoice(invoice.id()).await { error!("Failed to remove invoice: {}", e); }; } @@ -89,7 +90,14 @@ async fn main() -> std::io::Result<()> { // Templating setup. let mut handlebars = Handlebars::new(); handlebars - .register_templates_directory(".html", "./library/examples/nojs/static/templates") + .register_templates_directory( + "./library/examples/nojs/static/templates", + DirectorySourceOptions { + tpl_extension: ".html".to_string(), + hidden: false, + temporary: false, + }, + ) .expect("failed to register template directory"); handlebars.register_escape_fn(no_escape); @@ -131,6 +139,7 @@ async fn start_checkout( ) -> Result { let invoice_id = payment_gateway .new_invoice(1_000_000_000, 2, 5, checkout_info.message.clone()) + .await .unwrap(); session.insert("id", invoice_id)?; Ok(HttpResponse::TemporaryRedirect() @@ -149,7 +158,7 @@ async fn checkout( templater: Data>, ) -> Result { if let Ok(Some(invoice_id)) = session.get::("id") { - if let Ok(Some(invoice)) = payment_gateway.get_invoice(invoice_id) { + if let Ok(Some(invoice)) = payment_gateway.get_invoice(invoice_id).await { let mut instruction = "Send Monero to Address Below"; let mut address = invoice.address(); let mut qrcode = qrcode(&invoice.uri()); diff --git a/library/examples/persistence/main.rs b/library/examples/persistence/main.rs index d61d73b..4365fab 100644 --- a/library/examples/persistence/main.rs +++ b/library/examples/persistence/main.rs @@ -1,4 +1,4 @@ -#![warn(clippy::pedantic)] +//! Use a persistent invoice store to enable recovery from power loss. use acceptxmr::{storage::stores::Sqlite, PaymentGateway, PaymentGatewayBuilder}; use log::{error, info, LevelFilter}; @@ -33,6 +33,7 @@ async fn main() { ) .daemon_url("http://node.sethforprivacy.com:18089".to_string()) .build() + .await .unwrap(); info!("Payment gateway created."); @@ -41,9 +42,11 @@ async fn main() { // persistently in your Sqlite database. let invoice_id = payment_gateway .new_invoice(1000, 2, 5, "Demo invoice".to_string()) + .await .unwrap(); let invoice = payment_gateway .get_invoice(invoice_id) + .await .unwrap() .expect("invoice not found"); @@ -70,11 +73,13 @@ async fn main() { ) .daemon_url("http://node.sethforprivacy.com:18089".to_string()) .build() + .await .unwrap(); // The invoice is still there! let invoice = payment_gateway .get_invoice(invoice_id) + .await .unwrap() .expect("invoice not found"); diff --git a/library/examples/websockets/main.rs b/library/examples/websockets/main.rs index 76c15d0..abccd93 100644 --- a/library/examples/websockets/main.rs +++ b/library/examples/websockets/main.rs @@ -1,4 +1,4 @@ -#![warn(clippy::pedantic)] +//! Use websockets to notify of invoice updates. use std::{ future::Future, @@ -59,6 +59,7 @@ async fn main() -> std::io::Result<()> { ) .daemon_url("http://xmr-node.cakewallet.com:18081".to_string()) .build() + .await .expect("failed to build payment gateway"); info!("Payment gateway created."); @@ -70,7 +71,7 @@ async fn main() -> std::io::Result<()> { // Watch for invoice updates and deal with them accordingly. let gateway_copy = payment_gateway.clone(); - std::thread::spawn(move || { + tokio::spawn(async move { // Watch all invoice updates. let mut subscriber = gateway_copy.subscribe_all(); loop { @@ -86,7 +87,7 @@ async fn main() -> std::io::Result<()> { "Invoice to index {} is either confirmed or expired. Removing invoice now", invoice.index() ); - if let Err(e) = gateway_copy.remove_invoice(invoice.id()) { + if let Err(e) = gateway_copy.remove_invoice(invoice.id()).await { error!("Failed to remove fully confirmed invoice: {}", e); }; } @@ -137,6 +138,7 @@ async fn checkout( ) -> Result { let invoice_id = payment_gateway .new_invoice(1_000_000_000, 2, 5, checkout_info.message.clone()) + .await .unwrap(); session.insert("id", invoice_id)?; Ok(HttpResponse::Ok() @@ -152,7 +154,7 @@ async fn update( payment_gateway: web::Data>, ) -> Result { if let Ok(Some(invoice_id)) = session.get::("id") { - if let Ok(Some(invoice)) = payment_gateway.get_invoice(invoice_id) { + if let Ok(Some(invoice)) = payment_gateway.get_invoice(invoice_id).await { return Ok(HttpResponse::Ok() .append_header(CacheControl(vec![CacheDirective::NoStore])) .json(json!( diff --git a/library/src/caching/block_cache.rs b/library/src/caching/block_cache.rs index 57fcd41..50513b6 100644 --- a/library/src/caching/block_cache.rs +++ b/library/src/caching/block_cache.rs @@ -7,8 +7,9 @@ use std::{ }; use log::{debug, trace, warn}; +use thiserror::Error; -use crate::{rpc::RpcClient, AcceptXmrError}; +use crate::rpc::{RpcClient, RpcError}; pub(crate) struct BlockCache { height: Arc, @@ -23,7 +24,7 @@ impl BlockCache { cache_size: usize, initial_height: Arc, daemon_height: Arc, - ) -> Result { + ) -> Result { let mut blocks = Vec::with_capacity(cache_size); // TODO: Get blocks concurrently. for i in 0..cache_size { @@ -60,13 +61,13 @@ impl BlockCache { /// Advance block cache by 1 block if new block is available and apply reorg /// if one has occurred. Returns number of blocks updated. - pub(crate) async fn update(&mut self) -> Result { + pub(crate) async fn update(&mut self) -> Result { trace!("Checking for block cache updates"); let mut updated = 0; let blockchain_height = self.rpc_client.daemon_height().await?; self.daemon_height .store(blockchain_height, Ordering::Relaxed); - if self.height.load(Ordering::Relaxed) < blockchain_height - 1 { + if self.height.load(Ordering::Relaxed) < blockchain_height.saturating_sub(1) { let (block_id, block) = self .rpc_client .block(self.height.load(Ordering::Relaxed) + 1) @@ -81,12 +82,12 @@ impl BlockCache { transactions, }, ); - self.blocks.remove(self.blocks.len() - 1); + self.blocks.remove(self.blocks.len().saturating_sub(1)); self.height.fetch_add(1, Ordering::Relaxed); debug!( "Cache top block height updated to {}, blockchain top block height is {}, blockchain height is {}", self.height.load(Ordering::Relaxed), - blockchain_height - 1, + blockchain_height.saturating_sub(1), blockchain_height, ); self.log_cache_summary(); @@ -101,12 +102,16 @@ impl BlockCache { self.height.load(Ordering::Relaxed) } + pub(crate) fn daemon_height(&self) -> u64 { + self.daemon_height.load(Ordering::Relaxed) + } + pub(crate) fn blocks(&self) -> &Vec { &self.blocks } /// Check for reorgs, and update blocks if one has occurred. - async fn check_and_fix_reorg(&mut self) -> Result { + async fn check_and_fix_reorg(&mut self) -> Result { let mut updated = 0; let cache_height = self.height.load(Ordering::Relaxed); for i in 0..self.blocks.len() - 1 { @@ -141,6 +146,10 @@ impl BlockCache { } trace!("Block cache summary:\n{}", block_cache_summary); } + + pub(crate) fn is_synchronized(&self) -> bool { + self.height() >= self.daemon_height().saturating_sub(1) + } } pub(crate) struct Block { @@ -149,3 +158,11 @@ pub(crate) struct Block { inner: monero::Block, pub(crate) transactions: Vec, } + +/// Errors specific to the block cache. +#[derive(Error, Debug)] +pub enum BlockCacheError { + /// An error originating from a daemon RPC call. + #[error("RPC error: {0}")] + Rpc(#[from] RpcError), +} diff --git a/library/src/caching/mod.rs b/library/src/caching/mod.rs index a071d11..43f60d8 100644 --- a/library/src/caching/mod.rs +++ b/library/src/caching/mod.rs @@ -2,6 +2,6 @@ mod block_cache; mod subaddress_cache; mod txpool_cache; -pub(crate) use block_cache::BlockCache; +pub(crate) use block_cache::{BlockCache, BlockCacheError}; pub(crate) use subaddress_cache::SubaddressCache; -pub(crate) use txpool_cache::TxpoolCache; +pub(crate) use txpool_cache::{TxpoolCache, TxpoolCacheError}; diff --git a/library/src/caching/subaddress_cache.rs b/library/src/caching/subaddress_cache.rs index a6c3eed..dd22fd6 100644 --- a/library/src/caching/subaddress_cache.rs +++ b/library/src/caching/subaddress_cache.rs @@ -2,7 +2,7 @@ use std::{ cmp, sync::{ atomic::{AtomicU32, Ordering}, - Arc, PoisonError, RwLock, + Arc, Mutex, PoisonError, }, }; @@ -12,7 +12,10 @@ use monero::{cryptonote::subaddress, ViewPair}; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha12Rng; -use crate::{storage::InvoiceStorage, SubIndex}; +use crate::{ + storage::{Client as StorageClient, Storage, StorageError}, + SubIndex, +}; const MIN_AVAILABLE_SUBADDRESSES: u32 = 100; @@ -25,27 +28,35 @@ pub(crate) struct SubaddressCache { } impl SubaddressCache { - pub(crate) fn init( - storage: &Arc>, + pub(crate) async fn init( + storage: &StorageClient, viewpair: monero::ViewPair, major_index: u32, highest_minor_index: Arc, seed: Option, - ) -> Result { + ) -> Result { // Get currently used subindexes from database, so they won't be put in the list // of available subindexes. - let used_sub_indexes = storage - .read() - .unwrap_or_else(PoisonError::into_inner) - .try_iter()? - .map(|invoice_or_err| match invoice_or_err { - Ok(invoice) => Ok(invoice.index()), - Err(e) => Err(e), + let used_sub_indexes = Arc::new(Mutex::new(IndexSet::new())); + let cloned_sub_indexes = used_sub_indexes.clone(); + storage + .try_for_each_invoice(move |invoice_or_err| { + let sub_index = invoice_or_err?.index(); + cloned_sub_indexes + .lock() + .unwrap_or_else(PoisonError::into_inner) + .insert(sub_index); + Ok(()) }) - .collect::, S::Error>>()?; + .await?; // Get highest index from list of used subindexes. - let max_used = if let Some(max_sub_index) = used_sub_indexes.iter().max() { + let max_used = if let Some(max_sub_index) = used_sub_indexes + .lock() + .unwrap_or_else(PoisonError::into_inner) + .iter() + .max() + { debug!( "Highest subaddress index in the database: {}", SubIndex::new(major_index, max_sub_index.minor) @@ -70,7 +81,12 @@ impl SubaddressCache { .collect(); // Remove subaddresses that are present in the database. - available_subaddresses.retain(|sub_index, _| !used_sub_indexes.contains(sub_index)); + available_subaddresses.retain(|sub_index, _| { + !used_sub_indexes + .lock() + .unwrap_or_else(PoisonError::into_inner) + .contains(sub_index) + }); // If a seed is supplied, seed the random number generator with it. let mut rng = ChaCha12Rng::from_entropy(); diff --git a/library/src/caching/txpool_cache.rs b/library/src/caching/txpool_cache.rs index b6d4f68..e7ba5e8 100644 --- a/library/src/caching/txpool_cache.rs +++ b/library/src/caching/txpool_cache.rs @@ -2,9 +2,14 @@ use std::collections::HashMap; use log::trace; use monero::cryptonote::hash::Hashable; +use thiserror::Error; use tokio::join; -use crate::{invoice::Transfer, rpc::RpcClient, AcceptXmrError, SubIndex}; +use crate::{ + invoice::Transfer, + rpc::{RpcClient, RpcError}, + SubIndex, +}; pub(crate) struct TxpoolCache { rpc_client: RpcClient, @@ -13,7 +18,7 @@ pub(crate) struct TxpoolCache { } impl TxpoolCache { - pub(crate) async fn init(rpc_client: RpcClient) -> Result { + pub(crate) async fn init(rpc_client: RpcClient) -> Result { let txs = rpc_client.txpool().await?; let transactions = txs.iter().map(|tx| (tx.hash(), tx.clone())).collect(); @@ -26,7 +31,7 @@ impl TxpoolCache { /// Update the txpool cache with newest [tansactions](monero::Transaction) /// from daemon txpool. Returns transactions received. - pub(crate) async fn update(&mut self) -> Result, AcceptXmrError> { + pub(crate) async fn update(&mut self) -> Result, TxpoolCacheError> { trace!("Checking for new transactions in txpool"); let txpool_hashes = self.rpc_client.txpool_hashes().await?; @@ -71,3 +76,11 @@ impl TxpoolCache { ); } } + +/// Errors specific to the block cache. +#[derive(Error, Debug)] +pub enum TxpoolCacheError { + /// An error originating from a daemon RPC call. + #[error("RPC error: {0}")] + Rpc(#[from] RpcError), +} diff --git a/library/src/invoice.rs b/library/src/invoice.rs index dd721eb..e763fbb 100644 --- a/library/src/invoice.rs +++ b/library/src/invoice.rs @@ -11,6 +11,8 @@ use monero::cryptonote::subaddress; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +use crate::AcceptXmrError; + const PICONEROS_PER_XMR: u64 = 1_000_000_000_000; /// Representation of an invoice. `Invoice`s are created by the @@ -158,10 +160,12 @@ impl Invoice { /// /// # Examples /// - /// ``` + /// ```no_run /// # #[tokio::main] /// # async fn main() -> Result<(), Box> { /// # + /// # use std::time::Duration; + /// # /// # use acceptxmr::{PaymentGatewayBuilder, storage::stores::InMemory}; /// # /// # let store = InMemory::new(); @@ -170,20 +174,22 @@ impl Invoice { /// # let primary_address = "4613YiHLM6JMH4zejMB2zJY5TwQCxL8p65ufw8kBP5yxX9itmuGLqp1dS4tkVoTxjyH3aYhYNrtGHbQzJQP5bFus3KHVdmf"; /// # /// # let payment_gateway = PaymentGatewayBuilder::new(private_view_key.to_string(), primary_address.to_string(), store) - /// # .build()?; + /// # .build() + /// # .await?; + /// # /// // Create a new `Invoice` for 1 millinero. - /// let invoice_id = payment_gateway.new_invoice(1_000_000_000, 3, 5, "for pizza".to_string())?; - /// let small_invoice = payment_gateway.get_invoice(invoice_id)?.expect("invoice ID not found"); + /// let invoice_id = payment_gateway.new_invoice(1_000_000_000, 3, 5, "for pizza".to_string()).await?; + /// let small_invoice = payment_gateway.get_invoice(invoice_id).await?.expect("invoice ID not found"); /// /// // One millinero, as expected. /// assert_eq!(small_invoice.xmr_requested(), 0.001); /// /// // Create a new `Invoice` for 18446744.073709551615 XMR. - /// let invoice_id = payment_gateway.new_invoice(18_446_744_073_709_551_615, 3, 5, "for lambo".to_string())?; - /// let large_invoice = payment_gateway.get_invoice(invoice_id)?.expect("invoice ID not found"); + /// let invoice_id = payment_gateway.new_invoice(18_446_744_073_709_551_615, 3, 5, "for lambo".to_string()).await?; + /// let large_invoice = payment_gateway.get_invoice(invoice_id).await?.expect("invoice ID not found"); /// /// // The large value has been rounded slightly due to f64 precision limitations. - /// assert_eq!(large_invoice.xmr_requested(), 18446744.073709551245); + /// assert_eq!(large_invoice.xmr_requested(), 18446744.07370955); /// # Ok(()) /// # } /// ``` @@ -265,12 +271,13 @@ impl Invoice { /// # let store = InMemory::new(); /// # /// # let payment_gateway = PaymentGatewayBuilder::new(private_view_key.to_string(), primary_address.to_string(), store) - /// # .build()?; + /// # .build() + /// # .await?; /// # /// # payment_gateway.run().await?; /// # /// // Create a new `Invoice` requiring 3 confirmations, and expiring in 5 blocks. - /// let invoice_id = payment_gateway.new_invoice(10000, 3, 5, "for pizza".to_string())?; + /// let invoice_id = payment_gateway.new_invoice(10000, 3, 5, "for pizza".to_string()).await?; /// let mut subscriber = payment_gateway.subscribe(invoice_id).expect("invoice ID not found"); /// let invoice = subscriber.recv().await.expect("invoice update not received"); /// @@ -383,6 +390,100 @@ impl InvoiceId { creation_height, } } + + /// Returns a 32 byte blake3 hash of the invoice ID. + #[must_use] + pub fn hash(&self) -> InvoiceIdHash { + InvoiceIdHash(blake3::hash(self.to_string().as_bytes())) + } +} + +/// A 32 byte (256 bit) blake3 hash of the invoice ID. +#[derive(Copy, Clone)] +pub struct InvoiceIdHash(blake3::Hash); + +impl InvoiceIdHash { + /// Returns the `InvoiceIdHash` as bytes. + #[must_use] + pub fn as_bytes(&self) -> &[u8; 32] { + self.0.as_bytes() + } + + /// Returns the `InvoiceIdHash` as bytes. + #[must_use] + pub fn from_bytes(bytes: [u8; 32]) -> Self { + InvoiceIdHash(blake3::Hash::from_bytes(bytes)) + } + + #[must_use] + /// Returns the `InvoiceIdHash` as a hex string. + pub fn to_hex(self) -> String { + self.0.to_hex().to_string() + } + + /// Attempts to parse a `InvoiceIdHash` from hex. + /// + /// # Errors + /// + /// Returns an error if the input is not valid hex. + pub fn from_hex(hex: impl AsRef<[u8]>) -> Result { + let hash_or_err = blake3::Hash::from_hex(&hex); + let hash = hash_or_err.map_err(|e| AcceptXmrError::Parse { + datatype: "InvoiceIdHash", + input: hex.as_ref().to_ascii_lowercase().escape_ascii().to_string(), + error: e.to_string(), + })?; + Ok(InvoiceIdHash(hash)) + } +} + +impl From<[u8; 32]> for InvoiceIdHash { + #[inline] + fn from(bytes: [u8; 32]) -> Self { + Self::from_bytes(bytes) + } +} + +impl From for [u8; 32] { + #[inline] + fn from(hash: InvoiceIdHash) -> Self { + *hash.0.as_bytes() + } +} + +impl core::str::FromStr for InvoiceIdHash { + type Err = AcceptXmrError; + + fn from_str(s: &str) -> Result { + InvoiceIdHash::from_hex(s) + } +} + +impl PartialEq for InvoiceIdHash { + #[inline] + fn eq(&self, other: &InvoiceIdHash) -> bool { + self.0.eq(&other.0) + } +} + +impl Eq for InvoiceIdHash {} + +impl fmt::Display for InvoiceIdHash { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let hex = self.to_hex(); + let hex: &str = hex.as_str(); + + f.write_str(hex) + } +} + +impl fmt::Debug for InvoiceIdHash { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let hex = self.to_hex(); + let hex: &str = hex.as_str(); + + f.debug_tuple("Hash").field(&hex).finish() + } } impl Ord for InvoiceId { @@ -501,20 +602,12 @@ impl Transfer { #[cfg(test)] #[allow(clippy::expect_used)] +#[allow(clippy::panic)] mod tests { - use log::LevelFilter; use test_case::test_case; + use testing_utils::init_logger; - use crate::{Invoice, SubIndex}; - - fn init_logger() { - env_logger::builder() - .filter_level(LevelFilter::Warn) - .filter_module("acceptxmr", log::LevelFilter::Debug) - .is_test(true) - .try_init() - .ok(); - } + use crate::{AcceptXmrError, Invoice, InvoiceIdHash, SubIndex}; #[test_case(1, 0 => "0.000000000001".to_string(); "small")] #[test_case(u64::MAX, 0 => "18446744.073709551615".to_string(); "big")] @@ -543,6 +636,25 @@ mod tests { amount.to_string() } + #[test_case(1 => "0.000000000001".to_string(); "small")] + #[test_case(u64::MAX => "18446744.07370955".to_string(); "big")] + #[test_case(0 => "0".to_string(); "zero")] + fn xmr_requested(requested: u64) -> String { + // Setup. + init_logger(); + + let invoice = Invoice::new( + "testAddress".to_string(), + SubIndex::new(0, 1), + 0, + requested, + 5, + 10, + "test_description".to_string(), + ); + invoice.xmr_requested().to_string() + } + #[test] fn expires_in() { init_logger(); @@ -559,4 +671,53 @@ mod tests { assert_eq!(invoice.expiration_in(), 10); } + + #[test] + fn id_hash() { + init_logger(); + + let invoice = Invoice::new( + "testAddress".to_string(), + SubIndex::new(0, 1), + 12345, + 1, + 5, + 10, + "test_description".to_string(), + ); + let invoice_id = invoice.id(); + let invoice_id_hash = invoice_id.hash(); + + assert_eq!( + invoice_id_hash.to_string(), + "19657377c5d6a686c5e3be00706ff2ed6c9579d63b87ec1329b299990a4a13b4".to_string() + ); + } + + #[test_case("abcdeff12345678900" => Err("abcdeff12345678900".to_string()))] + #[test_case("38405f2dbb3a8b50b35be464cb4b71f2b2fefcdeeb7decbcec37be770df64f92" => Ok(()))] + fn id_hash_from_hex(hex: impl AsRef<[u8]>) -> Result<(), String> { + init_logger(); + + let invoice_id_hash = match InvoiceIdHash::from_hex(hex) { + Ok(hash) => hash, + Err(AcceptXmrError::Parse { + datatype, + input, + error: _, + }) => { + assert_eq!(datatype, "InvoiceIdHash"); + return Err(input); + } + Err(e) => { + panic!("Unexpected error parsing `InvoiceIdHash` from hex: {e}") + } + }; + + assert_eq!( + invoice_id_hash.to_hex(), + "38405f2dbb3a8b50b35be464cb4b71f2b2fefcdeeb7decbcec37be770df64f92".to_string() + ); + Ok(()) + } } diff --git a/library/src/lib.rs b/library/src/lib.rs index ed1708e..0389c01 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -67,7 +67,8 @@ //! consider lowering the [`PaymentGateway`]'s `scan_interval` below the default //! of 1 second: //! ``` -//! # fn main() -> Result<(), Box> { +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { //! use acceptxmr::{PaymentGateway, storage::stores::InMemory}; //! use std::time::Duration; //! @@ -84,7 +85,8 @@ //! store //! ) //! .scan_interval(Duration::from_millis(100)) // Scan for updates every 100 ms. -//! .build()?; +//! .build() +//! .await?; //! # Ok(()) //! # } //! ``` @@ -119,16 +121,10 @@ //! The `sqlite` feature enables the [`Sqlite`](storage::stores::Sqlite) storage //! implementation. The `bincode` feature will also be enabled by this feature. -#![deny(unsafe_code)] -#![warn(missing_docs)] -#![warn(unreachable_pub)] -#![warn(clippy::pedantic)] -#![warn(clippy::cargo)] #![warn(clippy::panic)] #![warn(clippy::unwrap_used)] #![warn(clippy::expect_used)] #![allow(clippy::multiple_crate_versions)] -#![allow(clippy::module_name_repetitions)] // Show feature flag tags on `docs.rs` #![cfg_attr(docsrs, feature(doc_auto_cfg))] @@ -142,10 +138,12 @@ pub mod storage; use std::fmt::Debug; -pub use invoice::{Invoice, InvoiceId, SubIndex}; +pub use invoice::{Invoice, InvoiceId, InvoiceIdHash, SubIndex}; pub use payment_gateway::{PaymentGateway, PaymentGatewayBuilder, PaymentGatewayStatus}; pub use pubsub::{Subscriber, SubscriberError}; use rpc::RpcError; +use scanner::ScannerError; +use storage::StorageError; use thiserror::Error; /// Library's custom error type. @@ -156,13 +154,7 @@ pub enum AcceptXmrError { Rpc(#[from] RpcError), /// An error storing/retrieving data from the storage layer. #[error("storage error: {0}")] - Storage(Box), - /// [`Subscriber`] failed to retrieve update. - #[error("subscriber failed to receive update: {0}")] - Subscriber(#[from] SubscriberError), - /// Failure to unblind the amount of an owned output. - #[error("unable to unblind amount of owned output sent to subaddress index {0}")] - Unblind(SubIndex), + Storage(#[from] StorageError), /// Failure to parse. #[error("failed to parse {datatype} from \"{input}\": {error}")] Parse { @@ -173,25 +165,12 @@ pub enum AcceptXmrError { /// Error encountered. error: String, }, - /// Failure to check if output is owned. - #[error("failed to check if output is owned: {0}")] - OwnedOutputCheck(#[from] monero::blockdata::transaction::Error), - /// Output has unsupported target. This could mean that AcceptXMR is - /// connected to an outdated daemon. - #[error("unsupported output target")] - OutputTarget, - /// Output index was too large. - #[error("output index too large")] - OutputIndex, - /// Scanning thread exited with panic. - #[error("scanning thread exited with panic")] - ScanningThreadPanic, + /// Blockchain scanner encountered an error. + #[error("blockchain scanner encountered an error: {0}")] + Scanner(#[from] ScannerError), /// Payment gateway is already running. #[error("payment gateway is already running")] AlreadyRunning, - /// Payment gateway encountered an error while creating scanning thread. - #[error("payment gateway encountered an error while creating scanning thread: {0}")] - Threading(#[from] std::io::Error), /// Payment gateway could not be stopped because the stop signal was not /// sent. #[error("payment gateway could not be stopped because the stop signal was not sent: {0}")] diff --git a/library/src/payment_gateway.rs b/library/src/payment_gateway.rs index c00041e..4263263 100644 --- a/library/src/payment_gateway.rs +++ b/library/src/payment_gateway.rs @@ -5,23 +5,22 @@ use std::{ sync::{ atomic::{self, AtomicU32, AtomicU64}, mpsc::{channel, Receiver, Sender, TryRecvError}, - Arc, Mutex, PoisonError, RwLock, + Arc, Mutex, PoisonError, }, - thread, time::Duration, }; use hyper::Uri; -use log::{debug, error, info, warn}; +use log::{debug, error, info, trace, warn}; use monero::cryptonote::onetime_key::SubKeyChecker; -use tokio::{join, runtime::Runtime, time}; +use tokio::{join, sync::Mutex as AsyncMutex, time}; use crate::{ caching::SubaddressCache, pubsub::{Publisher, Subscriber}, rpc::RpcClient, scanner::{Scanner, ScannerHandle}, - storage::{InvoiceStorage, Storage}, + storage::{Client as StorageClient, Storage}, AcceptXmrError, Invoice, InvoiceId, }; @@ -43,14 +42,14 @@ pub struct PaymentGatewayInner { rpc_client: RpcClient, viewpair: monero::ViewPair, scan_interval: Duration, - store: Arc>, + store: StorageClient, subaddresses: Mutex, major_index: u32, highest_minor_index: Arc, initial_height: Option, block_cache_height: Arc, cached_daemon_height: Arc, - scanner_handle: Mutex>, + scanner_handle: AsyncMutex>, /// Send commands to the scanning thread. scanner_command_sender: ( Mutex>, @@ -105,10 +104,7 @@ impl PaymentGateway { pub async fn run(&self) -> Result<(), AcceptXmrError> { // Determine if the scanning thread is already running. { - let scanner_handle = self - .scanner_handle - .lock() - .unwrap_or_else(PoisonError::into_inner); + let scanner_handle = self.scanner_handle.lock().await; if let Some(handle) = scanner_handle.as_ref() { if !handle.is_finished() { return Err(AcceptXmrError::AlreadyRunning); @@ -144,52 +140,73 @@ impl PaymentGateway { // Spawn the scanning thread. info!("Starting blockchain scanner"); - *self.scanner_handle.lock().unwrap_or_else(PoisonError::into_inner) = Some(thread::Builder::new() - .name("Scanning Thread".to_string()) - .spawn(move || -> Result<(), AcceptXmrError> { - // The thread needs a tokio runtime to process async functions. - let tokio_runtime = Runtime::new()?; - tokio_runtime.block_on(async move { - // Create persistent sub key checker for efficient tx output checking. - let mut sub_key_checker = SubKeyChecker::new( + *self.scanner_handle.lock().await = Some(ScannerHandle::from(tokio::spawn(async move { + // Create persistent sub key checker for efficient tx output checking. + let mut sub_key_checker = SubKeyChecker::new( + &viewpair, + major_index..major_index.saturating_add(1), + 0..highest_minor_index + .load(atomic::Ordering::Relaxed) + .saturating_add(1), + ); + // Scan for transactions once every scan_interval. + let mut blockscan_interval = time::interval(scan_interval); + loop { + // If we're received the stop signal, stop. + match command_receiver + .lock() + .unwrap_or_else(PoisonError::into_inner) + .try_recv() + { + Ok(MessageToScanner::Stop) => { + info!("Scanner received stop signal. Stopping scanning thread"); + break; + } + Err(TryRecvError::Empty) => {} + Err(TryRecvError::Disconnected) => { + error!( + "Scanner lost connection to payment gateway. Stopping scanning thread." + ); + break; + } + } + // Update sub key checker if necessary. + if sub_key_checker.table.len() + <= highest_minor_index.load(atomic::Ordering::Relaxed) as usize + { + sub_key_checker = SubKeyChecker::new( &viewpair, - 1..2, - 0..highest_minor_index.load(atomic::Ordering::Relaxed) + 1, + major_index..major_index.saturating_add(1), + 0..highest_minor_index + .load(atomic::Ordering::Relaxed) + .saturating_add(1), ); - // Scan for transactions once every scan_interval. - let mut blockscan_interval = time::interval(scan_interval); - loop { - // If we're received the stop signal, stop. - match command_receiver.lock().unwrap_or_else(PoisonError::into_inner).try_recv() { - Ok(MessageToScanner::Stop) => { - info!("Scanner received stop signal. Stopping gracefully"); - break; - } - Err(TryRecvError::Empty) => { - } - Err(TryRecvError::Disconnected) => { - error!("Scanner lost connection to payment gateway. Stopping gracefully."); - break; - } - } - // Update sub key checker if necessary. - if sub_key_checker.table.len() - <= highest_minor_index.load(atomic::Ordering::Relaxed) as usize - { - sub_key_checker = SubKeyChecker::new( - &viewpair, - major_index..major_index.saturating_add(1), - 0..highest_minor_index.load(atomic::Ordering::Relaxed).saturating_add(1), - ); - } - // Scan! - if let (_, Err(e)) = join!(blockscan_interval.tick(), scanner.scan(&sub_key_checker)) { - error!("Payment gateway encountered an error while scanning for payments: {}", e); - }; - } - }); - Ok(()) - })?.into()); + } + // Scan! + if let Err(e) = if scanner.is_synchronized().await { + // Scan at the specified interval if we're caught up. + trace!("Waiting for scan interval."); + let (_, result) = + join!(blockscan_interval.tick(), scanner.scan(&sub_key_checker)); + result + } else { + // Scan as fast as we can if we're behind. + trace!( + "Scanning at max speed to catch up. Cache height: {}, daemon height: {}", + scanner.cache_height().await, + scanner.daemon_height().await + ); + scanner.scan(&sub_key_checker).await + } { + error!( + "Payment gateway encountered an error while scanning for payments: {}", + e + ); + }; + } + + Ok(()) + }))); debug!("Scanner started successfully"); Ok(()) } @@ -197,25 +214,19 @@ impl PaymentGateway { /// Returns the enum [`PaymentGatewayStatus`] describing whether the payment /// gateway is running, not running, or has experienced an error. #[must_use] - pub fn status(&self) -> PaymentGatewayStatus { - let scanner_handle = self - .scanner_handle - .lock() - .unwrap_or_else(PoisonError::into_inner); + pub async fn status(&self) -> PaymentGatewayStatus { + let scanner_handle = self.scanner_handle.lock().await; match scanner_handle.as_ref() { None => PaymentGatewayStatus::NotRunning, Some(handle) if handle.is_finished() => { - let owned_handle = self - .scanner_handle - .lock() - .unwrap_or_else(PoisonError::into_inner) - .take(); - match owned_handle.map(ScannerHandle::join) { - None | Some(Ok(Ok(()))) => PaymentGatewayStatus::NotRunning, - Some(Ok(Err(e))) => PaymentGatewayStatus::Error(e), - Some(Err(_)) => { - PaymentGatewayStatus::Error(AcceptXmrError::ScanningThreadPanic) + let owned_handle = self.scanner_handle.lock().await.take(); + if let Some(handle) = owned_handle { + match handle.join().await { + Ok(()) => PaymentGatewayStatus::NotRunning, + Err(e) => PaymentGatewayStatus::Error(AcceptXmrError::Scanner(e)), } + } else { + PaymentGatewayStatus::NotRunning } } Some(_) => PaymentGatewayStatus::Running, @@ -235,18 +246,12 @@ impl PaymentGateway { /// /// * If the scanning thread exited with an error, returns the error /// encountered. - pub fn stop(&self) -> Result<(), AcceptXmrError> { - match self - .scanner_handle - .lock() - .unwrap_or_else(PoisonError::into_inner) - .take() - { + pub async fn stop(&self) -> Result<(), AcceptXmrError> { + match self.scanner_handle.lock().await.take() { None => Ok(()), - Some(thread) if thread.is_finished() => match thread.join() { - Ok(Ok(())) => Ok(()), - Ok(Err(e)) => Err(e), - Err(_) => Err(AcceptXmrError::ScanningThreadPanic), + Some(thread) if thread.is_finished() => match thread.join().await { + Ok(()) => Ok(()), + Err(e) => Err(AcceptXmrError::Scanner(e)), }, Some(thread) => { self.scanner_command_sender @@ -255,10 +260,9 @@ impl PaymentGateway { .unwrap_or_else(PoisonError::into_inner) .send(MessageToScanner::Stop) .map_err(|e| AcceptXmrError::StopSignal(e.to_string()))?; - match thread.join() { - Ok(Ok(())) => Ok(()), - Ok(Err(e)) => Err(e), - Err(_) => Err(AcceptXmrError::ScanningThreadPanic), + match thread.join().await { + Ok(()) => Ok(()), + Err(e) => Err(AcceptXmrError::Scanner(e)), } } } @@ -272,7 +276,7 @@ impl PaymentGateway { /// /// Returns an error if there are any underlying issues modifying data in /// the database. - pub fn new_invoice( + pub async fn new_invoice( &self, piconeros: u64, confirmations_required: u64, @@ -288,7 +292,12 @@ impl PaymentGateway { .unwrap_or_else(PoisonError::into_inner) .remove_random(); - let creation_height = self.cached_daemon_height.load(atomic::Ordering::Relaxed); + let cached_daemon_height = self.cached_daemon_height.load(atomic::Ordering::Relaxed); + let creation_height = if cached_daemon_height != 0 { + cached_daemon_height + } else { + self.daemon_height().await? + }; // Create invoice object. let invoice = Invoice::new( @@ -302,11 +311,7 @@ impl PaymentGateway { ); // Insert invoice into database for tracking. - InvoiceStorage::insert( - &mut *self.store.write().unwrap_or_else(PoisonError::into_inner), - invoice.clone(), - ) - .map_err(|e| AcceptXmrError::Storage(Box::new(e)))?; + self.store.insert_invoice(invoice.clone()).await?; debug!( "Now tracking invoice to subaddress index {}", invoice.index() @@ -326,14 +331,11 @@ impl PaymentGateway { /// /// Returns an error if there are any underlying issues modifying/retrieving /// data in the database. - pub fn remove_invoice(&self, invoice_id: InvoiceId) -> Result, AcceptXmrError> { - match self - .store - .write() - .unwrap_or_else(PoisonError::into_inner) - .remove(invoice_id) - .map_err(|e| AcceptXmrError::Storage(Box::new(e)))? - { + pub async fn remove_invoice( + &self, + invoice_id: InvoiceId, + ) -> Result, AcceptXmrError> { + match self.store.remove_invoice(invoice_id).await? { Some(old) => { if !(old.is_expired() || old.is_confirmed() && old.creation_height() < old.current_height()) @@ -395,12 +397,11 @@ impl PaymentGateway { /// /// Returns an error if there are any underlying issues retrieving data from /// the database. - pub fn get_invoice(&self, invoice_id: InvoiceId) -> Result, AcceptXmrError> { - InvoiceStorage::get( - &*self.store.read().unwrap_or_else(PoisonError::into_inner), - invoice_id, - ) - .map_err(|e| AcceptXmrError::Storage(Box::new(e))) + pub async fn get_invoice( + &self, + invoice_id: InvoiceId, + ) -> Result, AcceptXmrError> { + Ok(self.store.get_invoice(invoice_id).await?) } /// Returns URL of configured daemon. @@ -501,7 +502,8 @@ impl PaymentGatewayBuilder { /// store /// ) /// .daemon_url("http://example.com:18081".to_string()) // Set custom monero daemon URL. - /// .build()?; + /// .build() + /// .await?; /// /// // The payment gateway will now use the daemon specified. /// payment_gateway.run().await?; @@ -590,7 +592,7 @@ impl PaymentGatewayBuilder { /// Returns an error if the database cannot be opened at the path specified, /// if the internal RPC client cannot parse the provided URL, or if the /// primary address or private view key cannot be parsed. - pub fn build(self) -> Result, AcceptXmrError> { + pub async fn build(self) -> Result, AcceptXmrError> { let rpc_client = RpcClient::new( self.daemon_url .parse::() @@ -606,13 +608,13 @@ impl PaymentGatewayBuilder { self.seed, ); - let store = Arc::new(RwLock::new(self.store)); + let store = StorageClient::new(self.store); let viewpair = monero::ViewPair { view: monero::PrivateKey::from_str(&self.private_view_key).map_err(|e| { AcceptXmrError::Parse { datatype: "PrivateKey", - input: self.private_view_key.to_string(), + input: "[REDACTED]".to_string(), error: e.to_string(), } })?, @@ -633,7 +635,7 @@ impl PaymentGatewayBuilder { highest_minor_index.clone(), self.seed, ) - .map_err(|e| AcceptXmrError::Storage(Box::new(e)))?; + .await?; debug!("Generated {} initial subaddresses", subaddresses.len()); let (scanner_cmd_tx, scanner_cmd_rx) = channel(); @@ -653,7 +655,7 @@ impl PaymentGatewayBuilder { initial_height: self.initial_height, block_cache_height: Arc::new(atomic::AtomicU64::new(0)), cached_daemon_height: Arc::new(atomic::AtomicU64::new(0)), - scanner_handle: Mutex::new(None), + scanner_handle: AsyncMutex::new(None), scanner_command_sender, publisher: Arc::new(Publisher::new()), }))) @@ -680,26 +682,12 @@ pub(crate) enum MessageToScanner { #[cfg(test)] #[allow(clippy::unwrap_used)] mod tests { - use log::LevelFilter; + use testing_utils::{init_logger, PRIMARY_ADDRESS, PRIVATE_VIEW_KEY}; use crate::{storage::stores::InMemory, PaymentGateway, PaymentGatewayBuilder}; - fn init_logger() { - env_logger::builder() - .filter_level(LevelFilter::Warn) - .filter_module("acceptxmr", log::LevelFilter::Debug) - .is_test(true) - .try_init() - .ok(); - } - - const PRIVATE_VIEW_KEY: &str = - "ad2093a5705b9f33e6f0f0c1bc1f5f639c756cdfc168c8f2ac6127ccbdab3a03"; - const PRIMARY_ADDRESS: &str = - "4613YiHLM6JMH4zejMB2zJY5TwQCxL8p65ufw8kBP5yxX9itmuGLqp1dS4tkVoTxjyH3aYhYNrtGHbQzJQP5bFus3KHVdmf"; - - #[test] - fn daemon_url() { + #[tokio::test] + async fn daemon_url() { // Setup. init_logger(); let store = InMemory::new(); @@ -711,6 +699,7 @@ mod tests { ) .daemon_url("http://example.com:18081".to_string()) .build() + .await .unwrap(); assert_eq!( diff --git a/library/src/pubsub.rs b/library/src/pubsub.rs index 031275a..25426e6 100644 --- a/library/src/pubsub.rs +++ b/library/src/pubsub.rs @@ -217,14 +217,14 @@ impl Publisher { invoice_subs .get_mut(&id) - .and_then(|map| map.remove(&sender_id)); + .and_then(|map| map.swap_remove(&sender_id)); } else { let mut global_subs = self .global_subs .lock() .unwrap_or_else(PoisonError::into_inner); - global_subs.remove(&sender_id); + global_subs.swap_remove(&sender_id); } } } diff --git a/library/src/rpc/authentication.rs b/library/src/rpc/authentication.rs index 1055a90..10c6cd0 100644 --- a/library/src/rpc/authentication.rs +++ b/library/src/rpc/authentication.rs @@ -1,6 +1,5 @@ use std::{ cmp::Ordering, - string::ToString, sync::{ atomic::{self, AtomicU32}, Arc, Mutex, PoisonError, diff --git a/library/src/rpc/mod.rs b/library/src/rpc/mod.rs index beea95e..53e482d 100644 --- a/library/src/rpc/mod.rs +++ b/library/src/rpc/mod.rs @@ -8,14 +8,19 @@ use std::{ }; use authentication::{AuthError, AuthInfo}; -use http::StatusCode; +use backoff::{backoff::Backoff, ExponentialBackoffBuilder}; +use bytes::Bytes; +use http_body_util::{BodyExt, Full}; use hyper::{ - body, - client::connect::HttpConnector, header::{AUTHORIZATION, WWW_AUTHENTICATE}, - Body, Method, Request, Uri, + http::StatusCode, + Method, Request, Uri, }; use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; +use hyper_util::{ + client::legacy::{connect::HttpConnector, Client}, + rt::TokioExecutor, +}; use log::{debug, trace, warn}; use monero::consensus::{deserialize, encode}; use serde_json::json; @@ -27,7 +32,7 @@ const MAX_REQUESTED_TRANSACTIONS: usize = 100; #[derive(Debug, Clone)] pub(crate) struct RpcClient { - client: hyper::Client>, + client: Client, Full>, url: Uri, timeout: Duration, auth_info: Arc>>, @@ -46,13 +51,14 @@ impl RpcClient { let mut hyper_connector = HttpConnector::new(); hyper_connector.set_connect_timeout(Some(connection_timeout)); hyper_connector.enforce_http(false); + hyper_connector.set_keepalive(Some(Duration::from_secs(25))); let rustls_connector = HttpsConnectorBuilder::new() .with_webpki_roots() .https_or_http() .enable_http1() .enable_http2() .wrap_connector(hyper_connector); - let client = hyper::Client::builder().build(rustls_connector); + let client = Client::builder(TokioExecutor::new()).build(rustls_connector); let auth_info = Arc::new(Mutex::new(if username.is_some() || password.is_some() { Some(AuthInfo::new( username.unwrap_or_default(), @@ -236,7 +242,7 @@ impl RpcClient { let mut req = Request::builder() .method(Method::POST) .uri(self.url.clone().to_string() + endpoint) - .body(Body::from(body.to_owned()))?; + .body(Full::new(body.to_owned().into()))?; let (method, uri) = (req.method().clone(), req.uri().clone()); // If configured with a username and password, try to authenticate with most @@ -252,11 +258,17 @@ impl RpcClient { } // Await full response. - let mut response = timeout(self.timeout, self.client.request(req)).await??; + let mut response = timeout(self.timeout, self.client.request(req)) + .await? + .map_err(|e| RpcError::Request(Box::new(e)))?; // If response has www-authenticate header and 401 status, perform digest // authentication. - if response.status() == StatusCode::UNAUTHORIZED + let mut exponential_backoff = ExponentialBackoffBuilder::default() + .with_max_elapsed_time(None) + .with_max_interval(Duration::from_secs(30)) + .build(); + while response.status() == StatusCode::UNAUTHORIZED && response.headers().contains_key(WWW_AUTHENTICATE) { debug!("Recieved 401 UNAUTHORIZED response. Performing digest authentication."); @@ -271,15 +283,30 @@ impl RpcClient { .method(Method::POST) .uri(self.url.clone().to_string() + endpoint) .header(AUTHORIZATION, auth_header) - .body(Body::from(body.to_owned()))?; + .body(Full::new(body.to_owned().into()))?; // Await full response. - response = timeout(self.timeout, self.client.request(req)).await??; + response = timeout(self.timeout, self.client.request(req)) + .await? + .map_err(|e| RpcError::Request(Box::new(e)))?; + + #[allow(clippy::expect_used)] + tokio::time::sleep( + exponential_backoff + .next_backoff() + .expect("RPC exponential backoff timed out. This is a bug."), + ) + .await; } let (_parts, body) = response.into_parts(); - let full_body = body::to_bytes(body).await?; - Ok(serde_json::from_slice(&full_body)?) + Ok(serde_json::from_slice( + &body + .collect() + .await + .map_err(|e| RpcError::Request(Box::new(e)))? + .to_bytes(), + )?) } pub(crate) fn url(&self) -> String { @@ -290,9 +317,9 @@ impl RpcClient { #[derive(Error, Debug)] pub enum RpcError { #[error("HTTP request failed: {0}")] - Http(#[from] hyper::Error), - #[error("failed to build HTTP request: {0}")] - Request(#[from] hyper::http::Error), + Request(Box), + #[error("failed to build HTTP Request: {0}")] + InvalidRequest(#[from] hyper::http::Error), #[error("HTTP request timed out: {0}")] Timeout(#[from] error::Elapsed), #[error("hex decoding failed: {0}")] diff --git a/library/src/scanner.rs b/library/src/scanner.rs index dde3afd..0f00d61 100644 --- a/library/src/scanner.rs +++ b/library/src/scanner.rs @@ -2,9 +2,8 @@ use std::{ collections::HashMap, sync::{ atomic::{AtomicU64, Ordering}, - Arc, PoisonError, RwLock, + Arc, Mutex, PoisonError, }, - thread::{self, JoinHandle}, }; use log::{debug, error, info, trace}; @@ -13,44 +12,52 @@ use monero::{ cryptonote::{hash::Hashable, onetime_key::SubKeyChecker}, Amount, OwnedTxOut, Transaction, VarInt, }; -use tokio::{join, sync::Mutex}; +use rayon::prelude::*; +use thiserror::Error; +use tokio::{ + join, + sync::Mutex as AsyncMutex, + task::{JoinError, JoinHandle}, +}; use crate::{ - caching::{BlockCache, TxpoolCache}, + caching::{BlockCache, BlockCacheError, TxpoolCache, TxpoolCacheError}, invoice::Transfer, pubsub::Publisher, - rpc::RpcClient, - storage::{HeightStorage, OutputId, OutputKeyStorage, OutputPubKey, Storage}, - AcceptXmrError, Invoice, SubIndex, + rpc::{RpcClient, RpcError}, + storage::{Client as StorageClient, OutputId, OutputPubKey, Storage, StorageError}, + Invoice, SubIndex, }; pub(crate) struct Scanner { - store: Arc>, + store: StorageClient, // Block cache and txpool cache are mutexed to allow concurrent block & // txpool scanning. This is necessary even though txpool scanning doesn't // use the block cache, and vice versa, because rust doesn't allow mutably // borrowing only part of "self". - block_cache: Mutex, - txpool_cache: Mutex, + block_cache: AsyncMutex, + txpool_cache: AsyncMutex, publisher: Arc, first_scan: bool, } -impl Scanner { +impl Scanner { pub(crate) async fn new( rpc_client: RpcClient, - store: Arc>, + store: StorageClient, block_cache_size: usize, atomic_cache_height: Arc, atomic_daemon_height: Arc, // Optionally specify the height to start scanning from. initial_height: Option, publisher: Arc, - ) -> Result, AcceptXmrError> { + ) -> Result, ScannerError> { trace!("Retrieving daemon hight for scanner setup."); + let daemon_height = rpc_client.daemon_height().await?; - let cache_height = last_height(&store)? + let cache_height = last_height(&store) + .await? .or(initial_height) .unwrap_or(daemon_height) .min(daemon_height) @@ -75,8 +82,8 @@ impl Scanner { Ok(Scanner { store, - block_cache: Mutex::new(block_cache?), - txpool_cache: Mutex::new(txpool_cache?), + block_cache: AsyncMutex::new(block_cache?), + txpool_cache: AsyncMutex::new(txpool_cache?), publisher, first_scan: true, }) @@ -86,7 +93,7 @@ impl Scanner { pub(crate) async fn scan( &mut self, sub_key_checker: &SubKeyChecker<'_>, - ) -> Result<(), AcceptXmrError> { + ) -> Result<(), ScannerError> { // Update block and txpool caches. let (blocks_updated, new_transactions) = self.update_caches().await?; @@ -129,11 +136,7 @@ impl Scanner { invoice.index(), invoice ); - let result = self - .store - .write() - .unwrap_or_else(PoisonError::into_inner) - .update(invoice.clone()); + let result = self.store.update_invoice(invoice.clone()).await; if let Err(e) = result { error!( "Failed to save update to invoice for index {} to database: {}", @@ -141,23 +144,22 @@ impl Scanner { e ); } else { - // If the update was successful, send an update that down the subscriber - // channel. + // If the update was successful, send an update down the + // subscriber channel. self.publisher.send_updates(&invoice).await; + debug!( + "Published invoice update for subaddress index {}", + invoice.index() + ); } } // Update last scanned height in the database. let cache_height = self.block_cache.lock().await.height(); - self.store - .write() - .unwrap_or_else(PoisonError::into_inner) - .upsert(cache_height) - .map_err(|e| AcceptXmrError::Storage(Box::new(e)))?; + self.store.upsert_height(cache_height).await?; // Flush changes to the database. - Storage::flush(&*self.store.read().unwrap_or_else(PoisonError::into_inner)) - .map_err(|e| AcceptXmrError::Storage(Box::new(e)))?; + self.store.flush().await?; Ok(()) } @@ -166,18 +168,14 @@ impl Scanner { &self, transfers: Vec<(SubIndex, Transfer)>, blocks_updated: usize, - ) -> Result, AcceptXmrError> { + ) -> Result, ScannerError> { let block_cache_height = self.block_cache.lock().await.height(); let deepest_update = block_cache_height - blocks_updated as u64 + 1; - let mut updated_invoices = Vec::new(); - for invoice_or_err in self - .store - .read() - .unwrap_or_else(PoisonError::into_inner) - .try_iter() - .map_err(|e| AcceptXmrError::Storage(Box::new(e)))? - { + let updated_invoices = Arc::new(Mutex::new(Vec::new())); + let cloned_invoices = updated_invoices.clone(); + // TODO: Find a way to parallelize this. + self.store.try_for_each_invoice(move |invoice_or_err| { // Retrieve old invoice object. let old_invoice = match invoice_or_err { Ok(p) => p, @@ -185,7 +183,8 @@ impl Scanner { error!( "Failed to retrieve old invoice object from database while iterating through database: {}", e ); - continue; + // Return OK here because we still want to process the others. + return Ok(()); } }; let mut invoice = old_invoice.clone(); @@ -232,13 +231,19 @@ impl Scanner { // This invoice has been updated. We can now add it in with the other // updated_invoices. - updated_invoices.push(invoice); + cloned_invoices.lock().unwrap_or_else(PoisonError::into_inner).push(invoice); } - } + + Ok(()) + }).await?; + let updated_invoices = updated_invoices + .lock() + .unwrap_or_else(PoisonError::into_inner) + .to_vec(); Ok(updated_invoices) } - async fn update_caches(&self) -> Result<(usize, Vec), AcceptXmrError> { + async fn update_caches(&self) -> Result<(usize, Vec), ScannerError> { // Update block cache. let mut block_cache = self.block_cache.lock().await; let blocks_updated = block_cache.update().await?; @@ -258,7 +263,7 @@ impl Scanner { &self, sub_key_checker: &SubKeyChecker<'_>, mut blocks_updated: usize, - ) -> Result, AcceptXmrError> { + ) -> Result, ScannerError> { let block_cache = self.block_cache.lock().await; // If this is the first scan, we want to scan all the blocks in the cache. @@ -271,12 +276,14 @@ impl Scanner { // Scan updated blocks. for i in (0..blocks_updated).rev() { let transactions = &block_cache.blocks()[i].transactions; - let amounts_received = self.scan_transactions(transactions, sub_key_checker)?; + let amounts_received = self + .scan_transactions(transactions, sub_key_checker) + .await?; trace!( "Scanned {} transactions from block {}, and found {} transactions to tracked invoices", transactions.len(), block_cache.blocks()[i].height, - amounts_received.len() + amounts_received.len(), ); let block_cache_height: u64 = block_cache.height() - i as u64; @@ -306,7 +313,7 @@ impl Scanner { &self, sub_key_checker: &SubKeyChecker<'_>, new_transactions: &[Transaction], - ) -> Result, AcceptXmrError> { + ) -> Result, ScannerError> { let mut txpool_cache = self.txpool_cache.lock().await; // Transfers previously discovered in the txpool (no reason to scan the same @@ -314,7 +321,9 @@ impl Scanner { let discovered_transfers = txpool_cache.discovered_transfers(); // Scan txpool. - let amounts_received = self.scan_transactions(new_transactions, sub_key_checker)?; + let amounts_received = self + .scan_transactions(new_transactions, sub_key_checker) + .await?; trace!( "Scanned {} transactions from txpool, and found {} transfers for tracked invoices", new_transactions.len(), @@ -349,28 +358,41 @@ impl Scanner { .collect()) } - fn scan_transactions( + async fn scan_transactions( &self, transactions: &[monero::Transaction], sub_key_checker: &SubKeyChecker<'_>, - ) -> Result>, AcceptXmrError> { + ) -> Result>, ScannerError> { let mut amounts_received = HashMap::new(); - for tx in transactions { - // Ensure the time lock is zero. - if tx.prefix().unlock_time != VarInt(0) { - debug!("Saw time locked transaction with hash {}", tx.hash()); - continue; - } - // Scan transaction for owned outputs. - let owned_outputs = tx.check_outputs_with(sub_key_checker)?; + let owned_outputs_per_tx: Vec<(monero::Hash, Vec>)> = transactions + .par_iter() + .filter(|tx| { + // Ensure the time lock is zero. + if tx.prefix().unlock_time == VarInt(0) { + true + } else { + debug!("Saw time locked transaction with hash {}", tx.hash()); + false + } + }) + .try_fold(Vec::new, |mut outputs_per_tx, tx| { + let outputs = tx.check_outputs_with(sub_key_checker)?; + outputs_per_tx.push((tx.hash(), outputs)); + Ok::>)>, ScannerError>(outputs_per_tx) + }) + .try_reduce(Vec::new, |mut outputs, mut other_outputs| { + outputs.append(&mut other_outputs); + Ok(outputs) + })?; + for (tx_hash, owned_outputs) in owned_outputs_per_tx { for output in &owned_outputs { - if !self.output_key_is_unique(output, tx.hash())? { + if !self.output_key_is_unique(output, tx_hash).await? { debug!( "Owned output #{} in transaction {} has duplicate public key.", output.index(), - tx.hash() + tx_hash ); continue; } @@ -379,19 +401,13 @@ impl Scanner { // If this invoice is being tracked, add the amount and subindex to the result // set. - if self - .store - .read() - .unwrap_or_else(PoisonError::into_inner) - .contains_sub_index(sub_index) - .map_err(|e| AcceptXmrError::Storage(Box::new(e)))? - { + if self.store.contains_sub_index(sub_index).await? { let amount = OwnedAmount { sub_index, - amount: output.amount().ok_or(AcceptXmrError::Unblind(sub_index))?, + amount: output.amount().ok_or(ScannerError::Unblind(sub_index))?, }; amounts_received - .entry(tx.hash()) + .entry(tx_hash) .or_insert_with(Vec::new) .push(amount); } @@ -404,47 +420,49 @@ impl Scanner { /// Returns `true` if the output key is unique to this output, or false if /// the key has been used by a previous output (indicating an instance of /// the burning bug). - fn output_key_is_unique( + async fn output_key_is_unique( &self, - output: &OwnedTxOut, + output: &OwnedTxOut<'_>, tx_hash: monero::Hash, - ) -> Result { + ) -> Result { let key = match output.out().target { TxOutTarget::ToKey { key } | TxOutTarget::ToTaggedKey { key, view_tag: _ } => key, - TxOutTarget::ToScript { .. } | TxOutTarget::ToScriptHash { .. } => { - return Err(AcceptXmrError::OutputTarget) - } }; let output_id = OutputId { tx_hash: tx_hash.to_bytes(), - index: u8::try_from(output.index()).map_err(|_| AcceptXmrError::OutputIndex)?, + index: u8::try_from(output.index()).map_err(|_| ScannerError::OutputIndex)?, }; - let maybe_stored_output_id = OutputKeyStorage::get( - &*self.store.read().unwrap_or_else(PoisonError::into_inner), - OutputPubKey(key), - ) - .map_err(|e| AcceptXmrError::Storage(Box::new(e)))?; + let maybe_stored_output_id = self.store.get_output_key_id(OutputPubKey(key)).await?; if let Some(stored_output_id) = maybe_stored_output_id { if stored_output_id != output_id { return Ok(false); } } else { - OutputKeyStorage::insert( - &mut *self.store.write().unwrap_or_else(PoisonError::into_inner), - OutputPubKey(key), - output_id, - ) - .map_err(|e| AcceptXmrError::Storage(Box::new(e)))?; + self.store + .insert_output_key(OutputPubKey(key), output_id) + .await?; } Ok(true) } + + pub(crate) async fn is_synchronized(&self) -> bool { + self.block_cache.lock().await.is_synchronized() + } + + pub(crate) async fn cache_height(&self) -> u64 { + self.block_cache.lock().await.height() + } + + pub(crate) async fn daemon_height(&self) -> u64 { + self.block_cache.lock().await.daemon_height() + } } -pub(crate) struct ScannerHandle(JoinHandle>); +pub(crate) struct ScannerHandle(JoinHandle>); impl ScannerHandle { - pub(crate) fn join(self) -> thread::Result> { - self.0.join() + pub(crate) async fn join(self) -> Result<(), ScannerError> { + self.0.await? } pub(crate) fn is_finished(&self) -> bool { @@ -452,8 +470,8 @@ impl ScannerHandle { } } -impl From>> for ScannerHandle { - fn from(inner: JoinHandle>) -> Self { +impl From>> for ScannerHandle { + fn from(inner: JoinHandle>) -> Self { ScannerHandle(inner) } } @@ -463,31 +481,57 @@ struct OwnedAmount { amount: Amount, } -fn last_height(store: &Arc>) -> Result, AcceptXmrError> { - match HeightStorage::get(&*store.read().unwrap_or_else(PoisonError::into_inner)) { - Ok(Some(h)) => { - info!("Last block scanned: {}", h); - return Ok(Some(h)); - } - Ok(None) => {} - Err(e) => return Err(AcceptXmrError::Storage(Box::new(e))), +async fn last_height( + store: &StorageClient, +) -> Result, ScannerError> { + if let Some(h) = store.get_height().await? { + info!("Last block scanned: {}", h); + return Ok(Some(h)); } - match store - .read() - .unwrap_or_else(PoisonError::into_inner) - .lowest_height() - { - Ok(Some(h)) => { - info!( - "Pending invoices found in AcceptXMR database. Height of lowest invoice: {}", - h - ); - return Ok(Some(h)); - } - Ok(None) => {} - Err(e) => return Err(AcceptXmrError::Storage(Box::new(e))), + // Fall back on this for compatibility in case the user's height table + // wasn't populated yet. + if let Some(h) = store.lowest_invoice_height().await? { + info!( + "Pending invoices found in AcceptXMR database. Height of lowest invoice: {}", + h + ); + return Ok(Some(h)); }; Ok(None) } + +/// Errors specific to the blockchain scanner. +#[derive(Error, Debug)] +pub enum ScannerError { + /// Failure to unblind the amount of an owned output. + #[error("unable to unblind amount of owned output sent to subaddress index {0}")] + Unblind(SubIndex), + /// An error storing/retrieving data from the storage layer. + #[error("storage error: {0}")] + Storage(#[from] StorageError), + /// Payment gateway encountered an error while trying to join the scanning + /// thread. + #[error("payment gateway encountered an error while trying to join the scanning thread: {0}")] + Threading(#[from] JoinError), + /// Failure to check if output is owned. + #[error("failed to check if output is owned: {0}")] + OwnedOutputCheck(#[from] monero::blockdata::transaction::Error), + /// Output has unsupported target. This could mean that `AcceptXMR` is + /// connected to an outdated daemon. + #[error("unsupported output target")] + OutputTarget, + /// Output index was too large. + #[error("output index too large")] + OutputIndex, + /// An error originating from a daemon RPC call. + #[error("RPC error: {0}")] + Rpc(#[from] RpcError), + /// An error with the block cache. + #[error("Block cache error: {0}")] + BlockCache(#[from] BlockCacheError), + /// An error with the txpool cache. + #[error("Txpool cache error: {0}")] + TxpoolCache(#[from] TxpoolCacheError), +} diff --git a/library/src/storage/height_storage.rs b/library/src/storage/height_storage.rs index 1562fd5..2f0def1 100644 --- a/library/src/storage/height_storage.rs +++ b/library/src/storage/height_storage.rs @@ -26,26 +26,14 @@ pub trait HeightStorage: Send + Sync { mod test { use std::fmt::{Debug, Display}; - use tempfile::Builder; use test_case::test_case; + use testing_utils::new_temp_dir; use crate::storage::{ stores::{InMemory, Sled, Sqlite}, HeightStorage, }; - fn new_temp_dir() -> String { - Builder::new() - .prefix("temp_db_") - .rand_bytes(16) - .tempdir() - .unwrap() - .path() - .to_str() - .expect("failed to get temporary directory path") - .to_string() - } - #[test_case(Sled::new(&new_temp_dir(), "invoices", "output keys", "height").unwrap(); "sled")] #[test_case(InMemory::new(); "in-memory")] #[test_case(Sqlite::new(":memory:", "invoices", "output keys", "height").unwrap(); "sqlite")] diff --git a/library/src/storage/invoice_storage.rs b/library/src/storage/invoice_storage.rs index d87e246..53d9ce8 100644 --- a/library/src/storage/invoice_storage.rs +++ b/library/src/storage/invoice_storage.rs @@ -1,5 +1,3 @@ -use std::cmp::Ordering; - use crate::{Invoice, InvoiceId, SubIndex}; /// The [`InvoiceStorage`] trait describes the invoice storage layer for @@ -7,10 +5,6 @@ use crate::{Invoice, InvoiceId, SubIndex}; pub trait InvoiceStorage: Send + Sync { /// Error type for the storage layer. type Error: std::error::Error + Send + 'static; - /// An iterator over all invoices in storage. - type Iter<'a>: Iterator> - where - Self: 'a; /// Insert invoice into storage for tracking. /// @@ -50,43 +44,48 @@ pub trait InvoiceStorage: Send + Sync { /// could not be determined. fn contains_sub_index(&self, sub_index: SubIndex) -> Result; - /// Returns an iterator over all invoices in storage. + /// Iterates over all invoices in storage, executing the supplied closure on + /// each. /// /// # Errors /// - /// Returns an error if the iterator could not be created due to an - /// underlying issue with the storage layer. - fn try_iter(&self) -> Result, Self::Error>; + /// Stops iterating and returns an error if the supplied closure returns an + /// error. + fn try_for_each(&self, f: F) -> Result<(), Self::Error> + where + F: FnMut(Result) -> Result<(), Self::Error>; - /// Recover lowest current height of an invoice in storage. + /// Returns `true` if there are no invoices in storage. + /// + /// # Errors + /// + /// Returns an error if there was an underlying issue with the storage + /// layer. + fn is_empty(&self) -> Result; + + /// Find lowest current height of an invoice in storage. /// /// # Errors /// /// Returns an error if the lowest height of an invoice could not be /// determined. fn lowest_height(&self) -> Result, Self::Error> { - self.try_iter()? - .min_by(|invoice_1, invoice_2| { - match (invoice_1, invoice_2) { - // If there is an error, we want to return it. - (Err(_), _) => Ordering::Greater, - (_, Err(_)) => Ordering::Less, - // Otherwise, return the one with the lower height. - (Ok(inv1), Ok(inv2)) => inv1.current_height().cmp(&inv2.current_height()), + let mut lowest = None; + self.try_for_each(|invoice_or_err| { + let current_height = invoice_or_err?.current_height(); + match lowest { + Some(l) if l > current_height => { + lowest = Some(current_height); } - }) - .transpose() - .map(|maybe_invoice| maybe_invoice.map(|invoice| invoice.current_height())) - } + None => { + lowest = Some(current_height); + } + Some(_) => {} + } + Ok(()) + })?; - /// Returns `true` if there are no invoices in storage. - /// - /// # Errors - /// - /// Returns an error if there was an underlying issue with the storage - /// layer. - fn is_empty(&self) -> Result { - Ok(self.try_iter()?.next().is_none()) + Ok(lowest) } } @@ -95,8 +94,8 @@ pub trait InvoiceStorage: Send + Sync { mod test { use std::fmt::{Debug, Display}; - use tempfile::Builder; use test_case::test_case; + use testing_utils::new_temp_dir; use crate::{ storage::{ @@ -106,18 +105,6 @@ mod test { Invoice, SubIndex, }; - fn new_temp_dir() -> String { - Builder::new() - .prefix("temp_db_") - .rand_bytes(16) - .tempdir() - .unwrap() - .path() - .to_str() - .expect("failed to get temporary directory path") - .to_string() - } - fn dummy_invoice() -> Invoice { Invoice::new( "4A1WSBQdCbUCqt3DaGfmqVFchXScF43M6c5r4B6JXT3dUwuALncU9XTEnRPmUMcB3c16kVP9Y7thFLCJ5BaMW3UmSy93w3w".to_string(), @@ -133,11 +120,10 @@ mod test { #[test_case(Sled::new(&new_temp_dir(), "invoices", "output keys", "height").unwrap(); "sled")] #[test_case(InMemory::new(); "in-memory")] #[test_case(Sqlite::new(":memory:", "invoices", "output keys", "height").unwrap(); "sqlite")] - fn insert_and_get<'a, S, E, I>(mut store: S) + fn insert_and_get(mut store: S) where - S: InvoiceStorage = I> + 'static, + S: InvoiceStorage + 'static, E: Debug + Display + Send, - I: Iterator, { let invoice = dummy_invoice(); store.insert(invoice.clone()).unwrap(); @@ -147,11 +133,10 @@ mod test { #[test_case(Sled::new(&new_temp_dir(), "invoices", "output keys", "height").unwrap(); "sled")] #[test_case(InMemory::new(); "in-memory")] #[test_case(Sqlite::new(":memory:", "invoices", "output keys", "height").unwrap(); "sqlite")] - fn insert_existing<'a, S, E, I>(mut store: S) + fn insert_existing(mut store: S) where - S: InvoiceStorage = I> + 'static, + S: InvoiceStorage + 'static, E: Debug + Display + Send, - I: Iterator, { let mut invoice = dummy_invoice(); @@ -168,11 +153,10 @@ mod test { #[test_case(Sled::new(&new_temp_dir(), "invoices", "output keys", "height").unwrap(); "sled")] #[test_case(InMemory::new(); "in-memory")] #[test_case(Sqlite::new(":memory:", "invoices", "output keys", "height").unwrap(); "sqlite")] - fn remove<'a, S, E, I>(mut store: S) + fn remove(mut store: S) where - S: InvoiceStorage = I> + 'static, + S: InvoiceStorage + 'static, E: Debug + Display + Send, - I: Iterator, { let invoice = dummy_invoice(); store.insert(invoice.clone()).unwrap(); @@ -185,11 +169,10 @@ mod test { #[test_case(Sled::new(&new_temp_dir(), "invoices", "output keys", "height").unwrap(); "sled")] #[test_case(InMemory::new(); "in-memory")] #[test_case(Sqlite::new(":memory:", "invoices", "output keys", "height").unwrap(); "sqlite")] - fn remove_non_existent<'a, S, E, I>(mut store: S) + fn remove_non_existent(mut store: S) where - S: InvoiceStorage = I> + 'static, + S: InvoiceStorage + 'static, E: Debug + Display + Send, - I: Iterator, { let invoice = dummy_invoice(); assert_eq!(store.get(invoice.id()).unwrap(), None); @@ -201,11 +184,10 @@ mod test { #[test_case(Sled::new(&new_temp_dir(), "invoices", "output keys", "height").unwrap(); "sled")] #[test_case(InMemory::new(); "in-memory")] #[test_case(Sqlite::new(":memory:", "invoices", "output keys", "height").unwrap(); "sqlite")] - fn update<'a, S, E, I>(mut store: S) + fn update(mut store: S) where - S: InvoiceStorage = I> + 'static, + S: InvoiceStorage + 'static, E: Debug + Display + Send, - I: Iterator, { let invoice = dummy_invoice(); store.insert(invoice.clone()).unwrap(); @@ -223,11 +205,10 @@ mod test { #[test_case(Sled::new(&new_temp_dir(), "invoices", "output keys", "height").unwrap(); "sled")] #[test_case(InMemory::new(); "in-memory")] #[test_case(Sqlite::new(":memory:", "invoices", "output keys", "height").unwrap(); "sqlite")] - fn update_empty<'a, S, E, I>(mut store: S) + fn update_empty(mut store: S) where - S: InvoiceStorage = I> + 'static, + S: InvoiceStorage + 'static, E: Debug + Display + Send, - I: Iterator, { let invoice = dummy_invoice(); assert_eq!(store.update(invoice.clone()).unwrap(), None); @@ -237,11 +218,10 @@ mod test { #[test_case(&Sled::new(&new_temp_dir(), "invoices", "output keys", "height").unwrap(); "sled")] #[test_case(&InMemory::new(); "in-memory")] #[test_case(&Sqlite::new(":memory:", "invoices", "output keys", "height").unwrap(); "sqlite")] - fn get_non_existent<'a, S, E, I>(store: &S) + fn get_non_existent(store: &S) where - S: InvoiceStorage = I> + 'static, + S: InvoiceStorage + 'static, E: Debug + Display + Send, - I: Iterator, { let invoice = dummy_invoice(); assert_eq!(store.get(invoice.id()).unwrap(), None); @@ -252,11 +232,10 @@ mod test { #[test_case(Sled::new(&new_temp_dir(), "invoices", "output keys", "height").unwrap(); "sled")] #[test_case(InMemory::new(); "in-memory")] #[test_case(Sqlite::new(":memory:", "invoices", "output keys", "height").unwrap(); "sqlite")] - fn contains_subindex<'a, S, E, I>(mut store: S) + fn contains_subindex(mut store: S) where - S: InvoiceStorage = I> + 'static, + S: InvoiceStorage + 'static, E: Debug + Display + Send, - I: Iterator, { let invoice = dummy_invoice(); store.insert(invoice).unwrap(); @@ -267,11 +246,10 @@ mod test { #[test_case(&Sled::new(&new_temp_dir(), "invoices", "output keys", "height").unwrap(); "sled")] #[test_case(&InMemory::new(); "in-memory")] #[test_case(&Sqlite::new(":memory:", "invoices", "output keys", "height").unwrap(); "sqlite")] - fn doesnt_contain_subindex<'a, S, E, I>(store: &S) + fn doesnt_contain_subindex(store: &S) where - S: InvoiceStorage = I> + 'static, + S: InvoiceStorage + 'static, E: Debug + Display + Send, - I: Iterator, { assert!(!store.contains_sub_index(SubIndex::new(123, 123)).unwrap()); } @@ -279,30 +257,79 @@ mod test { #[test_case(&mut Sled::new(&new_temp_dir(), "invoices", "output keys", "height").unwrap(); "sled")] #[test_case(&mut InMemory::new(); "in-memory")] #[test_case(&mut Sqlite::new(":memory:", "invoices", "output keys", "height").unwrap(); "sqlite")] - fn iterate<'a, S, E, I>(store: &'a mut S) + fn for_each(store: &mut S) where - S: InvoiceStorage = I>, + S: InvoiceStorage, E: Debug + Display + Send, - I: Iterator>, { let invoice = dummy_invoice(); store.insert(invoice.clone()).unwrap(); + let mut count = 0; - let mut iter = store.try_iter().unwrap(); - assert_eq!(iter.next().transpose().unwrap(), Some(invoice)); - assert_eq!(iter.next().transpose().unwrap(), None); + store + .try_for_each(|invoice_or_err| { + assert_eq!(invoice_or_err.unwrap(), invoice); + count += 1; + Ok(()) + }) + .unwrap(); + + assert_eq!(count, 1); } - #[test_case(&Sled::new(&new_temp_dir(), "invoices", "output keys", "height").unwrap(); "sled")] - #[test_case(&InMemory::new(); "in-memory")] - #[test_case(&Sqlite::new(":memory:", "invoices", "output keys", "height").unwrap(); "sqlite")] - fn iterate_empty<'a, S, E, I>(store: &'a S) + #[test_case(&mut Sled::new(&new_temp_dir(), "invoices", "output keys", "height").unwrap(); "sled")] + #[test_case(&mut InMemory::new(); "in-memory")] + #[test_case(&mut Sqlite::new(":memory:", "invoices", "output keys", "height").unwrap(); "sqlite")] + fn for_each_empty(store: &mut S) where - S: InvoiceStorage = I>, + S: InvoiceStorage, E: Debug + Display + Send, - I: Iterator>, { - let mut iter = store.try_iter().unwrap(); - assert_eq!(iter.next().transpose().unwrap(), None); + let invoice = dummy_invoice(); + let mut count = 0; + + store + .try_for_each(|invoice_or_err| { + assert_eq!(invoice_or_err.unwrap(), invoice); + count += 1; + Ok(()) + }) + .unwrap(); + + assert_eq!(count, 0); + } + + #[test_case(&mut Sled::new(&new_temp_dir(), "invoices", "output keys", "height").unwrap(); "sled")] + #[test_case(&mut InMemory::new(); "in-memory")] + #[test_case(&mut Sqlite::new(":memory:", "invoices", "output keys", "height").unwrap(); "sqlite")] + fn is_empty(store: &mut S) + where + S: InvoiceStorage, + E: Debug + Display + Send, + { + assert!(store.is_empty().unwrap()); + + let invoice = dummy_invoice(); + store.insert(invoice.clone()).unwrap(); + assert!(!store.is_empty().unwrap()); + + store.remove(invoice.id()).unwrap(); + assert!(store.is_empty().unwrap()); + } + + #[test_case(&mut Sled::new(&new_temp_dir(), "invoices", "output keys", "height").unwrap(); "sled")] + #[test_case(&mut InMemory::new(); "in-memory")] + #[test_case(&mut Sqlite::new(":memory:", "invoices", "output keys", "height").unwrap(); "sqlite")] + fn lowest_height(store: &mut S) + where + S: InvoiceStorage, + E: Debug + Display + Send, + { + assert_eq!(store.lowest_height().unwrap(), None); + + let invoice = dummy_invoice(); + store.insert(invoice.clone()).unwrap(); + + assert_eq!(store.lowest_height().unwrap(), Some(0)); } } diff --git a/library/src/storage/mod.rs b/library/src/storage/mod.rs index 3e00f8d..3e6d95b 100644 --- a/library/src/storage/mod.rs +++ b/library/src/storage/mod.rs @@ -10,7 +10,15 @@ pub mod stores; pub use height_storage::HeightStorage; pub use invoice_storage::InvoiceStorage; +use log::error; pub use output_key_storage::{OutputId, OutputKeyStorage, OutputPubKey}; +use thiserror::Error; +use tokio::sync::{ + mpsc::{self}, + oneshot, +}; + +use crate::{Invoice, InvoiceId, SubIndex}; /// A supertrait of all necessary storage traits. pub trait Storage: InvoiceStorage + OutputKeyStorage + HeightStorage { @@ -28,3 +36,364 @@ pub trait Storage: InvoiceStorage + OutputKeyStorage + HeightStorage { Ok(()) } } + +/// The storage manager takes messages from a channel and runs the corresponding +/// storage method. This allows the blocking IO to be performed on a dedicated +/// thread. +struct Manager { + store: S, + receiver: mpsc::Receiver>, +} + +impl Manager { + fn handle(&mut self, message: Method) { + match message { + // Invoice storage methods. + Method::InsertInvoice { invoice, response } => { + let id = invoice.id(); + let result = InvoiceStorage::insert(&mut self.store, invoice); + if response.send(result).is_err() { + error!( + "Failed to send InsertInvoice response to storage client. Invoice ID: {id}" + ); + }; + } + Method::RemoveInvoice { id, response } => { + let invoice = InvoiceStorage::remove(&mut self.store, id); + if response.send(invoice).is_err() { + error!( + "Failed to send GetInvoice response to storage client. Invoice ID: {id}" + ); + }; + } + Method::UpdateInvoice { invoice, response } => { + let id = invoice.id(); + let result = InvoiceStorage::update(&mut self.store, invoice); + if response.send(result).is_err() { + error!( + "Failed to send UpdateInvoice response to storage client. Invoice ID: {id}" + ); + }; + } + Method::GetInvoice { id, response } => { + let invoice = InvoiceStorage::get(&self.store, id); + if response.send(invoice).is_err() { + error!( + "Failed to send GetInvoice response to storage client. Invoice ID: {id}" + ); + }; + } + Method::ContainsSubIndex { index, response } => { + if response.send(self.store.contains_sub_index(index)).is_err() { + error!( + "Failed to send ContainsSubIndes response to storage client. Index: {}", + index + ); + } + } + Method::ForEachInvoice { f, response } => { + let result = self.store.try_for_each(f); + if response.send(result).is_err() { + error!("Failed to send ForEachInvoice response to storage client."); + }; + } + Method::LowestInvoiceHeight(response) => { + if response.send(self.store.lowest_height()).is_err() { + error!("Failed to send LowestInvoiceHeight response to storage client."); + }; + } + + Method::GetHeight(response) => { + if response.send(HeightStorage::get(&self.store)).is_err() { + error!("Failed to send GetHeight response to storage client."); + }; + } + Method::UpsertHeight { height, response } => { + if response + .send(HeightStorage::upsert(&mut self.store, height)) + .is_err() + { + error!("Failed to send UpsertHeight response to storage client."); + }; + } + + Method::GetOutputKeyId { key, response } => { + if response + .send(OutputKeyStorage::get(&self.store, key)) + .is_err() + { + error!("Failed to send GetOutputKeyId response to storage client."); + }; + } + Method::InsertOutputKey { + key, + output_id, + response, + } => { + if response + .send(OutputKeyStorage::insert(&mut self.store, key, output_id)) + .is_err() + { + error!("Failed to send InsertOutputKey response to storage client."); + }; + } + + Method::Flush(response) => { + if response.send(self.store.flush()).is_err() { + error!("Failed to send Flush response to storage client."); + }; + } + } + } +} + +enum Method { + InsertInvoice { + invoice: Invoice, + response: oneshot::Sender::Error>>, + }, + RemoveInvoice { + id: InvoiceId, + response: oneshot::Sender, ::Error>>, + }, + UpdateInvoice { + invoice: Invoice, + response: oneshot::Sender, ::Error>>, + }, + GetInvoice { + id: InvoiceId, + response: oneshot::Sender, ::Error>>, + }, + ContainsSubIndex { + index: SubIndex, + response: oneshot::Sender::Error>>, + }, + ForEachInvoice { + f: Box>, + response: oneshot::Sender::Error>>, + }, + LowestInvoiceHeight(oneshot::Sender, ::Error>>), + GetHeight(oneshot::Sender, ::Error>>), + UpsertHeight { + height: u64, + response: oneshot::Sender, ::Error>>, + }, + GetOutputKeyId { + key: OutputPubKey, + response: oneshot::Sender, ::Error>>, + }, + InsertOutputKey { + key: OutputPubKey, + output_id: OutputId, + response: oneshot::Sender::Error>>, + }, + Flush(oneshot::Sender::Error>>), +} + +type ForEachClosure = dyn FnMut(Result::Error>) -> Result<(), ::Error> + + Send; + +pub(crate) struct Client(mpsc::Sender>); + +impl Client { + pub(crate) fn new(store: S) -> Self { + let (sender, receiver) = mpsc::channel(64); + let mut manager = Manager { store, receiver }; + + tokio::spawn(async move { + while let Some(message) = manager.receiver.recv().await { + manager.handle(message); + } + }); + + Self(sender) + } + + pub(crate) async fn insert_invoice(&self, invoice: Invoice) -> Result<(), StorageError> { + let (sender, receiver) = oneshot::channel(); + self.0 + .send(Method::InsertInvoice { + invoice, + response: sender, + }) + .await + .map_err(|e| StorageError::Send(Box::new(e)))?; + let response = receiver.await.map_err(|_| StorageError::Receive)?; + response.map_err(|e| StorageError::Internal(Box::new(e))) + } + + pub(crate) async fn remove_invoice( + &self, + id: InvoiceId, + ) -> Result, StorageError> { + let (sender, receiver) = oneshot::channel(); + self.0 + .send(Method::RemoveInvoice { + id, + response: sender, + }) + .await + .map_err(|e| StorageError::Send(Box::new(e)))?; + let response = receiver.await.map_err(|_| StorageError::Receive)?; + response.map_err(|e| StorageError::Internal(Box::new(e))) + } + + pub(crate) async fn update_invoice( + &self, + invoice: Invoice, + ) -> Result, StorageError> { + let (sender, receiver) = oneshot::channel(); + self.0 + .send(Method::UpdateInvoice { + invoice, + response: sender, + }) + .await + .map_err(|e| StorageError::Send(Box::new(e)))?; + let response = receiver.await.map_err(|_| StorageError::Receive)?; + response.map_err(|e| StorageError::Internal(Box::new(e))) + } + + pub(crate) async fn get_invoice(&self, id: InvoiceId) -> Result, StorageError> { + let (sender, receiver) = oneshot::channel(); + self.0 + .send(Method::GetInvoice { + id, + response: sender, + }) + .await + .map_err(|e| StorageError::Send(Box::new(e)))?; + let response = receiver.await.map_err(|_| StorageError::Receive)?; + response.map_err(|e| StorageError::Internal(Box::new(e))) + } + + pub(crate) async fn contains_sub_index(&self, index: SubIndex) -> Result { + let (sender, receiver) = oneshot::channel(); + self.0 + .send(Method::ContainsSubIndex { + index, + response: sender, + }) + .await + .map_err(|e| StorageError::Send(Box::new(e)))?; + let response = receiver.await.map_err(|_| StorageError::Receive)?; + response.map_err(|e| StorageError::Internal(Box::new(e))) + } + + pub(crate) async fn try_for_each_invoice(&self, f: F) -> Result<(), StorageError> + where + F: FnMut( + Result::Error>, + ) -> Result<(), ::Error> + + Send + + 'static, + { + let (sender, receiver) = oneshot::channel(); + self.0 + .send(Method::ForEachInvoice { + f: Box::new(f), + response: sender, + }) + .await + .map_err(|e| StorageError::Send(Box::new(e)))?; + let response = receiver.await.map_err(|_| StorageError::Receive)?; + response.map_err(|e| StorageError::Internal(Box::new(e))) + } + + pub(crate) async fn lowest_invoice_height(&self) -> Result, StorageError> { + let (sender, receiver) = oneshot::channel(); + self.0 + .send(Method::LowestInvoiceHeight(sender)) + .await + .map_err(|e| StorageError::Send(Box::new(e)))?; + let response = receiver.await.map_err(|_| StorageError::Receive)?; + response.map_err(|e| StorageError::Internal(Box::new(e))) + } + + pub(crate) async fn get_height(&self) -> Result, StorageError> { + let (sender, receiver) = oneshot::channel(); + self.0 + .send(Method::GetHeight(sender)) + .await + .map_err(|e| StorageError::Send(Box::new(e)))?; + let response = receiver.await.map_err(|_| StorageError::Receive)?; + response.map_err(|e| StorageError::Internal(Box::new(e))) + } + + pub(crate) async fn upsert_height(&self, height: u64) -> Result, StorageError> { + let (sender, receiver) = oneshot::channel(); + self.0 + .send(Method::UpsertHeight { + height, + response: sender, + }) + .await + .map_err(|e| StorageError::Send(Box::new(e)))?; + let response = receiver.await.map_err(|_| StorageError::Receive)?; + response.map_err(|e| StorageError::Internal(Box::new(e))) + } + + pub(crate) async fn get_output_key_id( + &self, + key: OutputPubKey, + ) -> Result, StorageError> { + let (sender, receiver) = oneshot::channel(); + self.0 + .send(Method::GetOutputKeyId { + key, + response: sender, + }) + .await + .map_err(|e| StorageError::Send(Box::new(e)))?; + let response = receiver.await.map_err(|_| StorageError::Receive)?; + response.map_err(|e| StorageError::Internal(Box::new(e))) + } + + pub(crate) async fn insert_output_key( + &self, + key: OutputPubKey, + output_id: OutputId, + ) -> Result<(), StorageError> { + let (sender, receiver) = oneshot::channel(); + self.0 + .send(Method::InsertOutputKey { + key, + output_id, + response: sender, + }) + .await + .map_err(|e| StorageError::Send(Box::new(e)))?; + let response = receiver.await.map_err(|_| StorageError::Receive)?; + response.map_err(|e| StorageError::Internal(Box::new(e))) + } + + pub(crate) async fn flush(&self) -> Result<(), StorageError> { + let (sender, receiver) = oneshot::channel(); + self.0 + .send(Method::Flush(sender)) + .await + .map_err(|e| StorageError::Send(Box::new(e)))?; + let response = receiver.await.map_err(|_| StorageError::Receive)?; + response.map_err(|e| StorageError::Internal(Box::new(e))) + } +} + +impl Clone for Client { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +/// An error occurring while storing or retrieving values from a database. +#[derive(Error, Debug)] +pub enum StorageError { + /// Failed to send message to the storage manager. + #[error("failed to send message to the storage manager: {0}")] + Send(Box), + /// Failed to receive result from the storage manager. + #[error("failed to receive result from the storage manager")] + Receive, + /// An error caused by the database, or some interaction with it. + #[error(transparent)] + Internal(Box), +} diff --git a/library/src/storage/output_key_storage.rs b/library/src/storage/output_key_storage.rs index 64c2117..b27264a 100644 --- a/library/src/storage/output_key_storage.rs +++ b/library/src/storage/output_key_storage.rs @@ -55,26 +55,14 @@ pub struct OutputId { mod test { use std::fmt::{Debug, Display}; - use tempfile::Builder; use test_case::test_case; + use testing_utils::new_temp_dir; use crate::storage::{ stores::{InMemory, Sled, Sqlite}, OutputId, OutputKeyStorage, OutputPubKey, }; - fn new_temp_dir() -> String { - Builder::new() - .prefix("temp_db_") - .rand_bytes(16) - .tempdir() - .unwrap() - .path() - .to_str() - .expect("failed to get temporary directory path") - .to_string() - } - fn dummy_key() -> OutputPubKey { OutputPubKey([0; 32]) } diff --git a/library/src/storage/stores/in_memory.rs b/library/src/storage/stores/in_memory.rs index 2573a06..9b0367b 100644 --- a/library/src/storage/stores/in_memory.rs +++ b/library/src/storage/stores/in_memory.rs @@ -1,7 +1,4 @@ -use std::collections::{ - btree_map::{self, Entry}, - BTreeMap, -}; +use std::collections::{btree_map::Entry, BTreeMap}; use thiserror::Error; @@ -40,7 +37,6 @@ impl Default for InMemory { impl InvoiceStorage for InMemory { type Error = InMemoryStorageError; - type Iter<'a> = InMemoryIter<'a>; fn insert(&mut self, invoice: Invoice) -> Result<(), Self::Error> { if self.invoices.contains_key(&invoice.id()) { @@ -73,19 +69,17 @@ impl InvoiceStorage for InMemory { .is_some()) } - fn try_iter(&self) -> Result, InMemoryStorageError> { - let iter = self.invoices.values(); - Ok(InMemoryIter(iter)) + fn try_for_each(&self, mut f: F) -> Result<(), Self::Error> + where + F: FnMut(Result) -> Result<(), Self::Error>, + { + self.invoices + .iter() + .try_for_each(move |(_, invoice)| f(Ok(invoice.clone()))) } -} - -pub struct InMemoryIter<'a>(btree_map::Values<'a, InvoiceId, Invoice>); - -impl<'a> Iterator for InMemoryIter<'a> { - type Item = Result; - fn next(&mut self) -> Option { - self.0.next().map(|v| Ok(v.clone())) + fn is_empty(&self) -> Result { + Ok(self.invoices.is_empty()) } } diff --git a/library/src/storage/stores/sled.rs b/library/src/storage/stores/sled.rs index 26f166e..a24f11b 100644 --- a/library/src/storage/stores/sled.rs +++ b/library/src/storage/stores/sled.rs @@ -60,7 +60,6 @@ impl Sled { impl InvoiceStorage for Sled { type Error = SledStorageError; - type Iter<'a> = SledIter; fn insert(&mut self, invoice: Invoice) -> Result<(), SledStorageError> { // Prepare key (invoice id). @@ -147,8 +146,20 @@ impl InvoiceStorage for Sled { Ok(self.invoices.scan_prefix(key).next().is_some()) } - fn try_iter(&self) -> Result, SledStorageError> { - Ok(SledIter(self.invoices.iter())) + fn try_for_each(&self, mut f: F) -> Result<(), Self::Error> + where + F: FnMut(Result) -> Result<(), Self::Error>, + { + self.invoices.iter().try_for_each(move |row| { + let invoice_or_err = match row { + Ok((_id, ivec)) => bincode::decode_from_slice(&ivec, bincode::config::standard()) + .map(|v| v.0) + .map_err(SledStorageError::Deserialize), + Err(e) => Err(SledStorageError::Database(e.into())), + }; + + f(invoice_or_err) + }) } fn is_empty(&self) -> Result { @@ -156,25 +167,6 @@ impl InvoiceStorage for Sled { } } -pub struct SledIter(sled::Iter); - -impl Iterator for SledIter { - type Item = Result; - - fn next(&mut self) -> Option { - match self.0.next()? { - Ok((_, value)) => { - let invoice_or_err = - bincode::decode_from_slice(&value, bincode::config::standard()) - .map(|v| v.0) - .map_err(SledStorageError::Deserialize); - Some(invoice_or_err) - } - Err(e) => Some(Err(SledStorageError::Database(e.into()))), - } - } -} - impl OutputKeyStorage for Sled { type Error = SledStorageError; diff --git a/library/src/storage/stores/sqlite.rs b/library/src/storage/stores/sqlite.rs index 024c95e..297878b 100644 --- a/library/src/storage/stores/sqlite.rs +++ b/library/src/storage/stores/sqlite.rs @@ -1,7 +1,7 @@ use std::fmt::Display; use log::{debug, trace, warn}; -use sqlite::{version, Connection, ConnectionWithFullMutex, CursorWithOwnership, State, Value}; +use sqlite::{version, Connection, ConnectionThreadSafe, State, Value}; use thiserror::Error; use crate::{ @@ -11,7 +11,7 @@ use crate::{ /// `SQLite` database. pub struct Sqlite { - db: ConnectionWithFullMutex, + db: ConnectionThreadSafe, invoices: TableName, output_keys: TableName, height: TableName, @@ -31,7 +31,7 @@ impl Sqlite { output_key_table: &str, height_table: &str, ) -> Result { - let db = Connection::open_with_full_mutex(path)?; + let db = Connection::open_thread_safe(path)?; debug!("Connection to SQLite v{} database established", version()); let invoices = TableName::new(invoice_table); @@ -75,7 +75,6 @@ impl Sqlite { impl InvoiceStorage for Sqlite { type Error = SqliteStorageError; - type Iter<'a> = SqliteIter<'a>; fn insert(&mut self, invoice: Invoice) -> Result<(), SqliteStorageError> { let invoice_id = invoice.id(); @@ -116,12 +115,12 @@ impl InvoiceStorage for Sqlite { fn remove(&mut self, invoice_id: InvoiceId) -> Result, SqliteStorageError> { let mut statement = self.db.prepare( - format!( - "DELETE FROM {} - WHERE major_subindex = :major AND minor_subindex = :minor AND creation_height = :height RETURNING invoice", - self.invoices - ) - )?; + format!( + "DELETE FROM {} + WHERE major_subindex = :major AND minor_subindex = :minor AND creation_height = :height RETURNING invoice", + self.invoices + ) + )?; statement.bind::<&[(_, Value)]>( &[ // Cast to i64 is needed because `Value` doesn't support u32. @@ -260,41 +259,55 @@ impl InvoiceStorage for Sqlite { let count = select_stmt.read::(0)?; if select_stmt.next()? != State::Done { warn!( - "Invoice query returned more than one row: {:?}", - select_stmt.read::("invoice")? + "Subaddress index query returned more than one row: {:?}", + select_stmt.read::(0)? ); } Ok(count > 0) } - fn try_iter(&self) -> Result, SqliteStorageError> { + fn try_for_each(&self, mut f: F) -> Result<(), Self::Error> + where + F: FnMut(Result) -> Result<(), Self::Error>, + { let statement = self .db .prepare(format!("SELECT invoice FROM {}", self.invoices))?; - Ok(SqliteIter(statement.into_iter())) + + statement.into_iter().try_for_each(move |item| { + let invoice_or_err = match item { + Ok(row) => match row.try_read("invoice") { + Ok(value) => bincode::decode_from_slice(value, bincode::config::standard()) + .map(|v| v.0) + .map_err(SqliteStorageError::Deserialize), + Err(e) => Err(SqliteStorageError::from(e)), + }, + Err(e) => Err(SqliteStorageError::Database(e)), + }; + + f(invoice_or_err) + }) } -} -pub struct SqliteIter<'a>(CursorWithOwnership<'a>); - -impl<'stmt> Iterator for SqliteIter<'stmt> { - type Item = Result; - - fn next(&mut self) -> Option { - match self.0.next()? { - Ok(row) => { - let value = match row.try_read("invoice") { - Ok(v) => v, - Err(e) => return Some(Err(SqliteStorageError::from(e))), - }; - let invoice_or_err = bincode::decode_from_slice(value, bincode::config::standard()) - .map(|v| v.0) - .map_err(SqliteStorageError::Deserialize); - Some(invoice_or_err) - } - Err(e) => Some(Err(SqliteStorageError::Database(e))), + fn is_empty(&self) -> Result { + let mut statement = self + .db + .prepare(format!("SELECT EXISTS (SELECT 1 FROM {})", self.invoices))?; + + if statement.next()? == State::Done { + debug!("Query determining if DB is empty returned no results."); + return Ok(true); + } + + let is_empty = statement.read::(0)?; + if statement.next()? != State::Done { + warn!( + "Invoice query returned more than one row: {:?}", + statement.read::(0)? + ); } + Ok(is_empty == 0) } } diff --git a/library/tests/common/mod.rs b/library/tests/common/mod.rs index 19ec045..3d73ee7 100644 --- a/library/tests/common/mod.rs +++ b/library/tests/common/mod.rs @@ -1,341 +1,14 @@ -use std::{cmp::max, collections::HashMap, fs, ops::Deref, sync::Mutex, time::Duration}; +use std::time::Duration; use acceptxmr::{ storage::{ stores::{InMemory, Sled, Sqlite}, Storage, }, - Invoice, PaymentGatewayBuilder, SubIndex, + PaymentGatewayBuilder, SubIndex, }; -use httpmock::{Mock, MockServer}; -use log::LevelFilter; -use serde_json::{json, Value}; -use tempfile::Builder; use test_case::test_case; - -pub const PRIVATE_VIEW_KEY: &str = - "ad2093a5705b9f33e6f0f0c1bc1f5f639c756cdfc168c8f2ac6127ccbdab3a03"; -pub const PRIMARY_ADDRESS: &str = - "4613YiHLM6JMH4zejMB2zJY5TwQCxL8p65ufw8kBP5yxX9itmuGLqp1dS4tkVoTxjyH3aYhYNrtGHbQzJQP5bFus3KHVdmf"; - -pub fn new_temp_dir() -> String { - Builder::new() - .prefix("temp_db_") - .rand_bytes(16) - .tempdir() - .expect("failed to generate temporary directory") - .path() - .to_str() - .expect("failed to get temporary directory path") - .to_string() -} - -pub fn init_logger() { - let _ = env_logger::builder() - .filter_level(LevelFilter::Warn) - .filter_module("acceptxmr", log::LevelFilter::Trace) - .is_test(true) - .try_init(); -} - -#[derive(Clone)] -pub struct MockInvoice { - pub address: Option, - pub index: SubIndex, - pub creation_height: u64, - pub amount_requested: u64, - pub amount_paid: u64, - pub paid_height: Option, - pub confirmations_required: u64, - pub current_height: u64, - pub expiration_height: u64, - pub description: String, - - // Calculated fields. - pub is_expired: bool, - pub expires_in: u64, - pub is_confirmed: bool, - pub confirmations: Option, -} - -impl MockInvoice { - pub fn new( - address: Option, - index: SubIndex, - creation_height: u64, - amount_requested: u64, - confirmations_required: u64, - expires_in: u64, - description: String, - ) -> MockInvoice { - MockInvoice { - address, - index, - creation_height, - amount_requested, - amount_paid: 0, - paid_height: None, - confirmations_required, - current_height: creation_height, - expiration_height: creation_height + expires_in, - description, - - is_expired: false, - expires_in, - is_confirmed: false, - confirmations: None, - } - } - - pub fn assert_eq(&self, update: &Invoice) { - if let Some(address) = &self.address { - assert_eq!(update.address(), address); - } - assert_eq!(update.index(), self.index); - assert_eq!(update.creation_height(), self.creation_height); - assert_eq!(update.amount_requested(), self.amount_requested); - assert_eq!(update.amount_paid(), self.amount_paid); - assert_eq!(update.confirmations_required(), self.confirmations_required); - assert_eq!(update.current_height(), self.current_height); - assert_eq!(update.expiration_height(), self.expiration_height); - assert_eq!(update.expiration_height(), self.expiration_height); - assert_eq!( - update.expiration_height() - max(update.creation_height(), update.current_height()), - self.expires_in - ); - assert_eq!(update.description(), self.description); - - // Calculated fields. - assert_eq!(update.is_expired(), self.is_expired); - assert_eq!(update.expiration_in(), self.expires_in); - assert_eq!(update.is_confirmed(), self.is_confirmed); - assert_eq!(update.confirmations(), self.confirmations); - } -} - -pub struct MockDaemon { - server: MockServer, - daemon_height_id: Mutex>, - block_ids: Mutex>, - txpool_id: Mutex>, - txpool_hashes_id: Mutex>, - txpool_transactions_id: Mutex>, -} - -impl Deref for MockDaemon { - type Target = MockServer; - - fn deref(&self) -> &MockServer { - &self.server - } -} - -impl MockDaemon { - pub async fn new_mock_daemon() -> MockDaemon { - let mock_daemon = MockDaemon { - server: MockServer::start_async().await, - daemon_height_id: Mutex::new(None), - block_ids: Mutex::new(HashMap::new()), - txpool_id: Mutex::new(None), - txpool_hashes_id: Mutex::new(None), - txpool_transactions_id: Mutex::new(None), - }; - // Mock daemon height request. - mock_daemon.mock_daemon_height(2477657); - // Mock txpool request. - mock_daemon.mock_txpool("tests/rpc_resources/txpools/txpool.json"); - - // Mock blocks. - for i in 2477647..2477666 { - // Mock block requests. - let response_path = - "tests/rpc_resources/blocks/".to_owned() + &i.to_string() + "/block.json"; - mock_daemon.mock_block(i, &response_path); - - // Skip block 2477661 when mocking transactions, because it has none. - if i == 2477661 { - continue; - } - - // Mock block transaction requests. - let request_path = - "tests/rpc_resources/blocks/".to_owned() + &i.to_string() + "/txs_hashes_0.json"; - let response_path = - "tests/rpc_resources/blocks/".to_owned() + &i.to_string() + "/transactions_0.json"; - mock_daemon.mock_transactions(&request_path, &response_path); - } - mock_daemon - } - - pub fn mock_daemon_height(&self, height: u64) -> Mock { - // Use mock ID to delete old daemon height mock. - if let Some(id) = *self - .daemon_height_id - .lock() - .expect("PoisonError when reading daemon height mock ID") - { - Mock::new(id, self).delete(); - }; - - // Create the new daemon height mock. - let mock = self.mock(|when, then| { - when.path("/json_rpc") - .body(r#"{"jsonrpc":"2.0","id":"0","method":"get_block_count"}"#); - then.status(200) - .header("content-type", "application/json") - .json_body(json!({ - "id": "0", - "jsonrpc": "2.0", - "result": { - "count": height, - "status": "OK" - } - })); - }); - *self - .daemon_height_id - .lock() - .expect("PoisonError when writing daemon height mock ID") = Some(mock.id); - mock - } - - pub fn mock_alt_2477657(&self) { - // Mock block requests. - let response_path = "tests/rpc_resources/blocks/2477657_alt/block.json"; - self.mock_block(2477657, response_path); - - // Mock block transaction requests. - let request_path = "tests/rpc_resources/blocks/2477657_alt/txs_hashes_0.json"; - let response_path = "tests/rpc_resources/blocks/2477657_alt/transactions_0.json"; - self.mock_transactions(request_path, response_path); - } - - pub fn mock_alt_2477658(&self) { - // Mock block requests. - let response_path = "tests/rpc_resources/blocks/2477658_alt/block.json"; - self.mock_block(2477658, response_path); - - // Mock block transaction requests. - let request_path = "tests/rpc_resources/blocks/2477658_alt/txs_hashes_0.json"; - let response_path = "tests/rpc_resources/blocks/2477658_alt/transactions_0.json"; - self.mock_transactions(request_path, response_path); - } - - pub fn mock_txpool(&self, path: &str) -> Mock { - // Use ID to delete old mock. - if let Some(id) = *self - .txpool_id - .lock() - .expect("PoisonError when reading txpool mock ID") - { - Mock::new(id, self).delete(); - }; - - // Create new mock. - let mock = self.mock(|when, then| { - when.path("/get_transaction_pool").body(""); - then.status(200) - .header("content-type", "application/json") - .body_from_file(path); - }); - *self - .txpool_id - .lock() - .expect("PoisonError when writing txpool mock ID") = Some(mock.id); - mock - } - - pub fn mock_transactions(&self, request_path: &str, response_path: &str) -> Mock { - let when_body: Value = serde_json::from_str( - &fs::read_to_string(request_path) - .expect("failed to read transaction request from file when preparing mock"), - ) - .expect("failed to parse transaction request as json"); - let when_txs: Vec<&str> = when_body["txs_hashes"] - .as_array() - .into_iter() - .flatten() - .map(|v| v.as_str().expect("failed to parse tx hash as string")) - .collect(); - self.mock(|when, then| { - let mut when = when.path("/get_transactions"); - for hash in when_txs { - // Ensure the request contains the hashes of all the expected transactions. - when = when.body_contains(hash); - } - then.status(200) - .header("content-type", "application/json") - .body_from_file(response_path); - }) - } - - pub fn mock_block(&self, height: u64, response_path: &str) { - // Use ID to delete old mock. - if let Some(id) = self - .block_ids - .lock() - .expect("PoisonError when reading txpool mock ID") - .get(&height) - { - Mock::new(*id, self).delete(); - }; - let mock = self.mock(|when, then| { - when.path("/json_rpc").body( - r#"{"jsonrpc":"2.0","id":"0","method":"get_block","params":{"height":"#.to_owned() - + &height.to_string() - + "}}", - ); - then.status(200) - .header("content-type", "application/json") - .body_from_file(response_path); - }); - self.block_ids - .lock() - .expect("PoisonError when writing daemon height mock ID") - .insert(height, mock.id); - } - - pub fn mock_txpool_hashes(&self, response_path: &str) -> Mock { - // Use ID to delete old mock. - if let Some(id) = *self - .txpool_hashes_id - .lock() - .expect("PoisonError when reading txpool hashes mock ID") - { - Mock::new(id, self).delete(); - }; - - // Create new mock. - let mock = self.mock(|when, then| { - when.path("/get_transaction_pool_hashes").body(""); - then.status(200) - .header("content-type", "application/json") - .body_from_file(response_path); - }); - *self - .txpool_hashes_id - .lock() - .expect("PoisonError when writing txpool hashes mock ID") = Some(mock.id); - mock - } - - pub fn mock_txpool_transactions(&self, request_path: &str, response_path: &str) -> Mock { - // Use ID to delete old mock. - if let Some(id) = *self - .txpool_transactions_id - .lock() - .expect("PoisonError when reading txpool transactions mock ID") - { - Mock::new(id, self).delete(); - }; - let mock = self.mock_transactions(request_path, response_path); - *self - .txpool_transactions_id - .lock() - .expect("PoisonError when writing txpool transactions mock ID") = Some(mock.id); - mock - } -} +use testing_utils::{init_logger, new_temp_dir, MockDaemon, PRIMARY_ADDRESS, PRIVATE_VIEW_KEY}; #[test_case(Sled::new(&new_temp_dir(), "invoices", "output keys", "height").unwrap(); "sled")] #[test_case(InMemory::new(); "in-memory")] @@ -361,6 +34,7 @@ where .account_index(1) .seed(1) .build() + .await .expect("failed to build payment gateway"); // Run it. @@ -372,6 +46,7 @@ where // Add the invoice. let invoice_id = payment_gateway .new_invoice(1, 5, 10, "test invoice".to_string()) + .await .expect("failed to add new invoice to payment gateway for tracking"); let mut subscriber = payment_gateway .subscribe(invoice_id) diff --git a/library/tests/integration_tests/block_cache.rs b/library/tests/integration_tests/block_cache.rs index c269042..1e25a70 100644 --- a/library/tests/integration_tests/block_cache.rs +++ b/library/tests/integration_tests/block_cache.rs @@ -8,8 +8,7 @@ use acceptxmr::{ PaymentGatewayBuilder, SubIndex, }; use test_case::test_case; - -use crate::common::{ +use testing_utils::{ init_logger, new_temp_dir, MockDaemon, MockInvoice, PRIMARY_ADDRESS, PRIVATE_VIEW_KEY, }; @@ -37,6 +36,7 @@ where .account_index(1) .seed(1) .build() + .await .expect("failed to build payment gateway"); // Run it. @@ -47,7 +47,8 @@ where // Add the invoice. let invoice_id = payment_gateway - .new_invoice(70000000, 2, 7, "invoice".to_string()) + .new_invoice(70_000_000, 2, 7, "invoice".to_string()) + .await .expect("failed to add new invoice to payment gateway for tracking"); let mut subscriber = payment_gateway .subscribe(invoice_id) @@ -63,8 +64,8 @@ where let mut expected = MockInvoice::new( Some(update.address().to_string()), SubIndex::new(1, 97), - 2477657, - 70000000, + 2_477_657, + 70_000_000, 2, 7, "invoice".to_string(), @@ -73,7 +74,7 @@ where // Check that it is as expected. expected.assert_eq(&update); - mock_daemon.mock_daemon_height(2477658); + mock_daemon.mock_daemon_height(2_477_658); let update = subscriber .recv_timeout(Duration::from_secs(120)) @@ -81,9 +82,9 @@ where .expect("timeout waiting for invoice update") .expect("subscription channel is closed"); - expected.amount_paid = 37419570; + expected.amount_paid = 37_419_570; expected.expires_in = 6; - expected.current_height = 2477658; + expected.current_height = 2_477_658; expected.assert_eq(&update); // Reorg to invalidate payment. @@ -95,7 +96,7 @@ where .await .expect_err("should not have received an update, but did"); - mock_daemon.mock_daemon_height(24776659); + mock_daemon.mock_daemon_height(24_776_659); let update = subscriber .recv_timeout(Duration::from_secs(120)) @@ -105,6 +106,6 @@ where expected.amount_paid = 0; expected.expires_in = 5; - expected.current_height = 2477659; + expected.current_height = 2_477_659; expected.assert_eq(&update); } diff --git a/library/tests/integration_tests/invoice_tracking.rs b/library/tests/integration_tests/invoice_tracking.rs index 47d7496..7379e23 100644 --- a/library/tests/integration_tests/invoice_tracking.rs +++ b/library/tests/integration_tests/invoice_tracking.rs @@ -9,8 +9,7 @@ use acceptxmr::{ }; use monero::consensus::deserialize; use test_case::test_case; - -use crate::common::{ +use testing_utils::{ init_logger, new_temp_dir, MockDaemon, MockInvoice, PRIMARY_ADDRESS, PRIVATE_VIEW_KEY, }; @@ -36,6 +35,7 @@ where .scan_interval(Duration::from_millis(100)) .daemon_url(mock_daemon.url("")) .build() + .await .expect("failed to build payment gateway"); // Run it. @@ -47,6 +47,7 @@ where // Add the invoice. let invoice_id = payment_gateway .new_invoice(1, 5, 10, "test invoice".to_string()) + .await .expect("failed to add new invoice to payment gateway for tracking"); let mut subscriber = payment_gateway .subscribe(invoice_id) @@ -86,7 +87,6 @@ async fn default_account_index(store: S) where S: Storage + 'static, { - // Setup. init_logger(); let mock_daemon = MockDaemon::new_mock_daemon().await; @@ -101,6 +101,7 @@ where .daemon_url(mock_daemon.url("")) .seed(1) .build() + .await .expect("failed to build payment gateway"); // Run it. @@ -112,6 +113,7 @@ where // Add the invoice. let invoice_id = payment_gateway .new_invoice(1, 5, 10, "test invoice".to_string()) + .await .expect("failed to add new invoice to payment gateway for tracking"); let mut subscriber = payment_gateway .subscribe(invoice_id) @@ -127,7 +129,7 @@ where let mut expected = MockInvoice::new( Some(update.address().to_string()), SubIndex::new(0, 97), - 2477657, + 2_477_657, 1, 5, 10, @@ -145,21 +147,25 @@ where ); // Add transfer to txpool. - let _transactions_mock = mock_daemon.mock_transactions( - "tests/rpc_resources/transactions/hashes_with_payment_account_0.json", - "tests/rpc_resources/transactions/txs_with_payment_account_0.json", + let txpool_hashes_mock = mock_daemon.mock_txpool_hashes( + "../testing-utils/rpc_resources/txpools/hashes_with_payment_account_0.json", + ); + let txpool_transactions_mock = mock_daemon.mock_txpool_transactions( + "../testing-utils/rpc_resources/transactions/hashes_with_payment_account_0.json", + "../testing-utils/rpc_resources/transactions/txs_with_payment_account_0.json", ); - let _txpool_hashes_mock = mock_daemon - .mock_txpool_hashes("tests/rpc_resources/txpools/hashes_with_payment_account_0.json"); // Get update. let update = subscriber - .recv_timeout(Duration::from_secs(120)) + .recv_timeout(Duration::from_secs(5)) .await .expect("timeout waiting for invoice update") .expect("subscription channel is closed"); - expected.amount_paid = 1468383460; + assert!(txpool_hashes_mock.hits() > 0); + assert!(txpool_transactions_mock.hits() > 0); + + expected.amount_paid = 1_468_383_460; expected.confirmations = Some(0); expected.assert_eq(&update); } @@ -188,6 +194,7 @@ where .account_index(1) .seed(1) .build() + .await .expect("failed to build payment gateway"); // Run it. @@ -198,7 +205,8 @@ where // Add the invoice. let invoice_id = payment_gateway - .new_invoice(37419570, 0, 10, "test invoice".to_string()) + .new_invoice(37_419_570, 0, 10, "test invoice".to_string()) + .await .expect("failed to add new invoice to payment gateway for tracking"); let mut subscriber = payment_gateway .subscribe(invoice_id) @@ -214,8 +222,8 @@ where let mut expected = MockInvoice::new( Some(update.address().to_string()), SubIndex::new(1, 97), - 2477657, - 37419570, + 2_477_657, + 37_419_570, 0, 10, "test invoice".to_string(), @@ -225,8 +233,8 @@ where expected.assert_eq(&update); // Add transfer to txpool. - let _txpool_hashes_mock = - mock_daemon.mock_txpool_hashes("tests/rpc_resources/txpools/hashes_with_payment.json"); + let _txpool_hashes_mock = mock_daemon + .mock_txpool_hashes("../testing-utils/rpc_resources/txpools/hashes_with_payment.json"); // Get update. let update = subscriber @@ -235,7 +243,7 @@ where .expect("timeout waiting for invoice update") .expect("subscription channel is closed"); - expected.amount_paid = 37419570; + expected.amount_paid = 37_419_570; expected.confirmations = Some(0); expected.is_confirmed = true; expected.assert_eq(&update); @@ -264,6 +272,7 @@ where .daemon_url(mock_daemon.url("")) .seed(1) .build() + .await .expect("failed to build payment gateway"); // Run it. @@ -275,6 +284,7 @@ where // Add the invoice. let invoice_id = payment_gateway .new_invoice(123, 1, 1, "test invoice".to_string()) + .await .expect("failed to add new invoice to payment gateway for tracking"); let mut subscriber = payment_gateway .subscribe(invoice_id) @@ -290,7 +300,7 @@ where let expected = MockInvoice::new( Some(update.address().to_string()), SubIndex::new(0, 97), - 2477657, + 2_477_657, 123, 1, 1, @@ -301,11 +311,12 @@ where expected.assert_eq(&update); // Add transfer to txpool. - let _txpool_hashes_mock = mock_daemon - .mock_txpool_hashes("tests/rpc_resources/txpools/hashes_with_payment_timelock.json"); + let _txpool_hashes_mock = mock_daemon.mock_txpool_hashes( + "../testing-utils/rpc_resources/txpools/hashes_with_payment_timelock.json", + ); let _transactions_mock = mock_daemon.mock_transactions( - "tests/rpc_resources/transactions/hashes_with_payment_timelock.json", - "tests/rpc_resources/transactions/txs_with_payment_timelock.json", + "../testing-utils/rpc_resources/transactions/hashes_with_payment_timelock.json", + "../testing-utils/rpc_resources/transactions/txs_with_payment_timelock.json", ); // There shouldn't be any update. @@ -330,7 +341,12 @@ where let tx_hex = hex::decode("02000202000bde84861298e38c01c99338d5b005cab9038d26d8e301d442e304a419f406af616b0e66f90a5ff643ee8ff496a25e2caf3d8fccf65895073212a795ae7a7c02000ba09ef20f9da08e02dcb4a301eba60def810cc3b20e9e8801bf9101cdcf04f6ae02f7a50118130528cdc0df212e936b5c95b35ebcdecd443000e94b637782f47a2a47171c0300026f5955625996a0051ff3a419b5fe967fd4a691ad99b2e7dce6fcb35d21821f0d0002b024664bccba2226849a95dfe653c64e02c5498ea88265457c44f85778f018f700027fd35731e1bd4d1ef4bb1f0f008c4b0ba54bb214bb3304fab61c2b5e3a8570f4830101349587e7557632f77ab342e1fb69dc1d106900bc3557e45b9f18d117be363bbc0403909dd7c66c8dba74062fa6d53bcbaa9c3c3a2481dd23d27be3b178449a33303aa17cbf28ae53a1ed402ba4bda9466b92471ce9ddafd5c2ae6e07470ed4d210728a20e76551ee2b320b3fac21c6d823bd6bf2c645e039e226290bc427cc3621930590d99107d6f209fb953c4370924e52a5b1b3df48fbf8306474b69d0998c657aa6aeb1f2f2ee09d5c8e8acf0a32a76aa7f949305829b5a35d53a8b7f72a9960e05ce91198e32bb1f5b049ae2f09aba0975fca8df6f176c947f70170e1514c5c6a75bfcea627e9fcedc7d75fc4a8440b5c331ff2b8a06b0d9c01aca9a1016700a9b4ca3f1f52fccda5354b21bd22bd0e0e895a12925137873dc2a7591e2650ff864e696f540e3c081c4d8d839388447bbba489a0662ca001ad5ac4008d4586d19e4b629957a95b566ad0ce9f78e3e4ac8bd98781eec4d785587f1a3410251141bd0263b04ee2e4948fbd04e63469d6c78b07d16810ad36399e8d751e5437863acd407f54a3814e7f1293a292830cc56882793cb5710bd46a80366c86880ed5c018b026277beb0892896941fe57e5e74a2222627d1dc5d9bd7e89048e0e0308b76103351b7bb3f1a188136a594c1eaa210e35b8c8e6f496cf75cb4df26d41a2a4008fa53018c49b5814d191eb59f79def3599cedae951e1fabe03040aab2a88dda1ebe4280667f758bef886709de3b0cce728b7eec6fb715d167d209532320603b31dc492a21d3804dfe17737c4f50fdbc3cfeb092cdc607bf24fc2d069ca033e1bb17baac9b23db8b853296c82e5daf9d3b1e79bc300b5ba6f9a5f97a13b1d2f27d086c4e237404d2b06df5d2fd16a7f0206e4b534e0b1c2408e9946f0d22a805eb23ea77bf4c5470d7daf0b0ec6df7f923c53197b0cfe68232ae28f3d79472d3a691a5f1c54c3bd31b75510d4ecc54ffafd6c07d4a6ad99a177d544bbef9b08ccf52f7df026fc714f1b7f2501f220d90972c89c10f87dd63fa28f61c1e900cae45f3baa4a6d596f95cb1b603481345360fd728c7f65202b9701caca1d099fe0006eba48e052d52e8b30f116c1107aa98bd8e32288f3a9bd658bbae512d83eec72ecbed4c22b0bbdb34205edb3207c218254a0bc860cf7f9f6a5b67ec934b1517a282d97076f4d43a999aeddca80d6f949eb36b65252b29f9d0053829565d29022fc82c9128ce08733afae38aee26395eaaee03313d75d9f9d6dc1a2a554ca13018d6b8ffd47d097e8f77798d8f5431a93154c5c4588783318115cf9d23f53e296319792368fd762e7aed6c16f404e9cf17345655213f7d95834218ece675a07bf2f07920bfe0bc61ac73ed8fed0d177ab9e116ef0afff097f301216bb9ee80c18a207918b57b6a5dfc59b6a1e9e4420fe9b8355721e384d1af64fa73953b005aeff256d47e8c4666b0995315c039745dbb866fa3c2138d103d53c7a2411c80b8a07eacd709743eda1f7358265358944b017bbeecb8b2e4afd062da7bdcee50d97cfc304ffdb3fb2bf10eb6c79797298afcf7aa9a698f14310896ac1ea3e61044b73a0f9bd4ede0f1907734789b66f0752f2e40cb0aafae3c7ecf284a239e5086205377c5458c1e31762ad6eab90a234ba7699595bf805886b460f32dcae6601d3a8fb42c28d751e7e13ad8a896448688308490e657b24b15c7ade2a6f60aa046148e609e420c8418f945db4df76f4011987870fca43ad2e4dc31c9667cabd0cf192a860df67bec0d794f85d93c99e645a6b27dc51cfe4593a27570d7489ad090e55dde38cd6acaedd04eb88982904d217929e10bb29e599fd3e80a3d5a86202fb629a13cbb47c1b8edd56d1f39b74b1fc6f3e8294e02bcd74e8490344e5c70f001260de40fe26fd7289b5a271460168fdb828dda2d548ca395fc0481188c904bb1c900be39468da0dbd46203addd690bfd53b25b9e3a551a65fc99132abbc0b1aa7de6e2878f6a326aaa73ec10ec17cfb7b16ae83279fa644b5ad69fa4a44093a86c19d31279efc8361f0d71237051689e5538e3409056edb5cdf209c648c9086fa753ea32a85a9254651ed68fb9c4fa89340fdf2e97e84ace81e8aba8be50adea34b61bd4e16e53a49c8a6731e9ea6ac0806aa30cbcf02b54aaab7a604710e5d40354cd2d7ef23863bbc764f0398a1da11597d9a49130d2b22e95bfb62770dbb5815145c5fe442d21b671c2852a8695b58abaedc9de67708d4a18b03fd0f053b77aaecdd12603947be45e4e44a960365ad881f1f79fb098022f4766628a20668de0253abf0ef530ab111368bd8b8a4940dd5fea0c66c37c887e1343d58770de2632ac9ba7400e79c40ff2c4558ef768d8fb1f21d2341a00ecc57ff34aa170b9f665fec6f8c120acb300955e25fd5e04c22edd9b6f939ae2ab8a5bac1210d0ba66f5b2eb7113dc36d469893ca52f7d6110cf1d849b5f32ad308ca1b3935700b76ba4726fff29019f0b4a85a5c0c1e69310f597a37b3580af823bbd6c7dec80ab0aabfe62aa035ec5596f954d6477d47e76090a40d8354694965cb66ab3d280b0465558641f266cd84010b8554a6e394a9a2764ff9e7d355f0e76c5af11e170c847278309f7905d98213e59cc2435e06b8a563b5929676dca3de7eaa0fa970a09a65a845966d8300fa6b941a0fcef6a2daf8a92c1bc69de7a3b6290086ccac9e98d42bec94d2ad302cee609a1bd2e533675a9b5702f30a985a0578b2d823029d").unwrap(); let tx: monero::Transaction = deserialize(&tx_hex).unwrap(); let output = &tx.prefix().outputs[1]; - let output_key = OutputPubKey(output.get_pubkeys().unwrap()[0].to_bytes()); + let output_key = OutputPubKey( + output + .get_one_time_key() + .expect("expected output public key") + .to_bytes(), + ); let output_id = OutputId { tx_hash: [0; 32], // An intentionally different tx hash. index: 1, @@ -350,6 +366,7 @@ where .account_index(1) .seed(1) .build() + .await .expect("failed to build payment gateway"); // Run it. @@ -361,6 +378,7 @@ where // Add the invoice. let invoice_id = payment_gateway .new_invoice(123, 1, 1, "test invoice".to_string()) + .await .expect("failed to add new invoice to payment gateway for tracking"); let mut subscriber = payment_gateway .subscribe(invoice_id) @@ -376,7 +394,7 @@ where let expected = MockInvoice::new( Some(update.address().to_string()), SubIndex::new(1, 97), - 2477657, + 2_477_657, 123, 1, 1, @@ -387,11 +405,11 @@ where expected.assert_eq(&update); // Add transfer to txpool. - let _txpool_hashes_mock = - mock_daemon.mock_txpool_hashes("tests/rpc_resources/txpools/hashes_with_payment.json"); + let _txpool_hashes_mock = mock_daemon + .mock_txpool_hashes("../testing-utils/rpc_resources/txpools/hashes_with_payment.json"); let _transactions_mock = mock_daemon.mock_transactions( - "tests/rpc_resources/transactions/hashes_with_payment.json", - "tests/rpc_resources/transactions/txs_with_payment.json", + "../testing-utils/rpc_resources/transactions/hashes_with_payment.json", + "../testing-utils/rpc_resources/transactions/txs_with_payment.json", ); // There shouldn't be any update. @@ -401,6 +419,7 @@ where .expect_err("timeout waiting for invoice update"); } +#[allow(clippy::too_many_lines)] #[test_case(Sled::new(&new_temp_dir(), "invoices", "output keys", "height").unwrap(); "sled")] #[test_case(InMemory::new(); "in-memory")] #[test_case(Sqlite::new(":memory:", "invoices", "output keys", "height").unwrap(); "sqlite")] @@ -425,6 +444,7 @@ where .account_index(1) .seed(1) .build() + .await .expect("failed to build payment gateway"); // Run it. @@ -435,7 +455,8 @@ where // Add the invoice. let invoice_id = payment_gateway - .new_invoice(70000000, 2, 7, "invoice 1".to_string()) + .new_invoice(70_000_000, 2, 7, "invoice 1".to_string()) + .await .expect("failed to add new invoice to payment gateway for tracking"); let mut subscriber_1 = payment_gateway .subscribe(invoice_id) @@ -451,8 +472,8 @@ where let mut expected_1 = MockInvoice::new( Some(update.address().to_string()), SubIndex::new(1, 97), - 2477657, - 70000000, + 2_477_657, + 70_000_000, 2, 7, "invoice 1".to_string(), @@ -463,7 +484,8 @@ where // Add the invoice. let invoice_id = payment_gateway - .new_invoice(70000000, 2, 7, "invoice 2".to_string()) + .new_invoice(70_000_000, 2, 7, "invoice 2".to_string()) + .await .expect("failed to add new invoice to payment gateway for tracking"); let mut subscriber_2 = payment_gateway .subscribe(invoice_id) @@ -485,8 +507,8 @@ where expected_2.assert_eq(&update); // Add double transfer to txpool. - let txpool_hashes_mock = - mock_daemon.mock_txpool_hashes("tests/rpc_resources/txpools/hashes_with_payment.json"); + let txpool_hashes_mock = mock_daemon + .mock_txpool_hashes("../testing-utils/rpc_resources/txpools/hashes_with_payment.json"); // Mock for these transactions themselves is unnecessary, because they are all // in block 2477657. @@ -498,7 +520,7 @@ where .expect("subscription channel is closed"); // Check that it is as expected. - expected_1.amount_paid = 37419570; + expected_1.amount_paid = 37_419_570; expected_1.assert_eq(&update); // Get update. @@ -509,14 +531,14 @@ where .expect("subscription channel is closed"); // Check that it is as expected. - expected_2.amount_paid = 37419570; + expected_2.amount_paid = 37_419_570; expected_2.assert_eq(&update); // Check that the mock server did in fact receive the requests. assert!(txpool_hashes_mock.hits() > 0); // Mock txpool with no payments (as if the payment moved to a block). - mock_daemon.mock_txpool_hashes("tests/rpc_resources/txpools/hashes.json"); + mock_daemon.mock_txpool_hashes("../testing-utils/rpc_resources/txpools/hashes.json"); // Both invoices should now show zero paid. let update = subscriber_1 @@ -533,7 +555,7 @@ where assert_eq!(update.amount_paid(), 0); // Move forward a few blocks. - for height in 2477658..2477663 { + for height in 2_477_658..2_477_663 { let height_mock = mock_daemon.mock_daemon_height(height); let update = subscriber_1 @@ -542,7 +564,7 @@ where .expect("timeout waiting for invoice update") .expect("subscription channel is closed"); - expected_1.expires_in = 2477664 - height; + expected_1.expires_in = 2_477_664 - height; expected_1.current_height = height; expected_1.assert_eq(&update); @@ -552,7 +574,7 @@ where .expect("timeout waiting for invoice update") .expect("subscription channel is closed"); - expected_2.expires_in = 2477664 - height; + expected_2.expires_in = 2_477_664 - height; expected_2.current_height = height; expected_2.assert_eq(&update); @@ -560,11 +582,11 @@ where } // Put second payment in txpool. - let txpool_hashes_mock = - mock_daemon.mock_txpool_hashes("tests/rpc_resources/txpools/hashes_with_payment_2.json"); + let txpool_hashes_mock = mock_daemon + .mock_txpool_hashes("../testing-utils/rpc_resources/txpools/hashes_with_payment_2.json"); let txpool_transactions_mock = mock_daemon.mock_txpool_transactions( - "tests/rpc_resources/transactions/hashes_with_payment_2.json", - "tests/rpc_resources/transactions/txs_with_payment_2.json", + "../testing-utils/rpc_resources/transactions/hashes_with_payment_2.json", + "../testing-utils/rpc_resources/transactions/txs_with_payment_2.json", ); // Invoice 1 should be paid now. @@ -574,7 +596,7 @@ where .expect("timeout waiting for invoice update") .expect("subscription channel is closed"); - expected_1.amount_paid = 74839140; + expected_1.amount_paid = 74_839_140; expected_1.confirmations = Some(0); expected_1.assert_eq(&update); @@ -591,7 +613,7 @@ where // (getting update after txpool change, so there's no data race between the // scanner and these two mock changes). let txpool_hashes_mock = - mock_daemon.mock_txpool_hashes("tests/rpc_resources/txpools/hashes.json"); + mock_daemon.mock_txpool_hashes("../testing-utils/rpc_resources/txpools/hashes.json"); subscriber_1 .recv_timeout(Duration::from_secs(120)) .await @@ -601,7 +623,7 @@ where .recv_timeout(Duration::from_secs(1)) .await .expect_err("should not have received an update, but did"); - let height_mock = mock_daemon.mock_daemon_height(2477663); + let height_mock = mock_daemon.mock_daemon_height(2_477_663); let update = subscriber_1 .recv_timeout(Duration::from_secs(120)) @@ -611,7 +633,7 @@ where expected_1.confirmations = Some(1); expected_1.expires_in = 1; - expected_1.current_height = 2477663; + expected_1.current_height = 2_477_663; expected_1.assert_eq(&update); let update = subscriber_2 @@ -621,14 +643,14 @@ where .expect("subscription channel is closed"); expected_2.expires_in = 1; - expected_2.current_height = 2477663; + expected_2.current_height = 2_477_663; expected_2.assert_eq(&update); assert!(txpool_hashes_mock.hits() > 0); assert!(height_mock.hits() > 0); // Move forward a block. - let height_mock = mock_daemon.mock_daemon_height(2477664); + let height_mock = mock_daemon.mock_daemon_height(2_477_664); let update = subscriber_1 .recv_timeout(Duration::from_secs(120)) @@ -639,7 +661,7 @@ where expected_1.confirmations = Some(2); expected_1.is_confirmed = true; expected_1.expires_in = 0; - expected_1.current_height = 2477664; + expected_1.current_height = 2_477_664; expected_1.assert_eq(&update); let update = subscriber_2 @@ -650,24 +672,44 @@ where expected_2.expires_in = 0; expected_2.is_expired = true; - expected_2.current_height = 2477664; + expected_2.current_height = 2_477_664; expected_2.assert_eq(&update); assert!(txpool_hashes_mock.hits() > 0); assert!(height_mock.hits() > 0); } +#[allow(clippy::too_many_lines)] #[test_case(Sled::new(&new_temp_dir(), "invoices", "output keys", "height").unwrap(); "sled")] #[test_case(InMemory::new(); "in-memory")] #[test_case(Sqlite::new(":memory:", "invoices", "output keys", "height").unwrap(); "sqlite")] -#[tokio::test] -async fn set_initial_height(store: S) +#[tokio::test(flavor = "multi_thread")] +async fn set_initial_height(mut store: S) where S: Storage + 'static, { // Setup. init_logger(); + // console_subscriber::init(); let mock_daemon = MockDaemon::new_mock_daemon().await; + let _height_mock = mock_daemon.mock_daemon_height(2_477_664); + + // TX from block 2_477_657. + let tx_hex = hex::decode("02000202000bde84861298e38c01c99338d5b005cab9038d26d8e301d442e304a419f406af616b0e66f90a5ff643ee8ff496a25e2caf3d8fccf65895073212a795ae7a7c02000ba09ef20f9da08e02dcb4a301eba60def810cc3b20e9e8801bf9101cdcf04f6ae02f7a50118130528cdc0df212e936b5c95b35ebcdecd443000e94b637782f47a2a47171c0300026f5955625996a0051ff3a419b5fe967fd4a691ad99b2e7dce6fcb35d21821f0d0002b024664bccba2226849a95dfe653c64e02c5498ea88265457c44f85778f018f700027fd35731e1bd4d1ef4bb1f0f008c4b0ba54bb214bb3304fab61c2b5e3a8570f4830101349587e7557632f77ab342e1fb69dc1d106900bc3557e45b9f18d117be363bbc0403909dd7c66c8dba74062fa6d53bcbaa9c3c3a2481dd23d27be3b178449a33303aa17cbf28ae53a1ed402ba4bda9466b92471ce9ddafd5c2ae6e07470ed4d210728a20e76551ee2b320b3fac21c6d823bd6bf2c645e039e226290bc427cc3621930590d99107d6f209fb953c4370924e52a5b1b3df48fbf8306474b69d0998c657aa6aeb1f2f2ee09d5c8e8acf0a32a76aa7f949305829b5a35d53a8b7f72a9960e05ce91198e32bb1f5b049ae2f09aba0975fca8df6f176c947f70170e1514c5c6a75bfcea627e9fcedc7d75fc4a8440b5c331ff2b8a06b0d9c01aca9a1016700a9b4ca3f1f52fccda5354b21bd22bd0e0e895a12925137873dc2a7591e2650ff864e696f540e3c081c4d8d839388447bbba489a0662ca001ad5ac4008d4586d19e4b629957a95b566ad0ce9f78e3e4ac8bd98781eec4d785587f1a3410251141bd0263b04ee2e4948fbd04e63469d6c78b07d16810ad36399e8d751e5437863acd407f54a3814e7f1293a292830cc56882793cb5710bd46a80366c86880ed5c018b026277beb0892896941fe57e5e74a2222627d1dc5d9bd7e89048e0e0308b76103351b7bb3f1a188136a594c1eaa210e35b8c8e6f496cf75cb4df26d41a2a4008fa53018c49b5814d191eb59f79def3599cedae951e1fabe03040aab2a88dda1ebe4280667f758bef886709de3b0cce728b7eec6fb715d167d209532320603b31dc492a21d3804dfe17737c4f50fdbc3cfeb092cdc607bf24fc2d069ca033e1bb17baac9b23db8b853296c82e5daf9d3b1e79bc300b5ba6f9a5f97a13b1d2f27d086c4e237404d2b06df5d2fd16a7f0206e4b534e0b1c2408e9946f0d22a805eb23ea77bf4c5470d7daf0b0ec6df7f923c53197b0cfe68232ae28f3d79472d3a691a5f1c54c3bd31b75510d4ecc54ffafd6c07d4a6ad99a177d544bbef9b08ccf52f7df026fc714f1b7f2501f220d90972c89c10f87dd63fa28f61c1e900cae45f3baa4a6d596f95cb1b603481345360fd728c7f65202b9701caca1d099fe0006eba48e052d52e8b30f116c1107aa98bd8e32288f3a9bd658bbae512d83eec72ecbed4c22b0bbdb34205edb3207c218254a0bc860cf7f9f6a5b67ec934b1517a282d97076f4d43a999aeddca80d6f949eb36b65252b29f9d0053829565d29022fc82c9128ce08733afae38aee26395eaaee03313d75d9f9d6dc1a2a554ca13018d6b8ffd47d097e8f77798d8f5431a93154c5c4588783318115cf9d23f53e296319792368fd762e7aed6c16f404e9cf17345655213f7d95834218ece675a07bf2f07920bfe0bc61ac73ed8fed0d177ab9e116ef0afff097f301216bb9ee80c18a207918b57b6a5dfc59b6a1e9e4420fe9b8355721e384d1af64fa73953b005aeff256d47e8c4666b0995315c039745dbb866fa3c2138d103d53c7a2411c80b8a07eacd709743eda1f7358265358944b017bbeecb8b2e4afd062da7bdcee50d97cfc304ffdb3fb2bf10eb6c79797298afcf7aa9a698f14310896ac1ea3e61044b73a0f9bd4ede0f1907734789b66f0752f2e40cb0aafae3c7ecf284a239e5086205377c5458c1e31762ad6eab90a234ba7699595bf805886b460f32dcae6601d3a8fb42c28d751e7e13ad8a896448688308490e657b24b15c7ade2a6f60aa046148e609e420c8418f945db4df76f4011987870fca43ad2e4dc31c9667cabd0cf192a860df67bec0d794f85d93c99e645a6b27dc51cfe4593a27570d7489ad090e55dde38cd6acaedd04eb88982904d217929e10bb29e599fd3e80a3d5a86202fb629a13cbb47c1b8edd56d1f39b74b1fc6f3e8294e02bcd74e8490344e5c70f001260de40fe26fd7289b5a271460168fdb828dda2d548ca395fc0481188c904bb1c900be39468da0dbd46203addd690bfd53b25b9e3a551a65fc99132abbc0b1aa7de6e2878f6a326aaa73ec10ec17cfb7b16ae83279fa644b5ad69fa4a44093a86c19d31279efc8361f0d71237051689e5538e3409056edb5cdf209c648c9086fa753ea32a85a9254651ed68fb9c4fa89340fdf2e97e84ace81e8aba8be50adea34b61bd4e16e53a49c8a6731e9ea6ac0806aa30cbcf02b54aaab7a604710e5d40354cd2d7ef23863bbc764f0398a1da11597d9a49130d2b22e95bfb62770dbb5815145c5fe442d21b671c2852a8695b58abaedc9de67708d4a18b03fd0f053b77aaecdd12603947be45e4e44a960365ad881f1f79fb098022f4766628a20668de0253abf0ef530ab111368bd8b8a4940dd5fea0c66c37c887e1343d58770de2632ac9ba7400e79c40ff2c4558ef768d8fb1f21d2341a00ecc57ff34aa170b9f665fec6f8c120acb300955e25fd5e04c22edd9b6f939ae2ab8a5bac1210d0ba66f5b2eb7113dc36d469893ca52f7d6110cf1d849b5f32ad308ca1b3935700b76ba4726fff29019f0b4a85a5c0c1e69310f597a37b3580af823bbd6c7dec80ab0aabfe62aa035ec5596f954d6477d47e76090a40d8354694965cb66ab3d280b0465558641f266cd84010b8554a6e394a9a2764ff9e7d355f0e76c5af11e170c847278309f7905d98213e59cc2435e06b8a563b5929676dca3de7eaa0fa970a09a65a845966d8300fa6b941a0fcef6a2daf8a92c1bc69de7a3b6290086ccac9e98d42bec94d2ad302cee609a1bd2e533675a9b5702f30a985a0578b2d823029d").unwrap(); + let tx: monero::Transaction = deserialize(&tx_hex).unwrap(); + let output = &tx.prefix().outputs[1]; + let output_key = OutputPubKey( + output + .get_one_time_key() + .expect("expected output public key") + .to_bytes(), + ); + let output_id = OutputId { + tx_hash: [0; 32], // An intentionally different tx hash. + index: 1, + }; + // Insert the key with a different ID so it looks like a re-used key. + OutputKeyStorage::insert(&mut store, output_key, output_id).unwrap(); // Create payment gateway pointing at temp directory and mock daemon. let payment_gateway_with_height = PaymentGatewayBuilder::new( @@ -676,12 +718,15 @@ where store, ) // Faster scan rate so the update is received sooner. - .scan_interval(Duration::from_millis(100)) + //.scan_interval(Duration::from_millis(100)) .daemon_url(mock_daemon.url("")) .account_index(1) - .initial_height(2477657) + .initial_height(2_477_657) .seed(1) + .rpc_timeout(Duration::from_secs(120)) + .rpc_connection_timeout(Duration::from_secs(60)) .build() + .await .expect("failed to build payment gateway"); // Create payment gateway pointing at temp directory and mock daemon. @@ -691,15 +736,16 @@ where InMemory::new(), ) // Faster scan rate so the update is received sooner. - .scan_interval(Duration::from_millis(100)) + //.scan_interval(Duration::from_millis(100)) .daemon_url(mock_daemon.url("")) .account_index(1) .seed(1) + .rpc_timeout(Duration::from_secs(120)) + .rpc_connection_timeout(Duration::from_secs(60)) .build() + .await .expect("failed to build payment gateway"); - let _height_mock = mock_daemon.mock_daemon_height(2477664); - // Run it. payment_gateway_with_height .run() @@ -710,9 +756,21 @@ where .await .expect("failed to run payment gateway"); + // Wait to catch up. + while payment_gateway_with_height.cache_height() + < payment_gateway_with_height + .daemon_height() + .await + .unwrap() + .saturating_sub(1) + { + tokio::time::sleep(Duration::from_millis(100)).await; + } + // Add the invoice. let invoice_id = payment_gateway_with_height - .new_invoice(70000000, 2, 7, "invoice 1".to_string()) + .new_invoice(70_000_000, 2, 7, "invoice 1".to_string()) + .await .expect("failed to add new invoice to payment gateway for tracking"); let mut subscriber_1 = payment_gateway_with_height .subscribe(invoice_id) @@ -728,21 +786,22 @@ where let mut expected_1 = MockInvoice::new( Some(update.address().to_string()), SubIndex::new(1, 97), - 2477664, - 70000000, + 2_477_664, + 70_000_000, 2, 7, "invoice 1".to_string(), ); - expected_1.current_height = 2477658; - expected_1.expiration_height = 2477671; + expected_1.current_height = 2_477_664; + expected_1.expiration_height = 2_477_671; // Check that it is as expected. expected_1.assert_eq(&update); // Add the invoice. let invoice_id = payment_gateway - .new_invoice(70000000, 2, 7, "invoice 2".to_string()) + .new_invoice(70_000_000, 2, 7, "invoice 2".to_string()) + .await .expect("failed to add new invoice to payment gateway for tracking"); let mut subscriber_2 = payment_gateway .subscribe(invoice_id) @@ -756,19 +815,29 @@ where .expect("subscription channel is closed"); let mut expected_2 = expected_1.clone(); - expected_2.current_height = 2477664; + expected_2.current_height = 2_477_664; expected_2.description = "invoice 2".to_string(); // Check that it is as expected. expected_2.assert_eq(&update); - for height in 2477659..2477665 { - let update = subscriber_1 - .recv_timeout(Duration::from_secs(120)) - .await - .expect("timeout waiting for invoice update") - .expect("subscription channel is closed"); - assert_eq!(update.current_height(), height); - assert_eq!(update.amount_paid(), 0); - } + // Add transaction with same key inserted previously. + let _txpool_hashes_mock = mock_daemon + .mock_txpool_hashes("../testing-utils/rpc_resources/txpools/hashes_with_payment.json"); + let _transactions_mock = mock_daemon.mock_transactions( + "../testing-utils/rpc_resources/transactions/hashes_with_payment.json", + "../testing-utils/rpc_resources/transactions/txs_with_payment.json", + ); + + // Assert that it is ignored for duplicate key. + subscriber_1 + .recv_timeout(Duration::from_secs(2)) + .await + .expect_err("timeout waiting for invoice update"); + + // Assert that the other gateway fails to ignore it. + subscriber_2 + .recv_timeout(Duration::from_secs(120)) + .await + .expect("timeout waiting for invoice update"); } diff --git a/library/tests/integration_tests/scanning_thread_management.rs b/library/tests/integration_tests/scanning_thread_management.rs index e4de78e..1886189 100644 --- a/library/tests/integration_tests/scanning_thread_management.rs +++ b/library/tests/integration_tests/scanning_thread_management.rs @@ -1,8 +1,7 @@ use acceptxmr::{ storage::stores::Sled, AcceptXmrError, PaymentGatewayBuilder, PaymentGatewayStatus, }; - -use crate::common::{init_logger, new_temp_dir, MockDaemon, PRIMARY_ADDRESS, PRIVATE_VIEW_KEY}; +use testing_utils::{init_logger, new_temp_dir, MockDaemon, PRIMARY_ADDRESS, PRIVATE_VIEW_KEY}; #[tokio::test] async fn run_payment_gateway() { @@ -22,6 +21,7 @@ async fn run_payment_gateway() { ) .daemon_url(mock_daemon.url("")) .build() + .await .expect("failed to build payment gateway"); // Run it. @@ -49,6 +49,7 @@ async fn cannot_run_payment_gateway_twice() { ) .daemon_url(mock_daemon.url("")) .build() + .await .expect("failed to build payment gateway"); // Run it. @@ -84,6 +85,7 @@ async fn stop_payment_gateway() { ) .daemon_url(mock_daemon.url("")) .build() + .await .expect("failed to build payment gateway"); // Run it. @@ -93,9 +95,9 @@ async fn stop_payment_gateway() { .expect("failed to run payment gateway"); assert!(matches!( - payment_gateway.status(), + payment_gateway.status().await, PaymentGatewayStatus::Running, )); - assert!(payment_gateway.stop().is_ok()); + assert!(payment_gateway.stop().await.is_ok()); } diff --git a/server/Cargo.toml b/server/Cargo.toml index 12899c1..b889434 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -3,7 +3,7 @@ name = "acceptxmr-server" publish = false version = "0.1.0" edition = "2021" -rust-version = "1.70" +rust-version = "1.74" license = "MIT OR Apache-2.0" description = "A monero payment gateway." repository = "https://github.com/busyboredom/acceptxmr" @@ -11,43 +11,50 @@ readme = "README.md" keywords = ["crypto", "gateway", "monero", "payment", "xmr"] categories = ["cryptography::cryptocurrencies"] +[lints] +workspace = true + [[bin]] name = "acceptxmr-server" path = "src/main.rs" [dependencies] -acceptxmr = { path = "../library", features = ["serde", "sqlite"] } -actix = "0.13" -actix-files = "0.6" -actix-session = { version = "0.8", features = ["cookie-session"] } -actix-web = { version = "4", features = ["rustls-0_21"] } -actix-web-actors = "4" -actix-web-httpauth = "0.8" -anyhow = "1" -bytestring = "1" -clap = { version = "4", features = ["env"] } -dotenv = "0.15" -env_logger = "0.10" -futures = "0.3" -log = { version = "0.4", features = ["serde"] } -monero = { version = "0.19", features = ["serde"] } -rand = "0.8" -rand_chacha = "0.3" -rcgen = "0.11" -rustls = "0.21" -rustls-pemfile = "1" -secrecy = { version = "0.8", features = ["serde"] } -serde = {version = "1", features = ["derive"] } -serde_json = "1" -serde_with = "3" -serde_yaml = "0.9" +acceptxmr = { workspace = true, features = ["serde", "sqlite"] } +axum = { workspace = true, features = ["http1", "http2", "tokio", "tower-log", "tracing", "query", "json", "ws", "macros"] } +bytes.workspace = true +clap = { workspace = true, features = ["env"] } +console-subscriber.workspace = true +dotenv.workspace = true +env_logger.workspace = true +futures.workspace = true +futures-util.workspace = true +http-body-util.workspace = true +hyper = { workspace = true, features = ["client", "http2"] } +hyper-rustls = { workspace = true, features = ["logging", "http1", "http2", "tls12"] } +hyper-util = { workspace = true, features = ["client-legacy"] } +log = { workspace = true, features = ["serde"] } +monero = { workspace = true, features = ["serde"] } +rand.workspace = true +rand_chacha.workspace = true +rcgen.workspace = true +rustls.workspace = true +rustls-pemfile.workspace = true +secrecy = { workspace = true, features = ["serde"] } +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +serde_with.workspace = true +serde_yaml.workspace = true +thiserror.workspace = true +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time", "tracing"] } +tokio-rustls.workspace = true +tower.workspace = true +tower-http = { workspace = true, features = ["fs", "auth", "validate-request"] } +utoipa.workspace = true +utoipa-swagger-ui = { workspace = true, features = ["axum"] } [dev-dependencies] -http = "0.2" -hyper = { version = "0.14", features = ["client", "http2", "tcp"] } -hyper-rustls = { version = "0.24", features = ["logging", "http1", "http2", "tls12"], default-features = false } -rustls = { version = "0.21", features = ["dangerous_configuration"] } -serde_json = "1" -test-case = "3" -thiserror = "1" -tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] } +hyper = { workspace = true, features = ["server"] } +hyper-util = { workspace = true, features = ["server-auto"] } +serde_json.workspace = true +test-case.workspace = true +testing-utils.workspace = true diff --git a/server/README.md b/server/README.md index f306eda..7c19443 100644 --- a/server/README.md +++ b/server/README.md @@ -103,4 +103,66 @@ Please click [here](../.env) for an example of how to configure secrets in a `.env` file. ### API -TODO + +`AcceptXMR-Server` serves two APIs. The first is an "internal" API meant to be +used server-side (i.e. not exposed to the internet). The second API is an +"external" API, which is safe to expose to the end-user (i.e. it may be exposed +to the internet). + +#### Internal API + +The internal API serves endpoints which the end user should not have access to +(for example, creating new invoices). + +**Create a new invoice: `POST /invoice`** + +Example body: +```json +{ + piconeros_due: 10000, + confirmations_required: 0, + expiration_in: 10, + description: "I am an example description", +} +``` + +Example response: +```json +{ + invoice_id: "748934525", +} +``` + +#### External API + +The external API serves endpoints which are safe to expose to the end user. +These endpoints do things like retreive the status of an invoice, cancel an +invoice, or start a websocket connection. + +**Get an invoice's status: `GET /invoice?id=`** + +Example response: +```json +{ + address: "84pKaXBd9biTwA7wihzUvrXN2YHoJBdFC4ZxEHQqaPuMFDa8Nyg1mywMXgzvjWBiTCfim7ZRfuJhvHavJrZ4Y7z3THW2Hmf", + amount_paid: 4000, + amount_requested: 5000, + uri: "monero:84pKaXBd9biTwA7wihzUvrXN2YHoJBdFC4ZxEHQqaPuMFDa8Nyg1mywMXgzvjWBiTCfim7ZRfuJhvHavJrZ4Y7z3THW2Hmf?tx_amount=0.000000001000" + confirmations: 1, + confirmations_required: 2, + expiration_in: 4, + description: "I am an example description", +} +``` + +**Subscribe to an invoice's updates via websocket: `GET /invoice/ws?id=`** + +Response: *Whatever the upgrade status code is.* + +**Cancel an invoice: `DELETE /invoice?id=`** + +Response: `200` + +**Go to payment UI: `GET /pay?id=`** + +Serves a minimal UI prompting the user for payment. diff --git a/server/acceptxmr.yaml b/server/acceptxmr.yaml index 800272b..b19de21 100644 --- a/server/acceptxmr.yaml +++ b/server/acceptxmr.yaml @@ -1,22 +1,26 @@ external-api: - port: 8080 + port: 0 ipv4: 127.0.0.1 - ipv6: ::1 + #ipv6: ::1 static_dir: static/ internal-api: - port: 8081 + port: 0 ipv4: 127.0.0.1 - ipv6: ::1 + #ipv6: ::1 tls: cert: tests/testdata/cert/certificate.pem key: tests/testdata/cert/privatekey.pem static_dir: static/ +callback: + queue-size: 10000 wallet: primary-address: 4613YiHLM6JMH4zejMB2zJY5TwQCxL8p65ufw8kBP5yxX9itmuGLqp1dS4tkVoTxjyH3aYhYNrtGHbQzJQP5bFus3KHVdmf account-index: 0 restore-height: null daemon: url: https://xmr-node.cakewallet.com:18081/ + rpc-timeout: 30 + connection-timeout: 20 database: path: AcceptXMR_DB/ logging: diff --git a/server/src/callbacks.rs b/server/src/callbacks.rs new file mode 100644 index 0000000..d1123e9 --- /dev/null +++ b/server/src/callbacks.rs @@ -0,0 +1,217 @@ +use std::{str::FromStr, time::Duration}; + +use acceptxmr::Invoice; +use bytes::Bytes; +use http_body_util::Full; +use hyper::{ + body::Incoming, + http::{ + header::{ACCEPT, CONTENT_TYPE}, + uri::InvalidUri, + StatusCode, + }, + Method, Request, Response, Uri, +}; +use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; +use hyper_util::{ + client::legacy::{connect::HttpConnector, Client}, + rt::TokioExecutor, +}; +use log::{debug, error, info}; +use serde_json::json; +use thiserror::Error; +use tokio::{ + sync::mpsc::{channel, error::SendError, Sender}, + time::timeout, +}; + +use crate::server::api::{InvoiceDescription, InvoiceUpdate}; + +/// Delay before retrying a callback, in seconds. +const CALLBACK_RETRY_DELAY: u64 = 5; + +#[derive(Debug, Clone)] +pub(crate) struct CallbackClient { + client: Client, Full>, + timeout: Duration, +} + +impl CallbackClient { + /// Returns a payment gateway client. + pub(crate) fn new(total_timeout: Duration, connection_timeout: Duration) -> CallbackClient { + let mut hyper_connector = HttpConnector::new(); + hyper_connector.set_connect_timeout(Some(connection_timeout)); + hyper_connector.enforce_http(false); + + let rustls_connector = HttpsConnectorBuilder::new() + .with_webpki_roots() + .https_or_http() + .enable_http1() + .enable_http2() + .wrap_connector(hyper_connector); + let client = Client::builder(TokioExecutor::new()).build(rustls_connector); + + CallbackClient { + client, + timeout: total_timeout, + } + } + + /// Call the invoices callback, if one exists. Return Ok(true) if the + /// callback was called, or Ok(false) if there was no callback to call. + pub(crate) async fn callback(&self, invoice: &Invoice) -> Result { + let description: InvoiceDescription = serde_json::from_str(invoice.description()) + .map_err(CallbackError::InvalidDescription)?; + let callback_uri = match description.callback { + Some(uri) => Uri::from_str(&uri).map_err(CallbackError::InvalidCallback)?, + None => return Ok(false), + }; + + let invoice_update: InvoiceUpdate = invoice.clone().into(); + self.request(json! {invoice_update}, &callback_uri).await?; + + Ok(true) + } + + async fn request( + &self, + body: serde_json::Value, + uri: &Uri, + ) -> Result, CallbackError> { + let mut response = None; + timeout(self.timeout, async { + let request_builder = Request::builder() + .method(Method::POST) + .header(ACCEPT, "*/*") + .header(CONTENT_TYPE, "application/json") + .uri(uri); + + let request = match request_builder.body(Full::new(body.to_string().into())) { + Ok(r) => r, + Err(e) => { + response = Some(Err(e.into())); + return; + } + }; + + match self.client.request(request).await { + Ok(r) if r.status().is_server_error() | r.status().is_client_error() => { + error!( + "Callback response contains an error. Callback will be retried. Status code: {}, body: {:?}", + r.status(), + r.body() + ); + response = Some(Err(r.into())); + } + Ok(r) => { + debug!("Callback successful. Response: {r:?}"); + response = Some(Ok(r)); + } + Err(e) => { + error!("Error calling callback, retrying: {}", e); + response = Some(Err(CallbackError::Request(Box::new(e)))); + } + }; + }) + .await + .map_err(|_| CallbackError::Timeout)?; + response.unwrap_or(Err(CallbackError::Timeout)) + } +} + +impl Default for CallbackClient { + fn default() -> Self { + CallbackClient::new(Duration::from_secs(10), Duration::from_secs(5)) + } +} + +pub(crate) struct CallbackQueue { + sender: Sender, +} + +impl CallbackQueue { + pub(crate) fn init(client: CallbackClient, queue_size: usize) -> CallbackQueue { + let (sender, mut receiver) = channel(queue_size); + let queue = CallbackQueue { + sender: sender.clone(), + }; + + tokio::spawn(async move { + loop { + match receiver.recv().await { + Some(CallbackCommand::Shutdown) => { + info!("Callback queue received shutdown signal"); + break; + } + Some(CallbackCommand::Call { invoice, delay }) => { + debug!("Processing callback for invoice with ID {}", invoice.id()); + let client_clone = client.clone(); + let sender_clone = sender.clone(); + tokio::spawn(async move { + tokio::time::sleep(delay).await; + if let Err(e) = client_clone.callback(&invoice).await { + error!("Failed to call callback: {}. Callback will be retried.", e); + sender_clone + .send(CallbackCommand::Call { + invoice, + delay: Duration::from_secs(CALLBACK_RETRY_DELAY), + }) + .await + .expect("failed to place callback back in the callback queue"); + } + }); + } + None => { + info!("Callback queue sender closed. Stopping callback queue."); + break; + } + } + } + }); + + info!("Callback queue initialized"); + queue + } + + pub(crate) async fn send( + &self, + command: CallbackCommand, + ) -> Result<(), SendError> { + self.sender.send(command).await + } +} + +pub(crate) enum CallbackCommand { + // TODO: Implement graceful shutdown. + #[allow(unused)] + Shutdown, + Call { + invoice: Invoice, + delay: Duration, + }, +} + +#[derive(Error, Debug)] +pub(crate) enum CallbackError { + #[error("HTTP request failed: {0}")] + Request(Box), + #[error("failed to build HTTP Request: {0}")] + InvalidRequest(#[from] hyper::http::Error), + #[error("Callback recipient returned an error. Status code: {status}, body: {body:?}")] + Response { status: StatusCode, body: Incoming }, + #[error("HTTP request timed out")] + Timeout, + #[error("failed to deserialize invoice description: {0}")] + InvalidDescription(serde_json::Error), + #[error("callback is not a valid URI: {0}")] + InvalidCallback(InvalidUri), +} + +impl From> for CallbackError { + fn from(value: Response) -> Self { + CallbackError::Response { + status: value.status(), + body: value.into_body(), + } + } +} diff --git a/server/src/config/callback.rs b/server/src/config/callback.rs new file mode 100644 index 0000000..168cece --- /dev/null +++ b/server/src/config/callback.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, PartialEq, Eq, Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct CallbackConfig { + /// Number of callbacks that can be queued. + pub queue_size: usize, +} + +impl Default for CallbackConfig { + fn default() -> Self { + Self { queue_size: 10_000 } + } +} diff --git a/server/src/config/daemon.rs b/server/src/config/daemon.rs index 6071b67..06e8452 100644 --- a/server/src/config/daemon.rs +++ b/server/src/config/daemon.rs @@ -1,14 +1,16 @@ -use std::{env, env::VarError}; +use std::{env, env::VarError, time::Duration}; -use actix_web::http::Uri; -use anyhow::Result; +use hyper::Uri; use log::warn; use secrecy::{ExposeSecret, Secret}; use serde::{Deserialize, Serialize}; -use serde_with::{serde_as, DisplayFromStr}; +use serde_with::{serde_as, DisplayFromStr, DurationSeconds}; + +use super::ConfigError; #[serde_as] #[derive(Deserialize, PartialEq, Debug, Serialize)] +#[serde(rename_all = "kebab-case")] pub struct DaemonConfig { /// URL of monero daemon. #[serde_as(as = "DisplayFromStr")] @@ -16,10 +18,16 @@ pub struct DaemonConfig { /// Monero daemon login credentials, if applicable. #[serde(skip_serializing_if = "Option::is_none")] pub login: Option, + /// Timeout in seconds for RPC calls to the daemon. + #[serde_as(as = "DurationSeconds")] + pub rpc_timeout: Duration, + /// Timeout in seconds for making an RPC connection to the daemon. + #[serde_as(as = "DurationSeconds")] + pub connection_timeout: Duration, } impl DaemonConfig { - pub(super) fn apply_env_overrides(mut self) -> Result { + pub(super) fn apply_env_overrides(mut self) -> Result { match env::var("DAEMON_PASSWORD") { Ok(password) => { if let Some(login) = self.login.as_mut() { @@ -49,6 +57,8 @@ impl Default for DaemonConfig { Self { url: Uri::from_static("https://xmr-node.cakewallet.com:18081"), login: None, + rpc_timeout: Duration::from_secs(30), + connection_timeout: Duration::from_secs(20), } } } @@ -82,7 +92,7 @@ impl PartialEq for DaemonLoginConfig { mod test { use std::{env, panic::catch_unwind}; - use actix_web::http::Uri; + use hyper::Uri; use secrecy::{ExposeSecret, Secret}; use test_case::test_case; @@ -113,13 +123,15 @@ mod test { #[test_case( &DaemonConfig { url: Uri::from_static("http://example.com"), - login: Some(DaemonLoginConfig {username: "jsmith".to_string(), password: None}) + login: Some(DaemonLoginConfig {username: "jsmith".to_string(), password: None}), + ..Default::default() } => false; "missing password" )] #[test_case( &DaemonConfig { url: Uri::from_static("http://example.com"), - login: Some(DaemonLoginConfig {username: "jsmith".to_string(), password: Some(Secret::new("p455w0rd".to_string()))}) + login: Some(DaemonLoginConfig {username: "jsmith".to_string(), password: Some(Secret::new("p455w0rd".to_string()))}), + ..Default::default() } => true; "with password" )] diff --git a/server/src/config/mod.rs b/server/src/config/mod.rs index b90838e..716a998 100644 --- a/server/src/config/mod.rs +++ b/server/src/config/mod.rs @@ -1,3 +1,4 @@ +mod callback; mod daemon; mod database; mod logging; @@ -7,21 +8,24 @@ mod wallet; use std::{ env::{self, VarError}, fs::File, + io, io::{ErrorKind as IoErrorKind, Write}, path::PathBuf, }; -use anyhow::Result; +pub(crate) use callback::CallbackConfig; use clap::{Arg, ArgAction, Command}; -pub use daemon::{DaemonConfig, DaemonLoginConfig}; -pub use database::DatabaseConfig; +pub(crate) use daemon::DaemonConfig; +pub(crate) use database::DatabaseConfig; use dotenv::dotenv; use log::info; -pub use logging::LoggingConfig; +pub(crate) use logging::LoggingConfig; use secrecy::Secret; use serde::{Deserialize, Serialize}; -pub use server::{ServerConfig, TlsConfig}; -pub use wallet::WalletConfig; +use serde_yaml::Error as YamlError; +pub(crate) use server::{ServerConfig, TlsConfig}; +use thiserror::Error; +pub(crate) use wallet::WalletConfig; /// AcceptXMR-Server configuration. #[derive(Deserialize, PartialEq, Debug, Serialize)] @@ -31,6 +35,8 @@ pub struct Config { pub external_api: ServerConfig, /// Config for the internal API. pub internal_api: ServerConfig, + /// Config for callback functionality. + pub callback: CallbackConfig, /// Monero wallet configuration. pub wallet: WalletConfig, /// Monero daemon configuration. @@ -42,9 +48,14 @@ pub struct Config { } impl Config { + /// Default configuration file path. + pub const DEFAULT_PATH: &'static str = "acceptxmr.yaml"; + /// Get config file path from CLI argument, env variable, or default (in /// that order). - fn get_path() -> PathBuf { + #[allow(clippy::missing_panics_doc)] + #[must_use] + pub fn get_path() -> PathBuf { let cli_matches = Command::new("AcceptXMR-Server") .arg( Arg::new("config-file") @@ -53,7 +64,7 @@ impl Config { .action(ArgAction::Set) .value_name("FILE") .env("CONFIG_FILE") - .default_value("acceptxmr.yaml") + .default_value(Self::DEFAULT_PATH) .help("Specifies the config file to use. Defaults to ./acceptxmr.yaml"), ) .get_matches(); @@ -64,16 +75,15 @@ impl Config { /// Creates config from file. If the file is not found, creates it /// and populates it from defaults. - fn from_file() -> Result { - let config_path = Self::get_path(); - let config_file = match File::open(&config_path) { + fn from_file(path: &PathBuf) -> Result { + let config_file = match File::open(path) { Ok(f) => f, Err(e) if e.kind() == IoErrorKind::NotFound => { info!( "Config file {} not found. Creating it from defaults.", - config_path.display() + path.display() ); - let mut f = File::create(config_path)?; + let mut f = File::create(path)?; let config = Config::default(); f.write_all(serde_yaml::to_string(&config)?.as_bytes())?; return Ok(config); @@ -84,7 +94,7 @@ impl Config { Ok(serde_yaml::from_reader(config_file)?) } - fn apply_env_overrides(mut self) -> Result { + fn apply_env_overrides(mut self) -> Result { // Read from dotenv file if real environment variables are not set. dotenv().ok(); @@ -118,8 +128,9 @@ impl Config { self.external_api.validate(); } - pub fn read() -> Result { - Self::from_file()?.apply_env_overrides() + /// Read config and apply environment overrides. + pub(crate) fn read(path: &PathBuf) -> Result { + Self::from_file(path)?.apply_env_overrides() } } @@ -136,6 +147,7 @@ impl Default for Config { }), ..Default::default() }, + callback: CallbackConfig::default(), wallet: WalletConfig::default(), daemon: DaemonConfig::default(), database: DatabaseConfig::default(), @@ -144,6 +156,16 @@ impl Default for Config { } } +#[derive(Error, Debug)] +pub(crate) enum ConfigError { + #[error("Failed to read config value from environement: {0}")] + Env(#[from] VarError), + #[error("Failed to read/write config file: {0}")] + Io(#[from] io::Error), + #[error("Error (de)serializing config file: {0}")] + Yaml(#[from] YamlError), +} + #[cfg(test)] mod test { use std::{ @@ -152,18 +174,16 @@ mod test { panic::catch_unwind, path::PathBuf, str::FromStr, + time::Duration, }; - use actix_web::http::Uri; + use hyper::Uri; use log::LevelFilter; use monero::{Address, PrivateKey}; use secrecy::Secret; - use super::{ - Config, DaemonConfig, DaemonLoginConfig, LoggingConfig, ServerConfig, TlsConfig, - WalletConfig, - }; - use crate::config::DatabaseConfig; + use super::{Config, DaemonConfig, LoggingConfig, ServerConfig, TlsConfig, WalletConfig}; + use crate::config::{daemon::DaemonLoginConfig, CallbackConfig, DatabaseConfig}; #[test] fn default() { @@ -189,6 +209,7 @@ mod test { }), static_dir: PathBuf::from("./server/static/"), }, + callback: CallbackConfig { queue_size: 10_000 }, wallet: WalletConfig { primary_address: None, private_viewkey: None, @@ -198,6 +219,8 @@ mod test { daemon: DaemonConfig { url: Uri::from_static("https://xmr-node.cakewallet.com:18081"), login: None, + rpc_timeout: Duration::from_secs(30), + connection_timeout: Duration::from_secs(20), }, database: DatabaseConfig { path: PathBuf::from_str("AcceptXMR_DB/").unwrap(), @@ -229,7 +252,8 @@ mod test { "tests/testdata/config/config_no_secrets.yaml", ); - let config_without_secrets = Config::from_file().unwrap(); + let config_path = Config::get_path(); + let config_without_secrets = Config::from_file(&config_path).unwrap(); assert_ne!(config_without_secrets, expected_config); catch_unwind(|| config_without_secrets.validate()) .expect_err("config without secrets should be invalid"); @@ -240,7 +264,7 @@ mod test { "ad2093a5705b9f33e6f0f0c1bc1f5f639c756cdfc168c8f2ac6127ccbdab3a03", ); env::set_var("INTERNAL_API_TOKEN", "supersecrettoken"); - let config = Config::read().unwrap(); + let config = Config::read(&config_path).unwrap(); assert_eq!(config, expected_config); config.validate(); } @@ -259,6 +283,9 @@ mod test { }), static_dir: PathBuf::from("./server/static/"), }, + callback: CallbackConfig { + queue_size: 1_000, + }, wallet: WalletConfig { primary_address: Some(Address::from_str("4613YiHLM6JMH4zejMB2zJY5TwQCxL8p65ufw8kBP5yxX9itmuGLqp1dS4tkVoTxjyH3aYhYNrtGHbQzJQP5bFus3KHVdmf").unwrap()), private_viewkey: Some(Secret::new(PrivateKey::from_str("ad2093a5705b9f33e6f0f0c1bc1f5f639c756cdfc168c8f2ac6127ccbdab3a03").unwrap().to_string())), @@ -271,6 +298,8 @@ mod test { username: "pinkpanther".to_string(), password: Some(Secret::new("supersecretpassword".to_string())), }), + rpc_timeout: Duration::from_secs(20), + connection_timeout: Duration::from_secs(10), }, database: DatabaseConfig { path: PathBuf::from_str("server/tests/AcceptXMR_DB/").unwrap(), diff --git a/server/src/config/wallet.rs b/server/src/config/wallet.rs index 612a9af..5c46fc0 100644 --- a/server/src/config/wallet.rs +++ b/server/src/config/wallet.rs @@ -3,11 +3,12 @@ use std::{ str::FromStr, }; -use anyhow::Result; use monero::{Address, PrivateKey}; use secrecy::{ExposeSecret, Secret}; use serde::{Deserialize, Serialize}; +use super::ConfigError; + #[derive(Deserialize, Debug, Serialize, Default)] #[serde(rename_all = "kebab-case")] pub struct WalletConfig { @@ -27,7 +28,7 @@ pub struct WalletConfig { } impl WalletConfig { - pub(super) fn apply_env_overrides(mut self) -> Result { + pub(super) fn apply_env_overrides(mut self) -> Result { match env::var("PRIVATE_VIEWKEY") { Ok(key) => { self.private_viewkey = Some(Secret::new(key)); diff --git a/server/src/lib.rs b/server/src/lib.rs index 32082fe..b13b6d2 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -5,41 +5,46 @@ //! This library is intended for use by the AcceptXMR-Server binary, and is not //! intended to be used on its own. -#![warn(clippy::pedantic)] -#![warn(missing_docs)] -#![warn(clippy::cargo)] -#![allow(clippy::module_name_repetitions)] - +mod callbacks; mod config; pub mod logging; mod server; +use std::{io::Error as IoError, net::SocketAddr, path::PathBuf, time::Duration}; + use acceptxmr::{storage::stores::Sqlite, PaymentGateway, PaymentGatewayBuilder}; -use futures::try_join; -use log::{debug, error, info, warn}; +use log::{debug, error, info}; use secrecy::ExposeSecret; +use server::Server; +use tokio::{join, try_join}; use crate::{ - config::Config, + callbacks::{CallbackClient, CallbackCommand, CallbackQueue}, logging::{init_logger, set_verbosity}, server::{ api::{external, internal}, new_server, }, }; +pub use crate::{config::Config, server::api}; /// Start a standalone payment gateway. pub async fn entrypoint() { init_logger(); - let config = load_config(); + // console_subscriber::init(); + + let config_path = Config::get_path(); + let config = load_config(&config_path); set_verbosity(config.logging); - let payment_gateway = build_gateway(&config); + let payment_gateway = build_gateway(&config).await; info!("Payment gateway created."); - let gateway_clone = spawn_gateway(payment_gateway).await; + let gateway_clone = spawn_gateway(payment_gateway, &config).await; - run_server(config, gateway_clone).await; + let server = build_server(&config, gateway_clone).await; + debug!("Built AcceptXMR-Server"); + run_server(server).await; } /// Loads config. @@ -48,8 +53,8 @@ pub async fn entrypoint() { /// /// Panics if the config could not be read or validated. #[must_use] -pub fn load_config() -> Config { - let config = Config::read().expect("failed to read config"); +pub fn load_config(path: &PathBuf) -> Config { + let config = Config::read(path).expect("failed to read config"); config.validate(); config @@ -60,7 +65,7 @@ pub fn load_config() -> Config { /// # Panics /// /// Panics if the payment gateway could not be built. -pub fn build_gateway(config: &Config) -> PaymentGateway { +pub async fn build_gateway(config: &Config) -> PaymentGateway { std::fs::create_dir_all(&config.database.path).expect("failed to create DB dir"); let db_path = config .database @@ -96,7 +101,7 @@ pub fn build_gateway(config: &Config) -> PaymentGateway { login.username.clone(), login .password - .as_ref() + .clone() .map(|pass| pass.expose_secret().clone()) .unwrap_or_default(), ); @@ -109,6 +114,7 @@ pub fn build_gateway(config: &Config) -> PaymentGateway { payment_gateway_builder .build() + .await .expect("failed to build payment gateway") } @@ -118,7 +124,10 @@ pub fn build_gateway(config: &Config) -> PaymentGateway { /// # Panics /// /// Panics if the payment gateway could not be run. -pub async fn spawn_gateway(payment_gateway: PaymentGateway) -> PaymentGateway { +pub async fn spawn_gateway( + payment_gateway: PaymentGateway, + config: &Config, +) -> PaymentGateway { payment_gateway .run() .await @@ -126,16 +135,35 @@ pub async fn spawn_gateway(payment_gateway: PaymentGateway) -> PaymentGa info!("Payment gateway running."); let gateway_clone = payment_gateway.clone(); + let callback_queue_size = config.callback.queue_size; // Watch for invoice updates and deal with them accordingly. - std::thread::spawn(move || { + tokio::spawn(async move { // Watch all invoice updates. let mut subscriber = payment_gateway.subscribe_all(); + info!("Subscribed to all invoice updates."); + + // Build http client for callbacks. + let callback_queue = CallbackQueue::init(CallbackClient::default(), callback_queue_size); + loop { - let Some(invoice) = subscriber.blocking_recv() else { + let Some(invoice) = subscriber.recv().await else { // TODO: Should this attempt to restart instead? panic!("Blockchain scanner crashed!") }; + debug!("Update for invoice with ID {}:\n{}", invoice.id(), &invoice); + + // Call the callback, if applicable. + if let Err(e) = callback_queue + .send(CallbackCommand::Call { + invoice: invoice.clone(), + delay: Duration::ZERO, + }) + .await + { + panic!("Callback queue closed unexpectedly before processing callback for invoice with ID {}. Cause: {}.", invoice.id(), e); + }; + // If it's confirmed or expired, we probably shouldn't bother tracking it // anymore. if (invoice.is_confirmed() && invoice.creation_height() < invoice.current_height()) @@ -145,7 +173,7 @@ pub async fn spawn_gateway(payment_gateway: PaymentGateway) -> PaymentGa "Invoice to index {} is either confirmed or expired. Removing invoice now", invoice.index() ); - if let Err(e) = payment_gateway.remove_invoice(invoice.id()) { + if let Err(e) = payment_gateway.remove_invoice(invoice.id()).await { error!("Failed to remove fully confirmed invoice: {}", e); }; } @@ -155,16 +183,60 @@ pub async fn spawn_gateway(payment_gateway: PaymentGateway) -> PaymentGa gateway_clone } +/// Build an instance of `AcceptXmrServer`. +/// +/// # Panics +/// +/// Panics if the external or internal API servers could not be created (for +/// example, if the specified port could not be bound). +pub async fn build_server( + config: &Config, + payment_gateway: PaymentGateway, +) -> AcceptXmrServer { + let (external_server, internal_server) = try_join!( + new_server( + config.external_api.clone(), + external, + payment_gateway.clone(), + ), + new_server(config.internal_api.clone(), internal, payment_gateway) + ) + .expect("failed to start internal or external API server"); + + debug!("Built API servers"); + + AcceptXmrServer { + external: external_server, + internal: internal_server, + } +} + +/// An instance of AcceptXmr-Server. +pub struct AcceptXmrServer { + external: Server, + internal: Server, +} + +impl AcceptXmrServer { + /// Return the ipv4 address of the internal API server. + /// + /// # Errors + /// + /// Returns an IO error if there was an issue getting the address. + pub fn internal_ipv4_address(&self) -> Result { + self.internal.ipv4_address() + } +} + /// Start the internal and external HTTP servers. /// /// # Panics /// /// Panics if one of the servers could not be run, or if they encounter an /// unrecoverable error while running. -pub async fn run_server(config: Config, payment_gateway: PaymentGateway) { - let external_server = new_server(&config.external_api, external, payment_gateway.clone()) - .expect("failed to start external API server"); - let internal_server = new_server(&config.internal_api, internal, payment_gateway) - .expect("failed to start internal API server"); - try_join! {external_server, internal_server}.unwrap(); +pub async fn run_server(server: AcceptXmrServer) { + let AcceptXmrServer { external, internal } = server; + let external_handle = tokio::spawn(external.serve()); + let internal_handle = tokio::spawn(internal.serve()); + let _ = join! {external_handle, internal_handle}; } diff --git a/server/src/main.rs b/server/src/main.rs index db472c2..c671d9f 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -5,14 +5,9 @@ //! If your application requires more flexibility than `AcceptXMR-Server` //! offers, please see the [`AcceptXMR`](../library/) library instead. -#![warn(clippy::pedantic)] -#![warn(missing_docs)] -#![warn(clippy::cargo)] -#![allow(clippy::module_name_repetitions)] - use acceptxmr_server::entrypoint; -#[actix_web::main] +#[tokio::main] async fn main() { entrypoint().await; } diff --git a/server/src/server/api/external.rs b/server/src/server/api/external.rs index 4df9cc8..b245357 100644 --- a/server/src/server/api/external.rs +++ b/server/src/server/api/external.rs @@ -1,67 +1,170 @@ -use acceptxmr::{storage::stores::Sqlite, InvoiceId, PaymentGateway}; -use actix_session::Session; -use actix_web::{ - get, - http::header::{CacheControl, CacheDirective}, - web, HttpRequest, HttpResponse, +use std::borrow::Cow; + +use acceptxmr::Subscriber; +use axum::{ + debug_handler, + extract::{ + ws::{close_code, CloseFrame, Message, WebSocket}, + Query, State as AxumState, WebSocketUpgrade, + }, + http::HeaderValue, + response::IntoResponse, + routing::get, + Json, Router, }; -use actix_web_actors::ws; -use serde_json::json; +use futures::{SinkExt, StreamExt}; +use hyper::{http::header::CACHE_CONTROL, StatusCode}; +use log::{debug, error}; +use utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; -use crate::server::WebSocket; +use super::{ApiError, InvoiceIdPayload}; +use crate::server::{api::InvoiceUpdate, State}; -pub fn external(service_config: &mut web::ServiceConfig) { - service_config.service(update).service(websocket); +#[derive(OpenApi)] +#[openapi( + paths( + get_invoice_status, + websocket, + ), + components( + schemas(InvoiceIdPayload) + ), + tags( + (name = "External API", description = "AcceptXMR's user-facing API") + ) +)] +struct ApiDoc; + +pub(crate) fn external(state: State) -> Router { + Router::new() + .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi())) + .route("/invoice", get(get_invoice_status)) + .route("/invoice/ws", get(websocket)) + .with_state(state) } -// Get invoice update without waiting for websocket. -#[allow(clippy::unused_async)] -#[get("/update")] -async fn update( - session: Session, - payment_gateway: web::Data>, -) -> Result { - if let Ok(Some(invoice_id)) = session.get::("id") { - if let Ok(Some(invoice)) = payment_gateway.get_invoice(invoice_id) { - return Ok(HttpResponse::Ok() - .append_header(CacheControl(vec![CacheDirective::NoStore])) - .json(json!( - { - "address": invoice.address(), - "amount_paid": invoice.amount_paid(), - "amount_requested": invoice.amount_requested(), - "uri": invoice.uri(), - "confirmations": invoice.confirmations(), - "confirmations_required": invoice.confirmations_required(), - "expiration_in": invoice.expiration_in(), - } - ))); - }; +/// Get invoice status. +/// +/// Get invoice status by ID with query param. +#[utoipa::path( + get, + path = "/invoice", + params( + InvoiceIdPayload + ), + responses( + (status = 200, description = "Status of invoice", body = InvoiceUpdate) + ) +)] +#[debug_handler(state = State)] +async fn get_invoice_status( + query: Query, + AxumState(state): AxumState, +) -> Result { + match state.payment_gateway.get_invoice(query.invoice_id).await { + Ok(Some(invoice)) => Ok(( + [(CACHE_CONTROL, HeaderValue::from_static("no-store"))], + Json(InvoiceUpdate::from(invoice)), + )), + Ok(None) => Err(ApiError::InvoiceNotFound(query.invoice_id)), + Err(e) => { + error!("Error getting invoice status: {e}"); + Err(ApiError::AcceptXmr(e)) + } } - Ok(HttpResponse::Gone() - .append_header(CacheControl(vec![CacheDirective::NoStore])) - .finish()) } -/// WebSocket rout. -#[allow(clippy::unused_async)] -#[get("/ws/")] +/// Subscribe to an invoice. +/// +/// Subscribe to an invoice's updates via websocket. +#[utoipa::path( + get, + path = "/invoice/ws", + params( + InvoiceIdPayload + ), + responses( + (status = 200, description = "Status of invoice", body = InvoiceUpdate) // TODO: What does this actually return? + ) +)] +#[debug_handler(state = State)] async fn websocket( - session: Session, - req: HttpRequest, - stream: web::Payload, - payment_gateway: web::Data>, -) -> Result { - let Ok(Some(invoice_id)) = session.get::("id") else { - return Ok(HttpResponse::NotFound() - .append_header(CacheControl(vec![CacheDirective::NoStore])) - .finish()); + query: Query, + ws: WebSocketUpgrade, + AxumState(state): AxumState, +) -> Result { + let Some(subscriber) = state.payment_gateway.subscribe(query.invoice_id) else { + return Ok(StatusCode::NOT_FOUND.into_response()); }; - let Some(subscriber) = payment_gateway.subscribe(invoice_id) else { - return Ok(HttpResponse::NotFound() - .append_header(CacheControl(vec![CacheDirective::NoStore])) - .finish()); - }; - let websocket = WebSocket::new(subscriber); - ws::start(websocket, &req, stream) + Ok(ws.on_upgrade(|socket| handle_websocket(socket, subscriber))) +} + +#[allow(clippy::unused_async)] +async fn handle_websocket(socket: WebSocket, mut subscriber: Subscriber) { + let (mut sender, mut receiver) = socket.split(); + + tokio::spawn(async move { + while let Some(message) = receiver.next().await { + match message { + Ok(Message::Ping(_)) => {} + Ok(Message::Close(cf)) => { + let close_message = if let Some(msg) = cf { + format!(": {}", msg.reason) + } else { + String::default() + }; + debug!("Websocket client closed the connection{}.", close_message); + } + Ok(m) => { + debug!("Unexpected message from websocket client: {m:?}"); + } + Err(e) => { + error!("Error receiving websocket message: {e}"); + } + } + } + }); + + tokio::spawn(async move { + while let Some(invoice) = subscriber.recv().await { + match sender + .send(Message::Text(Json(invoice.clone()).to_string())) + .await + { + Ok(()) => {} + Err(e) => { + error!("Error sending invoice update: {e}"); + } + } + // If the invoice is confirmed or expired, stop checking for updates. + if invoice.is_confirmed() { + if let Err(e) = sender + .send(Message::Close(Some(CloseFrame { + code: close_code::NORMAL, + reason: Cow::Borrowed("Invoice Complete"), + }))) + .await + { + error!("Error sending websocket close message after invoice confirmation: {e}"); + }; + if let Err(e) = sender.close().await { + error!("Error closing websocket after invoice confirmation: {e}"); + }; + } else if invoice.is_expired() { + if let Err(e) = sender + .send(Message::Close(Some(CloseFrame { + code: close_code::NORMAL, + reason: Cow::Borrowed("Invoice Expired"), + }))) + .await + { + error!("Error sending websocket close message after invoice expiration: {e}"); + }; + if let Err(e) = sender.close().await { + error!("Error closing websocket after invoice expiration: {e}"); + }; + } + } + }); } diff --git a/server/src/server/api/internal.rs b/server/src/server/api/internal.rs index fd74296..b227b05 100644 --- a/server/src/server/api/internal.rs +++ b/server/src/server/api/internal.rs @@ -1,35 +1,92 @@ -use acceptxmr::{storage::stores::Sqlite, PaymentGateway}; -use actix_session::Session; -use actix_web::{ - http::header::{CacheControl, CacheDirective}, - post, web, HttpResponse, +use std::str::FromStr; + +use axum::{ + extract::State as AxumState, http::HeaderValue, response::IntoResponse, routing::post, Json, + Router, }; +use hyper::http::{header::CACHE_CONTROL, Uri}; use log::debug; use serde::Deserialize; +use utoipa::{IntoParams, OpenApi, ToSchema}; +use utoipa_swagger_ui::SwaggerUi; + +use crate::server::{ + api::{ApiError, InvoiceDescription, InvoiceIdPayload}, + State, +}; -pub fn internal(cfg: &mut web::ServiceConfig) { - cfg.service(checkout); +#[derive(OpenApi)] +#[openapi( + paths( + new_invoice, + ), + components( + schemas(InvoiceIdPayload, NewInvoiceParams) + ), + tags( + (name = "Internal API", description = "AcceptXMR's non-user-facing API") + ) +)] +struct ApiDoc; + +pub(crate) fn internal(state: State) -> Router { + Router::new() + .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi())) + .route("/invoice", post(new_invoice)) + .with_state(state) } -#[derive(Deserialize)] -struct CheckoutInfo { +#[derive(Deserialize, ToSchema, IntoParams)] +struct NewInvoiceParams { + piconeros_due: u64, + confirmations_required: u64, + expiration_in: u64, message: String, + callback: Option, } -/// Create new invoice and place cookie. -#[allow(clippy::unused_async)] -#[post("/checkout")] -async fn checkout( - session: Session, - checkout_info: web::Json, - payment_gateway: web::Data>, -) -> Result { - let invoice_id = payment_gateway - .new_invoice(1_000_000_000, 2, 5, checkout_info.message.clone()) - .unwrap(); - session.insert("id", invoice_id)?; - debug!("Checkout out successfully. Invoice ID: {}", invoice_id); - Ok(HttpResponse::Ok() - .append_header(CacheControl(vec![CacheDirective::NoStore])) - .finish()) +/// Create a new invoice. +/// +/// Create a new invoice with the provided details. Returns the ID of the new +/// invoice. +#[utoipa::path( + post, + path = "/invoice", + params( + NewInvoiceParams + ), + responses( + (status = 200, description = "Created a new invoice", body = InvoiceId) + ) +)] +async fn new_invoice( + AxumState(state): AxumState, + Json(payload): Json, +) -> Result { + // If there's a callback, check that it is valid. + if let Some(callback) = &payload.callback { + let _uri = Uri::from_str(callback).map_err(ApiError::InvalidCallback)?; + } + + let invoice_id = state + .payment_gateway + .new_invoice( + payload.piconeros_due, + payload.confirmations_required, + payload.expiration_in, + serde_json::to_string(&InvoiceDescription { + message: payload.message.clone(), + callback: payload.callback.clone(), + }) + .map_err(ApiError::DescriptionSerialization)?, + ) + .await?; + debug!( + "Created new invoice successfully. Invoice ID: {}", + invoice_id + ); + Ok(( + [(CACHE_CONTROL, HeaderValue::from_static("no-store"))], + Json(InvoiceIdPayload::from(invoice_id)), + )) } diff --git a/server/src/server/api/mod.rs b/server/src/server/api/mod.rs index 41902f0..42a19c1 100644 --- a/server/src/server/api/mod.rs +++ b/server/src/server/api/mod.rs @@ -1,5 +1,136 @@ +//! The AcceptXMR-Server HTTP API. + mod external; -mod internal; +pub(crate) mod internal; + +use acceptxmr::{AcceptXmrError, Invoice, InvoiceId}; +use axum::response::{IntoResponse, Response}; +pub(crate) use external::external; +use hyper::{ + http::{uri::InvalidUri, Error as HttpError}, + StatusCode, +}; +pub(crate) use internal::internal; +use log::error; +use serde::{Deserialize, Serialize}; +use serde_json::Error as JsonError; +use thiserror::Error; +use utoipa::{IntoParams, ToSchema}; + +#[derive(Deserialize, ToSchema, IntoParams, Serialize)] +struct InvoiceIdPayload { + #[schema(example = 4353165978)] + invoice_id: InvoiceId, +} + +impl From for InvoiceIdPayload { + fn from(value: InvoiceId) -> Self { + Self { invoice_id: value } + } +} + +/// An invoice update meant to be sent over the HTTP API. +#[derive(Serialize, Deserialize)] +pub struct InvoiceUpdate { + /// The ID of the invoice. + pub id: InvoiceId, + /// The XMR address. + pub address: String, + /// The payment URI. + pub uri: String, + /// The amount requested in piconeros. + pub amount_requested: u64, + /// The amount paid in piconeros. + pub amount_paid: u64, + /// The number of confirmations required. + pub confirmations_required: u64, + /// The number of confirmations received, or `None` if the invoice is fully + /// paid yet. + pub confirmations: Option, + /// The number of blocks until invoice exiration. + pub expiration_in: u64, + /// The current block height of the payment gateway. + pub current_height: u64, + /// The message associated with the invoice. + pub message: String, + /// The callback associated with the invoice. + pub callback: Option, +} + +impl From for InvoiceUpdate { + fn from(value: Invoice) -> Self { + let InvoiceDescription { message, callback } = + InvoiceDescription::from_json_or_any(value.description().to_string()); + + InvoiceUpdate { + id: value.id(), + address: value.address().to_string(), + uri: value.uri(), + amount_requested: value.amount_requested(), + amount_paid: value.amount_paid(), + confirmations_required: value.confirmations_required(), + confirmations: value.confirmations(), + expiration_in: value.expiration_in(), + current_height: value.current_height(), + message, + callback, + } + } +} + +#[derive(Deserialize, Serialize)] +pub(crate) struct InvoiceDescription { + pub(crate) message: String, + pub(crate) callback: Option, +} + +impl InvoiceDescription { + /// Attempt to deserialize from json string. On failure, assume the + /// description is a message only. + pub(crate) fn from_json_or_any(value: String) -> Self { + serde_json::from_str(&value).unwrap_or(InvoiceDescription { + message: value, + callback: None, + }) + } +} + +#[derive(Error, Debug)] +enum ApiError { + #[error(transparent)] + AcceptXmr(#[from] AcceptXmrError), + #[error("failed to serialize invoice description: {0}")] + DescriptionSerialization(JsonError), + #[error("invalid callback URI: {0}")] + InvalidCallback(InvalidUri), + #[error("failed to build HTTP response: {0}")] + InvalidResponse(#[from] HttpError), + #[error("invoice with ID {0} not found")] + InvoiceNotFound(InvoiceId), +} + +impl ApiError { + fn status_code(&self) -> StatusCode { + match self { + Self::AcceptXmr(_) | Self::InvalidResponse(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::DescriptionSerialization(_) | Self::InvalidCallback(_) => StatusCode::BAD_REQUEST, + Self::InvoiceNotFound(_) => StatusCode::NOT_FOUND, + } + } + + fn message(&self) -> &'static str { + match self { + Self::AcceptXmr(_) => "Internal payment gateway error", + Self::DescriptionSerialization(_) => "Failed to serialize invoice description", + Self::InvalidCallback(_) => "Callback is not a valid URI", + Self::InvalidResponse(_) => "Failed to build HTTP response", + Self::InvoiceNotFound(_) => "invoice not found", + } + } +} -pub use external::external; -pub use internal::internal; +impl IntoResponse for ApiError { + fn into_response(self) -> Response { + (self.status_code(), self.message()).into_response() + } +} diff --git a/server/src/server/auth.rs b/server/src/server/auth.rs index 89537d4..1ddb5dc 100644 --- a/server/src/server/auth.rs +++ b/server/src/server/auth.rs @@ -1,34 +1,60 @@ -use actix_web::{dev::ServiceRequest, web, Error as ActixError}; -use actix_web_httpauth::extractors::{ - bearer::{self, BearerAuth}, - AuthenticationError, -}; -use log::{debug, trace, warn}; -use secrecy::ExposeSecret; +use std::marker::PhantomData; -use crate::config::ServerConfig; +use axum::body::HttpBody; +use hyper::http::{header, Request, Response, StatusCode}; +use log::{debug, trace}; +use secrecy::{ExposeSecret, Secret}; +use tower_http::validate_request::ValidateRequest; -#[allow(clippy::unused_async)] -pub async fn bearer_auth_validator( - req: ServiceRequest, - credentials: BearerAuth, -) -> Result { - if let Some(server_config) = req.app_data::>() { - if let Some(expected_token) = &server_config.token { - if credentials.token() != expected_token.expose_secret() { - let bearer_config = req - .app_data::() - .cloned() - .unwrap_or_default(); +pub(crate) struct MaybeBearer { + token: Option>, + _ty: PhantomData ResBody>, +} + +impl MaybeBearer { + pub(crate) fn new(token: Option>) -> Self { + Self { + token, + _ty: PhantomData, + } + } +} + +impl Clone for MaybeBearer { + fn clone(&self) -> Self { + Self { + token: self.token.clone(), + _ty: PhantomData, + } + } +} + +impl ValidateRequest for MaybeBearer +where + ResBody: HttpBody + Default, +{ + type ResponseBody = ResBody; - debug!("Authentication denied. Bearer auth token mismatch."); - return Err((AuthenticationError::from(bearer_config).into(), req)); + fn validate(&mut self, request: &mut Request) -> Result<(), Response> { + if let Some(token) = &self.token { + match request.headers().get(header::AUTHORIZATION) { + Some(actual) if actual == &format!("Bearer {}", token.expose_secret()) => Ok(()), + Some(_) => { + debug!("Authentication denied. Bearer auth token mismatch."); + let mut res = Response::new(ResBody::default()); + *res.status_mut() = StatusCode::UNAUTHORIZED; + Err(res) + } + None => { + debug!("Authentication denied. Bearer auth token missing."); + let mut res = Response::new(ResBody::default()); + *res.status_mut() = StatusCode::UNAUTHORIZED; + Err(res) + } } } else { trace!("Bearer auth token not set. Not enforcing bearer auth."); + Ok(()) } - } else { - warn!("No server configuration found while attempting to evaluate bearer auth policy."); } - Ok(req) } diff --git a/server/src/server/mod.rs b/server/src/server/mod.rs index 122915f..6fcf2c9 100644 --- a/server/src/server/mod.rs +++ b/server/src/server/mod.rs @@ -1,73 +1,160 @@ pub mod api; mod auth; +mod state; mod tls; -mod websocket; -use acceptxmr::{storage::stores::Sqlite, PaymentGateway}; -use actix_files::Files; -use actix_web::{ - dev::Server, - middleware::Condition, - web::{Data, ServiceConfig}, - App, HttpServer, +use std::{ + io::Error as IoError, + net::{SocketAddr, SocketAddrV4, SocketAddrV6}, + pin::Pin, }; -use actix_web_httpauth::middleware::HttpAuthentication; -use auth::bearer_auth_validator; -use log::{debug, info}; + +use acceptxmr::{storage::stores::Sqlite, PaymentGateway}; +use axum::{extract::Request, Router}; +use futures_util::pin_mut; +use hyper::body::Incoming; +use hyper_util::rt::{TokioExecutor, TokioIo}; +use log::{debug, error, info}; +use state::State; use tls::prepare_tls; -use websocket::WebSocket; +use tokio::{ + io::{AsyncRead, AsyncWrite}, + join, + net::TcpListener, +}; +use tokio_rustls::TlsAcceptor; +use tower::Service; +use tower_http::{services::ServeDir, validate_request::ValidateRequestHeaderLayer}; use super::config::ServerConfig; +use crate::server::auth::MaybeBearer; -pub fn new_server( - server_config: &ServerConfig, - service: F, +pub(crate) async fn new_server( + server_config: ServerConfig, + api: F, payment_gateway: PaymentGateway, ) -> std::io::Result where - F: FnOnce(&mut ServiceConfig) + Copy + Send + 'static, + F: Fn(State) -> Router, { - let bearer_auth_enabled = server_config.token.is_some(); - let shared_payment_gateway = Data::new(payment_gateway); - let shared_config = Data::new(server_config.clone()); let static_dir = server_config.static_dir.clone(); - let mut server_builder = HttpServer::new(move || { - App::new() - .app_data(shared_payment_gateway.clone()) - .app_data(shared_config.clone()) - .configure(service) - .service(Files::new("", static_dir.clone()).index_file("index.html")) - .wrap(Condition::new( - bearer_auth_enabled, - HttpAuthentication::bearer(bearer_auth_validator), - )) - }); - // Enable TLS. - server_builder = if let Some(tls) = &server_config.tls { - info!( - "Binding with TLS to {}:{}", - server_config.ipv4, server_config.port - ); - let rustls_config = prepare_tls(tls); - // Enable IPv6. - if let Some(ipv6) = server_config.ipv6 { - server_builder = server_builder - .bind_rustls_021((ipv6, server_config.port), rustls_config.clone())?; - debug!("Bound to: {:?}", server_builder.addrs()); + + let state = State::new(payment_gateway); + let router = api(state); + + let router = Router::new() + .merge(router) + .fallback_service(ServeDir::new(static_dir)) + .layer(ValidateRequestHeaderLayer::custom(MaybeBearer::new( + server_config.token, + ))); + + debug!("Binding to {}:{}", server_config.ipv4, server_config.port); + let tcp_v4_listener: TcpListener = TcpListener::bind::(SocketAddrV4::new( + server_config.ipv4, + server_config.port, + )) + .await?; + if let Ok(v4_addr) = tcp_v4_listener.local_addr() { + info!("Bound to: {:?}", v4_addr); + } + let tcp_v6_listener = if let Some(ipv6) = server_config.ipv6 { + debug!("Binding to {}:{}", ipv6, server_config.port); + let listener = + TcpListener::bind::(SocketAddrV6::new(ipv6, server_config.port, 0, 0)) + .await?; + if let Ok(v6_addr) = listener.local_addr() { + info!("Bound to: {:?}", v6_addr); } - server_builder.bind_rustls_021((server_config.ipv4, server_config.port), rustls_config)? + Some(listener) } else { - info!( - "Binding without TLS to {}:{}", - server_config.ipv4, server_config.port - ); - // Enable IPv6. - if let Some(ipv6) = server_config.ipv6 { - server_builder = server_builder.bind((ipv6, server_config.port))?; - debug!("Bound to: {:?}", server_builder.addrs()); - } - server_builder.bind((server_config.ipv4, server_config.port))? + None }; - debug!("Bound to: {:?}", server_builder.addrs()); - Ok(server_builder.run()) + + let tls_acceptor = if let Some(tls) = &server_config.tls { + let rustls_config = prepare_tls(tls); + Some(TlsAcceptor::from(rustls_config)) + } else { + None + }; + + Ok(Server { + ipv4: tcp_v4_listener, + ipv6: tcp_v6_listener, + tls: tls_acceptor, + router, + }) } + +pub(crate) struct Server { + ipv4: TcpListener, + ipv6: Option, + tls: Option, + router: Router, +} + +impl Server { + pub(crate) async fn serve(self) { + if let Some(ipv6) = &self.ipv6 { + join!(self.serve_inner(&self.ipv4), self.serve_inner(ipv6)); + } else { + self.serve_inner(&self.ipv4).await; + } + } + + async fn serve_inner(&self, listener: &TcpListener) { + pin_mut!(listener); + loop { + let tower_service = self.router.clone(); + let tls_acceptor = self.tls.clone(); + + // Wait for new tcp connection. + // TODO: Bound the number of open connections somehow. + let (cnx, addr) = listener.accept().await.unwrap(); + + tokio::spawn(async move { + // Wait for tls handshake to happen. + let stream: TokioIo>> = if let Some(tls) = tls_acceptor + { + match tls.accept(cnx).await { + Ok(s) => TokioIo::new(Box::pin(s)), + Err(e) => { + error!("Error during tls handshake connection from {}: {e}", addr); + return; + } + } + } else { + TokioIo::new(Box::pin(cnx)) + }; + + // Hyper also has its own `Service` trait and doesn't use tower. We can use + // `hyper::service::service_fn` to create a hyper `Service` that calls our app + // through `tower::Service::call`. + let hyper_service = + hyper::service::service_fn(move |request: Request| { + // We have to clone `tower_service` because hyper's `Service` uses `&self` + // whereas tower's `Service` requires `&mut self`. + // + // We don't need to call `poll_ready` since `Router` is always ready. + tower_service.clone().call(request) + }); + + let ret = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new()) + .serve_connection_with_upgrades(stream, hyper_service) + .await; + + if let Err(err) = ret { + error!("Error serving connection from {}: {}", addr, err); + } + }); + } + } + + pub(crate) fn ipv4_address(&self) -> Result { + self.ipv4.local_addr() + } +} + +trait TokioReadWrite: AsyncRead + AsyncWrite + Send {} + +impl TokioReadWrite for T where T: AsyncRead + AsyncWrite + Send {} diff --git a/server/src/server/state.rs b/server/src/server/state.rs new file mode 100644 index 0000000..3a1281c --- /dev/null +++ b/server/src/server/state.rs @@ -0,0 +1,29 @@ +use std::{ops::Deref, sync::Arc}; + +use acceptxmr::{storage::stores::Sqlite, PaymentGateway}; + +pub(crate) struct State(Arc); + +impl State { + pub(crate) fn new(payment_gateway: PaymentGateway) -> Self { + Self(Arc::new(StateInner { payment_gateway })) + } +} + +pub(crate) struct StateInner { + pub(crate) payment_gateway: PaymentGateway, +} + +impl Deref for State { + type Target = StateInner; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Clone for State { + fn clone(&self) -> Self { + State(self.0.clone()) + } +} diff --git a/server/src/server/tls.rs b/server/src/server/tls.rs index 435fad4..fef22e1 100644 --- a/server/src/server/tls.rs +++ b/server/src/server/tls.rs @@ -1,21 +1,23 @@ use std::{ fs::{create_dir_all, File}, io::{BufReader, Write}, + sync::Arc, }; use log::warn; use rcgen::generate_simple_self_signed; -use rustls::{Certificate, PrivateKey as RustlsPrivateKey, ServerConfig as RustlsServerConfig}; +use rustls::{ + pki_types::{CertificateDer, PrivatePkcs8KeyDer as RustlsPrivateKey}, + ServerConfig as RustlsServerConfig, +}; use rustls_pemfile::{certs, pkcs8_private_keys}; use crate::config::TlsConfig; // Attempt to load TLS, falling back on self-signed certificate if necessary. -pub fn prepare_tls(config: &TlsConfig) -> RustlsServerConfig { +pub(crate) fn prepare_tls(config: &TlsConfig) -> Arc { // Init server config builder with safe defaults. - let rustls_config = RustlsServerConfig::builder() - .with_safe_defaults() - .with_no_client_auth(); + let rustls_config = RustlsServerConfig::builder().with_no_client_auth(); let (cert_chain, mut keys) = match (config.cert.try_exists(), config.key.try_exists()) { (Ok(true), Ok(true)) => load_tls(config), @@ -36,28 +38,25 @@ pub fn prepare_tls(config: &TlsConfig) -> RustlsServerConfig { std::process::exit(1); } - rustls_config - .with_single_cert(cert_chain, keys.remove(0)) - .unwrap() + Arc::new( + rustls_config + .with_single_cert( + cert_chain, + rustls::pki_types::PrivateKeyDer::Pkcs8(keys.remove(0)), + ) + .unwrap(), + ) } -fn load_tls(config: &TlsConfig) -> (Vec, Vec) { +fn load_tls<'b>(config: &TlsConfig) -> (Vec>, Vec>) { let cert_file = &mut BufReader::new(File::open(&config.cert).expect("failed to load TLS certificate file")); let key_file = &mut BufReader::new(File::open(&config.key).expect("failed to load TLS key file")); // Convert files to key/cert objects - let cert_chain = certs(cert_file) - .unwrap() - .into_iter() - .map(Certificate) - .collect(); - let keys: Vec = pkcs8_private_keys(key_file) - .unwrap() - .into_iter() - .map(RustlsPrivateKey) - .collect(); + let cert_chain = certs(cert_file).map(Result::unwrap).collect(); + let keys: Vec = pkcs8_private_keys(key_file).map(Result::unwrap).collect(); (cert_chain, keys) } diff --git a/server/src/server/websocket.rs b/server/src/server/websocket.rs deleted file mode 100644 index 47eead8..0000000 --- a/server/src/server/websocket.rs +++ /dev/null @@ -1,132 +0,0 @@ -use std::{ - future::Future, - pin::Pin, - task::Poll, - time::{Duration, Instant}, -}; - -use acceptxmr::{Invoice, Subscriber}; -use actix::{prelude::Stream, Actor, ActorContext, AsyncContext, StreamHandler}; -use actix_web_actors::ws; -use bytestring::ByteString; -use log::{debug, warn}; -use serde_json::json; - -/// Time before lack of client response causes a timeout. -const CLIENT_TIMEOUT: Duration = Duration::from_secs(10); -/// Time between sending heartbeat pings. -const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(1); - -/// Define websocket HTTP actor -pub struct WebSocket { - last_heartbeat: Instant, - invoice_subscriber: Option, -} - -impl WebSocket { - pub fn new(invoice_subscriber: Subscriber) -> Self { - Self { - last_heartbeat: Instant::now(), - invoice_subscriber: Some(invoice_subscriber), - } - } - - /// Sends ping to client every `HEARTBEAT_INTERVAL` and checks for responses - /// from client - fn heartbeat(ctx: &mut ::Context) { - ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| { - // check client heartbeats - if Instant::now().duration_since(act.last_heartbeat) > CLIENT_TIMEOUT { - // heartbeat timed out - warn!("Websocket Client heartbeat failed, disconnecting!"); - ctx.stop(); - return; - } - ctx.ping(b""); - }); - } -} - -impl Actor for WebSocket { - type Context = ws::WebsocketContext; - - /// This method is called on actor start. We add the invoice subscriber as a - /// stream here, and start heartbeat checks as well. - fn started(&mut self, ctx: &mut Self::Context) { - if let Some(subscriber) = self.invoice_subscriber.take() { - >::add_stream(InvoiceStream(subscriber), ctx); - } - Self::heartbeat(ctx); - } -} - -/// Handle incoming websocket messages. -impl StreamHandler> for WebSocket { - fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { - match msg { - Ok(ws::Message::Pong(_)) => { - self.last_heartbeat = Instant::now(); - } - Ok(ws::Message::Close(reason)) => { - match &reason { - Some(r) => debug!("Websocket client closing: {:#?}", r.description), - None => debug!("Websocket client closing"), - } - ctx.close(reason); - ctx.stop(); - } - Ok(m) => debug!("Received unexpected message from websocket client: {:?}", m), - Err(e) => warn!("Received error from websocket client: {:?}", e), - } - } -} - -/// Handle incoming invoice updates. -impl StreamHandler for WebSocket { - fn handle(&mut self, invoice_update: Invoice, ctx: &mut Self::Context) { - // Send the update to the user. - ctx.text(ByteString::from( - json!( - { - "address": invoice_update.address(), - "amount_paid": invoice_update.amount_paid(), - "amount_requested": invoice_update.amount_requested(), - "uri": invoice_update.uri(), - "confirmations": invoice_update.confirmations(), - "confirmations_required": invoice_update.confirmations_required(), - "expiration_in": invoice_update.expiration_in(), - } - ) - .to_string(), - )); - // If the invoice is confirmed or expired, stop checking for updates. - if invoice_update.is_confirmed() { - ctx.close(Some(ws::CloseReason::from(( - ws::CloseCode::Normal, - "Invoice Complete", - )))); - ctx.stop(); - } else if invoice_update.is_expired() { - ctx.close(Some(ws::CloseReason::from(( - ws::CloseCode::Normal, - "Invoice Expired", - )))); - ctx.stop(); - } - } -} - -// Wrapping `Subscriber` and implementing `Stream` on the wrapper allows us to -// use it as an efficient asynchronous stream for the Actix websocket. -struct InvoiceStream(Subscriber); - -impl Stream for InvoiceStream { - type Item = Invoice; - - fn poll_next( - mut self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> Poll> { - Pin::new(&mut self.0).poll(cx) - } -} diff --git a/server/tests/common/mod.rs b/server/tests/common/mod.rs index cab8bd3..1927f29 100644 --- a/server/tests/common/mod.rs +++ b/server/tests/common/mod.rs @@ -1,35 +1,51 @@ -use std::{sync::Arc, time::Duration}; +use std::{ + net::SocketAddr, + str::FromStr, + sync::{Arc, Mutex, PoisonError}, + time::Duration, +}; -use http::header::{ACCEPT, CONTENT_TYPE}; +use acceptxmr_server::api::InvoiceUpdate; +use bytes::Bytes; +use http_body_util::{BodyExt, Empty, Full}; use hyper::{ - client::connect::HttpConnector, header::AUTHORIZATION, Body, Method, Request, Response, Uri, + body::Incoming, + header::AUTHORIZATION, + http::header::{ACCEPT, CONTENT_TYPE}, + service::service_fn, + Error as HyperError, Method, Request, Response, StatusCode, Uri, }; use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; -use log::{debug, error, LevelFilter}; +use hyper_util::{ + client::legacy::{connect::HttpConnector, Client}, + rt::{TokioExecutor, TokioIo}, + server::conn::auto::Builder as ServerBuilder, +}; +use log::{debug, error, info}; use rustls::{ - client::{ServerCertVerified, ServerCertVerifier}, - ClientConfig, + client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, + pki_types::{CertificateDer, ServerName, UnixTime}, + ClientConfig, SignatureScheme, }; use serde::{Deserialize, Serialize}; use thiserror::Error; -use tokio::time::{error, timeout}; - -pub const PRIVATE_VIEW_KEY: &str = - "ad2093a5705b9f33e6f0f0c1bc1f5f639c756cdfc168c8f2ac6127ccbdab3a03"; -pub const PRIMARY_ADDRESS: &str = - "4613YiHLM6JMH4zejMB2zJY5TwQCxL8p65ufw8kBP5yxX9itmuGLqp1dS4tkVoTxjyH3aYhYNrtGHbQzJQP5bFus3KHVdmf"; +use tokio::{ + net::TcpListener, + sync::mpsc::{self, Receiver, Sender}, + time::{error, error::Elapsed, timeout}, +}; #[derive(Debug, Clone)] -pub struct GatewayClient { - client: hyper::Client>, - pub url: Uri, - timeout: Duration, - pub token: Option, +pub(crate) struct GatewayClient { + client: Client, Full>, + pub(crate) url: Uri, + pub(crate) timeout: Duration, + pub(crate) token: Option, } impl GatewayClient { /// Returns a payment gateway client. - pub fn new( + pub(crate) fn new( url: Uri, total_timeout: Duration, connection_timeout: Duration, @@ -42,7 +58,7 @@ impl GatewayClient { let rustls_connector = HttpsConnectorBuilder::new() .with_tls_config( ClientConfig::builder() - .with_safe_defaults() + .dangerous() .with_custom_certificate_verifier(Arc::new(NoCertVerifier {})) .with_no_client_auth(), ) @@ -50,7 +66,7 @@ impl GatewayClient { .enable_http1() .enable_http2() .wrap_connector(hyper_connector); - let client = hyper::Client::builder().build(rustls_connector); + let client = Client::builder(TokioExecutor::new()).build(rustls_connector); GatewayClient { client, @@ -60,62 +76,68 @@ impl GatewayClient { } } - pub async fn request(&self, body: &str, endpoint: &str) -> Result, ClientError> { + pub(crate) async fn request( + &self, + body: &str, + endpoint: &str, + ) -> Result, ClientError> { let mut response = None; timeout(self.timeout, async { - loop { - let mut request_builder = Request::builder() - .method(Method::POST) - .header(ACCEPT, "*/*") - .header(CONTENT_TYPE, "application/json") - .uri(self.url.clone().to_string() + endpoint); - - if let Some(token) = &self.token { - request_builder = - request_builder.header(AUTHORIZATION, format!("Bearer {}", token)); - } + let mut request_builder = Request::builder() + .method(Method::POST) + .header(ACCEPT, "*/*") + .header(CONTENT_TYPE, "application/json") + .uri(self.url.clone().to_string() + endpoint); + + if let Some(token) = &self.token { + request_builder = request_builder.header(AUTHORIZATION, format!("Bearer {token}")); + } - let request = match request_builder.body(Body::from(body.to_string())) { + let request = + match request_builder.body(Full::new(Bytes::copy_from_slice(body.as_bytes()))) { Ok(r) => r, Err(e) => { response = Some(Err(e.into())); - break; + return; } }; - debug!("Sending request: {:?}", request); + debug!("Sending request: {:?}", request); - match self.client.request(request).await { - Ok(r) => { - response = Some(Ok(r)); - break; - } - Err(e) if e.is_connect() => { - error!("Error connecting to gateway, retrying: {}", e); - continue; - } - Err(e) => { - error!("Checkout response contains an error: {}", e); - response = Some(Err(e.into())) - } - }; - } + match self.client.request(request).await { + Ok(r) if r.status().is_server_error() | r.status().is_client_error() => { + error!( + "Response contains an error. Status code: {}, body: {:?}", + r.status(), + r.body() + ); + response = Some(Ok(r)); + } + Ok(r) => { + debug!("Request successful. Response: {r:?}"); + response = Some(Ok(r)); + } + Err(e) => { + error!("Response contains an error: {}", e); + response = Some(Err(ClientError::Request(Box::new(e)))); + } + }; }) .await?; response.expect("Timed out waiting for response.") } - pub async fn checkout(&self) -> Result, ClientError> { - let endpoint = "checkout"; - - #[derive(Deserialize, Serialize)] - struct CheckoutInfo { - message: String, - } + pub(crate) async fn new_invoice( + &self, + payload: MockNewInvoicePayload, + ) -> Result, ClientError> { + let endpoint = "invoice"; - let body = r#"{"message":"This is a test message"}"#; + let body = serde_json::to_value(payload) + .expect("failed to build json from new_invoice payload") + .to_string(); - self.request(body, endpoint).await + self.request(&body, endpoint).await } } @@ -123,50 +145,183 @@ impl Default for GatewayClient { fn default() -> Self { GatewayClient::new( Uri::from_static("https://localhost:8081"), - Duration::from_secs(1), - Duration::from_millis(500), + Duration::from_secs(60), + Duration::from_secs(30), Some("supersecrettoken".to_string()), ) } } -/// Initialize the logging implementation. Defaults to `Trace` verbosity for -/// `AcceptXMR` and `Warn` for dependencies. -pub fn init_logger() { - let _ = env_logger::builder() - .is_test(true) - .filter_level(LevelFilter::Debug) - .filter_module("acceptxmr", LevelFilter::Trace) - .filter_module("acceptxmr_server", LevelFilter::Trace) - .try_init(); -} - +#[derive(Debug)] struct NoCertVerifier {} impl ServerCertVerifier for NoCertVerifier { fn verify_server_cert( &self, - _end_entity: &rustls::Certificate, - _intermediates: &[rustls::Certificate], - _server_name: &rustls::ServerName, - _scts: &mut dyn Iterator, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, _ocsp_response: &[u8], - _now: std::time::SystemTime, - ) -> Result { + _now: UnixTime, + ) -> Result { Ok(ServerCertVerified::assertion()) } - fn request_scts(&self) -> bool { - false + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &rustls::pki_types::CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + vec![ + SignatureScheme::RSA_PKCS1_SHA1, + SignatureScheme::ECDSA_SHA1_Legacy, + SignatureScheme::RSA_PKCS1_SHA256, + SignatureScheme::ECDSA_NISTP256_SHA256, + SignatureScheme::RSA_PKCS1_SHA384, + SignatureScheme::ECDSA_NISTP384_SHA384, + SignatureScheme::RSA_PKCS1_SHA512, + SignatureScheme::ECDSA_NISTP521_SHA512, + SignatureScheme::RSA_PSS_SHA256, + SignatureScheme::RSA_PSS_SHA384, + SignatureScheme::RSA_PSS_SHA512, + SignatureScheme::ED25519, + SignatureScheme::ED448, + ] + } +} + +#[derive(Deserialize, Serialize)] +pub(crate) struct MockNewInvoicePayload { + pub(crate) piconeros_due: u64, + pub(crate) confirmations_required: u64, + pub(crate) expiration_in: u64, + pub(crate) message: String, + pub(crate) callback: Option, +} + +impl Default for MockNewInvoicePayload { + fn default() -> Self { + MockNewInvoicePayload { + piconeros_due: 2_234_345, + confirmations_required: 2, + expiration_in: 20, + message: "I am a test message".to_string(), + callback: Some("http://localhost:1234".to_string()), + } + } +} + +pub(crate) struct CallbackListener { + address: SocketAddr, + rx: Receiver, + tx: Sender, +} + +async fn handle( + req: Request, + tx: Sender, + rx: Arc>>, +) -> Result>, HyperError> { + let invoice: InvoiceUpdate = + serde_json::from_slice(&req.into_body().collect().await.unwrap().to_bytes()).unwrap(); + tx.send(invoice).await.unwrap(); + + // Check for any commands before responding. + if let Ok(command) = rx.lock().unwrap_or_else(PoisonError::into_inner).try_recv() { + match command { + // Send a bad gateway response as if the service were down. + ListenerCommand::FailNext => { + let mut response = Response::new(Empty::new()); + let status = response.status_mut(); + *status = StatusCode::BAD_GATEWAY; + return Ok::<_, HyperError>(response); + } + } + } + + Ok::<_, HyperError>(Response::new(Empty::new())) +} + +impl CallbackListener { + pub(crate) async fn init() -> Self { + let addr: SocketAddr = ([127, 0, 0, 1], 0).into(); + let listener = TcpListener::bind(addr).await.unwrap(); + let address = listener.local_addr().unwrap(); + + let (update_tx, update_rx) = mpsc::channel(100); + let (command_tx, command_rx) = mpsc::channel(100); + let sharable_command_rx = Arc::new(Mutex::new(command_rx)); + + info!("Callback listener bound to {}", address); + tokio::spawn(async move { + let service = service_fn(|req: Request| { + handle(req, update_tx.clone(), sharable_command_rx.clone()) + }); + + loop { + let (stream, _) = listener.accept().await.unwrap(); + let stream = TokioIo::new(stream); + + let _ = ServerBuilder::new(TokioExecutor::new()) + .serve_connection_with_upgrades(stream, service) + .await; + } + }); + + CallbackListener { + address, + rx: update_rx, + tx: command_tx, + } + } + + pub(crate) async fn recv_timeout( + &mut self, + timeout: Duration, + ) -> Result, Elapsed> { + tokio::time::timeout(timeout, self.rx.recv()).await + } + + pub(crate) fn url(&self) -> Uri { + Uri::from_str(&format!( + "http://{}:{}", + self.address.ip(), + self.address.port() + )) + .unwrap() } + + /// Artificially fail callback to test retry mechanism. + pub(crate) async fn fail_one_callback(&mut self) { + self.tx.send(ListenerCommand::FailNext).await.unwrap(); + } +} + +enum ListenerCommand { + /// Fail the next callback. + FailNext, } #[derive(Error, Debug)] -pub enum ClientError { +pub(crate) enum ClientError { #[error("HTTP request failed: {0}")] - Http(#[from] hyper::Error), + Request(Box), #[error("failed to build HTTP request: {0}")] - Request(#[from] hyper::http::Error), + InvalidRequest(#[from] hyper::http::Error), #[error("HTTP request timed out: {0}")] Timeout(#[from] error::Elapsed), #[error("failed to interpret response body as json: {0}")] diff --git a/server/tests/integration_tests/bearer_auth.rs b/server/tests/integration_tests/bearer_auth.rs index 5ca8dd3..bcdba8a 100644 --- a/server/tests/integration_tests/bearer_auth.rs +++ b/server/tests/integration_tests/bearer_auth.rs @@ -1,10 +1,13 @@ +use std::{path::PathBuf, str::FromStr}; + use acceptxmr::{storage::stores::Sqlite, PaymentGatewayBuilder}; -use acceptxmr_server::{load_config, run_server}; -use hyper::StatusCode; +use acceptxmr_server::{build_server, load_config, run_server, Config}; +use hyper::{http::Uri, StatusCode}; use log::{debug, info}; use test_case::test_case; +use testing_utils::{init_logger, PRIMARY_ADDRESS, PRIVATE_VIEW_KEY}; -use crate::common::{init_logger, GatewayClient, PRIMARY_ADDRESS, PRIVATE_VIEW_KEY}; +use crate::common::{GatewayClient, MockNewInvoicePayload}; #[test_case(Some("supersecrettoken") => StatusCode::OK; "Correct token")] #[test_case(Some("I am the wrong token!") => StatusCode::UNAUTHORIZED; "Wrong token")] @@ -20,22 +23,27 @@ async fn bearer_auth(token: Option<&str>) -> StatusCode { store, ) .build() + .await .unwrap(); info!("Payment gateway created."); // Deliberately not starting the payment gateway itself, because we don't // need it for this test. - let config = load_config(); - tokio::spawn(run_server(config, payment_gateway)); + let config = load_config(&PathBuf::from(Config::DEFAULT_PATH)); + let server = build_server(&config, payment_gateway).await; + let address = server.internal_ipv4_address().unwrap(); + tokio::spawn(run_server(server)); let mut client = GatewayClient::default(); - client.token = token.map(|s| s.to_string()); - let checkout_response = client - .checkout() + client.url = Uri::from_str(&format!("https://{}:{}", address.ip(), address.port())).unwrap(); + client.token = token.map(ToString::to_string); + + let new_invoice_response = client + .new_invoice(MockNewInvoicePayload::default()) .await - .expect("failed to call `checkout` endpoint"); - debug!("Checkout response: {:?}", checkout_response); - checkout_response.status() + .expect("failed to call new invoice endpoint"); + debug!("Checkout response: {:?}", new_invoice_response); + new_invoice_response.status() } diff --git a/server/tests/integration_tests/callback.rs b/server/tests/integration_tests/callback.rs new file mode 100644 index 0000000..88ebe93 --- /dev/null +++ b/server/tests/integration_tests/callback.rs @@ -0,0 +1,137 @@ +use std::{path::PathBuf, str::FromStr, time::Duration}; + +use acceptxmr::{storage::stores::Sqlite, PaymentGatewayBuilder}; +use acceptxmr_server::{build_server, load_config, run_server, spawn_gateway, Config}; +use hyper::http::Uri; +use log::{debug, info}; +use testing_utils::{init_logger, MockDaemon, PRIMARY_ADDRESS, PRIVATE_VIEW_KEY}; + +use crate::common::{CallbackListener, GatewayClient, MockNewInvoicePayload}; + +#[tokio::test] +async fn callback() { + init_logger(); + let mock_daemon = MockDaemon::new_mock_daemon().await; + + let store = Sqlite::new(":memory:", "invoices", "output keys", "height").unwrap(); + let payment_gateway = PaymentGatewayBuilder::new( + PRIVATE_VIEW_KEY.to_string(), + PRIMARY_ADDRESS.to_string(), + store, + ) + // Faster scan rate so the update is received sooner. + .scan_interval(Duration::from_millis(100)) + .daemon_url(mock_daemon.url("")) + .seed(1) + .build() + .await + .unwrap(); + info!("Payment gateway created."); + + let config = load_config(&PathBuf::from(Config::DEFAULT_PATH)); + let payment_gateway = spawn_gateway(payment_gateway, &config).await; + + let server = build_server(&config, payment_gateway).await; + let address = server.internal_ipv4_address().unwrap(); + tokio::spawn(run_server(server)); + + let mut client = GatewayClient::default(); + client.token = Some("supersecrettoken".to_string()); + client.url = Uri::from_str(&format!("https://{}:{}", address.ip(), address.port())).unwrap(); + + let mut callback_listener = CallbackListener::init().await; + let callback_url = callback_listener.url(); + + let new_invoice_payload = MockNewInvoicePayload { + callback: Some(callback_url.to_string()), + ..Default::default() + }; + let new_invoice_response = client + .new_invoice(new_invoice_payload) + .await + .expect("failed to call `checkout` endpoint"); + debug!("Checkout response: {:?}", new_invoice_response); + + // Listen for callback from initial update. + let callback = callback_listener + .recv_timeout(Duration::from_secs(120)) + .await + .expect("timeout waiting for callback") + .expect("channel to callback listener closed"); + assert_eq!(callback.message, "I am a test message"); + assert_eq!(callback.uri, "monero:82ZZhxB2dAtGwRQSSzvc9fUfM2oFWCUBUFJUAYDsureAB57RZEXm7fyZjwVXGyDGMA3wMtZjMSzECjfbkk5jYkA1SDmWWkx?tx_amount=0.000002234345"); +} + +/// Fail the first callback and assert that it is retried. +#[tokio::test] +async fn callback_retry() { + init_logger(); + let mock_daemon = MockDaemon::new_mock_daemon().await; + + let store = Sqlite::new(":memory:", "invoices", "output keys", "height").unwrap(); + let payment_gateway = PaymentGatewayBuilder::new( + PRIVATE_VIEW_KEY.to_string(), + PRIMARY_ADDRESS.to_string(), + store, + ) + // Faster scan rate so the update is received sooner. + .scan_interval(Duration::from_millis(1000)) + .daemon_url(mock_daemon.url("")) + .seed(1) + .build() + .await + .unwrap(); + info!("Payment gateway created."); + + let config = load_config(&PathBuf::from(Config::DEFAULT_PATH)); + let payment_gateway = spawn_gateway(payment_gateway, &config).await; + + let server = build_server(&config, payment_gateway).await; + info!("Built AcceptXMR server."); + let address = server.internal_ipv4_address().unwrap(); + info!("Starting with internal address {address}"); + tokio::spawn(run_server(server)); + + let mut client = GatewayClient::default(); + client.token = Some("supersecrettoken".to_string()); + client.url = Uri::from_str(&format!("https://{}:{}", address.ip(), address.port())).unwrap(); + + info!("Starting callback listener."); + let mut callback_listener = CallbackListener::init().await; + info!("Started callback listener."); + callback_listener.fail_one_callback().await; + callback_listener.fail_one_callback().await; + let callback_url = callback_listener.url(); + + let new_invoice_payload = MockNewInvoicePayload { + callback: Some(callback_url.to_string()), + ..Default::default() + }; + info!("Creating new invoice."); + let new_invoice_response = client + .new_invoice(new_invoice_payload) + .await + .expect("failed to call `checkout` endpoint"); + info!("Checkout response: {:?}", new_invoice_response); + + // This one will have been failed. + callback_listener + .recv_timeout(Duration::from_secs(120)) + .await + .unwrap(); + + // This one will also have been failed. + callback_listener + .recv_timeout(Duration::from_secs(120)) + .await + .unwrap(); + + // Now ensure it gets retried. + let callback = callback_listener + .recv_timeout(Duration::from_secs(120)) + .await + .expect("timeout waiting for callback") + .expect("channel to callback listener closed"); + assert_eq!(callback.message, "I am a test message"); + assert_eq!(callback.uri, "monero:82ZZhxB2dAtGwRQSSzvc9fUfM2oFWCUBUFJUAYDsureAB57RZEXm7fyZjwVXGyDGMA3wMtZjMSzECjfbkk5jYkA1SDmWWkx?tx_amount=0.000002234345"); +} diff --git a/server/tests/integration_tests/mod.rs b/server/tests/integration_tests/mod.rs index 5d46ef0..ecde57e 100644 --- a/server/tests/integration_tests/mod.rs +++ b/server/tests/integration_tests/mod.rs @@ -1 +1,2 @@ mod bearer_auth; +mod callback; diff --git a/server/tests/testdata/config/config_full.yaml b/server/tests/testdata/config/config_full.yaml index a4c1d42..f106638 100644 --- a/server/tests/testdata/config/config_full.yaml +++ b/server/tests/testdata/config/config_full.yaml @@ -11,6 +11,8 @@ internal-api: cert: "/path/to/cert.pem" key: "/path/to/key.pem" static_dir: server/static/ +callback: + queue-size: 1000 wallet: primary-address: "4613YiHLM6JMH4zejMB2zJY5TwQCxL8p65ufw8kBP5yxX9itmuGLqp1dS4tkVoTxjyH3aYhYNrtGHbQzJQP5bFus3KHVdmf" private-viewkey: "ad2093a5705b9f33e6f0f0c1bc1f5f639c756cdfc168c8f2ac6127ccbdab3a03" @@ -20,6 +22,8 @@ daemon: login: username: "pinkpanther" password: "supersecretpassword" + rpc-timeout: 20 + connection-timeout: 10 database: path: "server/tests/AcceptXMR_DB/" logging: diff --git a/server/tests/testdata/config/config_no_secrets.yaml b/server/tests/testdata/config/config_no_secrets.yaml index d2c695b..f61d2c5 100644 --- a/server/tests/testdata/config/config_no_secrets.yaml +++ b/server/tests/testdata/config/config_no_secrets.yaml @@ -10,6 +10,8 @@ internal-api: cert: "/path/to/cert.pem" key: "/path/to/key.pem" static_dir: server/static/ +callback: + queue-size: 1000 wallet: primary-address: "4613YiHLM6JMH4zejMB2zJY5TwQCxL8p65ufw8kBP5yxX9itmuGLqp1dS4tkVoTxjyH3aYhYNrtGHbQzJQP5bFus3KHVdmf" restore-height: 2947000 @@ -17,6 +19,8 @@ daemon: url: "https://node.example.com:18081" login: username: "pinkpanther" + rpc-timeout: 20 + connection-timeout: 10 database: path: "server/tests/AcceptXMR_DB/" logging: diff --git a/testing-utils/Cargo.toml b/testing-utils/Cargo.toml new file mode 100644 index 0000000..42a733b --- /dev/null +++ b/testing-utils/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "testing-utils" +publish = false +version = "0.1.0" +edition = "2021" +rust-version = "1.70" +license = "MIT OR Apache-2.0" +description = "Testing utilities for AcceptXMR" +repository = "https://github.com/busyboredom/acceptxmr" +readme = "README.md" +keywords = ["crypto", "gateway", "monero", "payment", "xmr"] +categories = ["cryptography::cryptocurrencies"] + +[dependencies] +acceptxmr.workspace = true +console-subscriber.workspace = true +env_logger.workspace = true +httpmock.workspace = true +log.workspace = true +serde_json.workspace = true +tempfile.workspace = true +tracing-subscriber.workspace = true diff --git a/library/tests/rpc_resources/blocks/2477647/block.json b/testing-utils/rpc_resources/blocks/2477647/block.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477647/block.json rename to testing-utils/rpc_resources/blocks/2477647/block.json diff --git a/library/tests/rpc_resources/blocks/2477647/transactions_0.json b/testing-utils/rpc_resources/blocks/2477647/transactions_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477647/transactions_0.json rename to testing-utils/rpc_resources/blocks/2477647/transactions_0.json diff --git a/library/tests/rpc_resources/blocks/2477647/txs_hashes_0.json b/testing-utils/rpc_resources/blocks/2477647/txs_hashes_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477647/txs_hashes_0.json rename to testing-utils/rpc_resources/blocks/2477647/txs_hashes_0.json diff --git a/library/tests/rpc_resources/blocks/2477648/block.json b/testing-utils/rpc_resources/blocks/2477648/block.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477648/block.json rename to testing-utils/rpc_resources/blocks/2477648/block.json diff --git a/library/tests/rpc_resources/blocks/2477648/transactions_0.json b/testing-utils/rpc_resources/blocks/2477648/transactions_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477648/transactions_0.json rename to testing-utils/rpc_resources/blocks/2477648/transactions_0.json diff --git a/library/tests/rpc_resources/blocks/2477648/txs_hashes_0.json b/testing-utils/rpc_resources/blocks/2477648/txs_hashes_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477648/txs_hashes_0.json rename to testing-utils/rpc_resources/blocks/2477648/txs_hashes_0.json diff --git a/library/tests/rpc_resources/blocks/2477649/block.json b/testing-utils/rpc_resources/blocks/2477649/block.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477649/block.json rename to testing-utils/rpc_resources/blocks/2477649/block.json diff --git a/library/tests/rpc_resources/blocks/2477649/transactions_0.json b/testing-utils/rpc_resources/blocks/2477649/transactions_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477649/transactions_0.json rename to testing-utils/rpc_resources/blocks/2477649/transactions_0.json diff --git a/library/tests/rpc_resources/blocks/2477649/txs_hashes_0.json b/testing-utils/rpc_resources/blocks/2477649/txs_hashes_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477649/txs_hashes_0.json rename to testing-utils/rpc_resources/blocks/2477649/txs_hashes_0.json diff --git a/library/tests/rpc_resources/blocks/2477650/block.json b/testing-utils/rpc_resources/blocks/2477650/block.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477650/block.json rename to testing-utils/rpc_resources/blocks/2477650/block.json diff --git a/library/tests/rpc_resources/blocks/2477650/transactions_0.json b/testing-utils/rpc_resources/blocks/2477650/transactions_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477650/transactions_0.json rename to testing-utils/rpc_resources/blocks/2477650/transactions_0.json diff --git a/library/tests/rpc_resources/blocks/2477650/txs_hashes_0.json b/testing-utils/rpc_resources/blocks/2477650/txs_hashes_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477650/txs_hashes_0.json rename to testing-utils/rpc_resources/blocks/2477650/txs_hashes_0.json diff --git a/library/tests/rpc_resources/blocks/2477651/block.json b/testing-utils/rpc_resources/blocks/2477651/block.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477651/block.json rename to testing-utils/rpc_resources/blocks/2477651/block.json diff --git a/library/tests/rpc_resources/blocks/2477651/transactions_0.json b/testing-utils/rpc_resources/blocks/2477651/transactions_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477651/transactions_0.json rename to testing-utils/rpc_resources/blocks/2477651/transactions_0.json diff --git a/library/tests/rpc_resources/blocks/2477651/txs_hashes_0.json b/testing-utils/rpc_resources/blocks/2477651/txs_hashes_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477651/txs_hashes_0.json rename to testing-utils/rpc_resources/blocks/2477651/txs_hashes_0.json diff --git a/library/tests/rpc_resources/blocks/2477652/block.json b/testing-utils/rpc_resources/blocks/2477652/block.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477652/block.json rename to testing-utils/rpc_resources/blocks/2477652/block.json diff --git a/library/tests/rpc_resources/blocks/2477652/transactions_0.json b/testing-utils/rpc_resources/blocks/2477652/transactions_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477652/transactions_0.json rename to testing-utils/rpc_resources/blocks/2477652/transactions_0.json diff --git a/library/tests/rpc_resources/blocks/2477652/txs_hashes_0.json b/testing-utils/rpc_resources/blocks/2477652/txs_hashes_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477652/txs_hashes_0.json rename to testing-utils/rpc_resources/blocks/2477652/txs_hashes_0.json diff --git a/library/tests/rpc_resources/blocks/2477653/block.json b/testing-utils/rpc_resources/blocks/2477653/block.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477653/block.json rename to testing-utils/rpc_resources/blocks/2477653/block.json diff --git a/library/tests/rpc_resources/blocks/2477653/transactions_0.json b/testing-utils/rpc_resources/blocks/2477653/transactions_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477653/transactions_0.json rename to testing-utils/rpc_resources/blocks/2477653/transactions_0.json diff --git a/library/tests/rpc_resources/blocks/2477653/txs_hashes_0.json b/testing-utils/rpc_resources/blocks/2477653/txs_hashes_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477653/txs_hashes_0.json rename to testing-utils/rpc_resources/blocks/2477653/txs_hashes_0.json diff --git a/library/tests/rpc_resources/blocks/2477654/block.json b/testing-utils/rpc_resources/blocks/2477654/block.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477654/block.json rename to testing-utils/rpc_resources/blocks/2477654/block.json diff --git a/library/tests/rpc_resources/blocks/2477654/transactions_0.json b/testing-utils/rpc_resources/blocks/2477654/transactions_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477654/transactions_0.json rename to testing-utils/rpc_resources/blocks/2477654/transactions_0.json diff --git a/library/tests/rpc_resources/blocks/2477654/txs_hashes_0.json b/testing-utils/rpc_resources/blocks/2477654/txs_hashes_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477654/txs_hashes_0.json rename to testing-utils/rpc_resources/blocks/2477654/txs_hashes_0.json diff --git a/library/tests/rpc_resources/blocks/2477655/block.json b/testing-utils/rpc_resources/blocks/2477655/block.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477655/block.json rename to testing-utils/rpc_resources/blocks/2477655/block.json diff --git a/library/tests/rpc_resources/blocks/2477655/transactions_0.json b/testing-utils/rpc_resources/blocks/2477655/transactions_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477655/transactions_0.json rename to testing-utils/rpc_resources/blocks/2477655/transactions_0.json diff --git a/library/tests/rpc_resources/blocks/2477655/txs_hashes_0.json b/testing-utils/rpc_resources/blocks/2477655/txs_hashes_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477655/txs_hashes_0.json rename to testing-utils/rpc_resources/blocks/2477655/txs_hashes_0.json diff --git a/library/tests/rpc_resources/blocks/2477656/block.json b/testing-utils/rpc_resources/blocks/2477656/block.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477656/block.json rename to testing-utils/rpc_resources/blocks/2477656/block.json diff --git a/library/tests/rpc_resources/blocks/2477656/daemon_height.json b/testing-utils/rpc_resources/blocks/2477656/daemon_height.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477656/daemon_height.json rename to testing-utils/rpc_resources/blocks/2477656/daemon_height.json diff --git a/library/tests/rpc_resources/blocks/2477656/transactions_0.json b/testing-utils/rpc_resources/blocks/2477656/transactions_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477656/transactions_0.json rename to testing-utils/rpc_resources/blocks/2477656/transactions_0.json diff --git a/library/tests/rpc_resources/blocks/2477656/txs_hashes_0.json b/testing-utils/rpc_resources/blocks/2477656/txs_hashes_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477656/txs_hashes_0.json rename to testing-utils/rpc_resources/blocks/2477656/txs_hashes_0.json diff --git a/library/tests/rpc_resources/blocks/2477657/block.json b/testing-utils/rpc_resources/blocks/2477657/block.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477657/block.json rename to testing-utils/rpc_resources/blocks/2477657/block.json diff --git a/library/tests/rpc_resources/blocks/2477657/daemon_height.json b/testing-utils/rpc_resources/blocks/2477657/daemon_height.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477657/daemon_height.json rename to testing-utils/rpc_resources/blocks/2477657/daemon_height.json diff --git a/library/tests/rpc_resources/blocks/2477657/transactions_0.json b/testing-utils/rpc_resources/blocks/2477657/transactions_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477657/transactions_0.json rename to testing-utils/rpc_resources/blocks/2477657/transactions_0.json diff --git a/library/tests/rpc_resources/blocks/2477657/txs_hashes_0.json b/testing-utils/rpc_resources/blocks/2477657/txs_hashes_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477657/txs_hashes_0.json rename to testing-utils/rpc_resources/blocks/2477657/txs_hashes_0.json diff --git a/library/tests/rpc_resources/blocks/2477657_alt/block.json b/testing-utils/rpc_resources/blocks/2477657_alt/block.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477657_alt/block.json rename to testing-utils/rpc_resources/blocks/2477657_alt/block.json diff --git a/library/tests/rpc_resources/blocks/2477657_alt/daemon_height.json b/testing-utils/rpc_resources/blocks/2477657_alt/daemon_height.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477657_alt/daemon_height.json rename to testing-utils/rpc_resources/blocks/2477657_alt/daemon_height.json diff --git a/library/tests/rpc_resources/blocks/2477657_alt/transactions_0.json b/testing-utils/rpc_resources/blocks/2477657_alt/transactions_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477657_alt/transactions_0.json rename to testing-utils/rpc_resources/blocks/2477657_alt/transactions_0.json diff --git a/library/tests/rpc_resources/blocks/2477657_alt/txs_hashes_0.json b/testing-utils/rpc_resources/blocks/2477657_alt/txs_hashes_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477657_alt/txs_hashes_0.json rename to testing-utils/rpc_resources/blocks/2477657_alt/txs_hashes_0.json diff --git a/library/tests/rpc_resources/blocks/2477658/block.json b/testing-utils/rpc_resources/blocks/2477658/block.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477658/block.json rename to testing-utils/rpc_resources/blocks/2477658/block.json diff --git a/library/tests/rpc_resources/blocks/2477658/daemon_height.json b/testing-utils/rpc_resources/blocks/2477658/daemon_height.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477658/daemon_height.json rename to testing-utils/rpc_resources/blocks/2477658/daemon_height.json diff --git a/library/tests/rpc_resources/blocks/2477658/transactions_0.json b/testing-utils/rpc_resources/blocks/2477658/transactions_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477658/transactions_0.json rename to testing-utils/rpc_resources/blocks/2477658/transactions_0.json diff --git a/library/tests/rpc_resources/blocks/2477658/txs_hashes_0.json b/testing-utils/rpc_resources/blocks/2477658/txs_hashes_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477658/txs_hashes_0.json rename to testing-utils/rpc_resources/blocks/2477658/txs_hashes_0.json diff --git a/library/tests/rpc_resources/blocks/2477658_alt/block.json b/testing-utils/rpc_resources/blocks/2477658_alt/block.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477658_alt/block.json rename to testing-utils/rpc_resources/blocks/2477658_alt/block.json diff --git a/library/tests/rpc_resources/blocks/2477658_alt/daemon_height.json b/testing-utils/rpc_resources/blocks/2477658_alt/daemon_height.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477658_alt/daemon_height.json rename to testing-utils/rpc_resources/blocks/2477658_alt/daemon_height.json diff --git a/library/tests/rpc_resources/blocks/2477658_alt/transactions_0.json b/testing-utils/rpc_resources/blocks/2477658_alt/transactions_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477658_alt/transactions_0.json rename to testing-utils/rpc_resources/blocks/2477658_alt/transactions_0.json diff --git a/library/tests/rpc_resources/blocks/2477658_alt/txs_hashes_0.json b/testing-utils/rpc_resources/blocks/2477658_alt/txs_hashes_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477658_alt/txs_hashes_0.json rename to testing-utils/rpc_resources/blocks/2477658_alt/txs_hashes_0.json diff --git a/library/tests/rpc_resources/blocks/2477659/block.json b/testing-utils/rpc_resources/blocks/2477659/block.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477659/block.json rename to testing-utils/rpc_resources/blocks/2477659/block.json diff --git a/library/tests/rpc_resources/blocks/2477659/daemon_height.json b/testing-utils/rpc_resources/blocks/2477659/daemon_height.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477659/daemon_height.json rename to testing-utils/rpc_resources/blocks/2477659/daemon_height.json diff --git a/library/tests/rpc_resources/blocks/2477659/transactions_0.json b/testing-utils/rpc_resources/blocks/2477659/transactions_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477659/transactions_0.json rename to testing-utils/rpc_resources/blocks/2477659/transactions_0.json diff --git a/library/tests/rpc_resources/blocks/2477659/txs_hashes_0.json b/testing-utils/rpc_resources/blocks/2477659/txs_hashes_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477659/txs_hashes_0.json rename to testing-utils/rpc_resources/blocks/2477659/txs_hashes_0.json diff --git a/library/tests/rpc_resources/blocks/2477660/block.json b/testing-utils/rpc_resources/blocks/2477660/block.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477660/block.json rename to testing-utils/rpc_resources/blocks/2477660/block.json diff --git a/library/tests/rpc_resources/blocks/2477660/daemon_height.json b/testing-utils/rpc_resources/blocks/2477660/daemon_height.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477660/daemon_height.json rename to testing-utils/rpc_resources/blocks/2477660/daemon_height.json diff --git a/library/tests/rpc_resources/blocks/2477660/transactions_0.json b/testing-utils/rpc_resources/blocks/2477660/transactions_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477660/transactions_0.json rename to testing-utils/rpc_resources/blocks/2477660/transactions_0.json diff --git a/library/tests/rpc_resources/blocks/2477660/txs_hashes_0.json b/testing-utils/rpc_resources/blocks/2477660/txs_hashes_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477660/txs_hashes_0.json rename to testing-utils/rpc_resources/blocks/2477660/txs_hashes_0.json diff --git a/library/tests/rpc_resources/blocks/2477661/block.json b/testing-utils/rpc_resources/blocks/2477661/block.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477661/block.json rename to testing-utils/rpc_resources/blocks/2477661/block.json diff --git a/library/tests/rpc_resources/blocks/2477661/daemon_height.json b/testing-utils/rpc_resources/blocks/2477661/daemon_height.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477661/daemon_height.json rename to testing-utils/rpc_resources/blocks/2477661/daemon_height.json diff --git a/library/tests/rpc_resources/blocks/2477661/transactions_0.json b/testing-utils/rpc_resources/blocks/2477661/transactions_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477661/transactions_0.json rename to testing-utils/rpc_resources/blocks/2477661/transactions_0.json diff --git a/library/tests/rpc_resources/blocks/2477661/txs_hashes_0.json b/testing-utils/rpc_resources/blocks/2477661/txs_hashes_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477661/txs_hashes_0.json rename to testing-utils/rpc_resources/blocks/2477661/txs_hashes_0.json diff --git a/library/tests/rpc_resources/blocks/2477662/block.json b/testing-utils/rpc_resources/blocks/2477662/block.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477662/block.json rename to testing-utils/rpc_resources/blocks/2477662/block.json diff --git a/library/tests/rpc_resources/blocks/2477662/daemon_height.json b/testing-utils/rpc_resources/blocks/2477662/daemon_height.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477662/daemon_height.json rename to testing-utils/rpc_resources/blocks/2477662/daemon_height.json diff --git a/library/tests/rpc_resources/blocks/2477662/transactions_0.json b/testing-utils/rpc_resources/blocks/2477662/transactions_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477662/transactions_0.json rename to testing-utils/rpc_resources/blocks/2477662/transactions_0.json diff --git a/library/tests/rpc_resources/blocks/2477662/txs_hashes_0.json b/testing-utils/rpc_resources/blocks/2477662/txs_hashes_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477662/txs_hashes_0.json rename to testing-utils/rpc_resources/blocks/2477662/txs_hashes_0.json diff --git a/library/tests/rpc_resources/blocks/2477663/block.json b/testing-utils/rpc_resources/blocks/2477663/block.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477663/block.json rename to testing-utils/rpc_resources/blocks/2477663/block.json diff --git a/library/tests/rpc_resources/blocks/2477663/daemon_height.json b/testing-utils/rpc_resources/blocks/2477663/daemon_height.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477663/daemon_height.json rename to testing-utils/rpc_resources/blocks/2477663/daemon_height.json diff --git a/library/tests/rpc_resources/blocks/2477663/transactions_0.json b/testing-utils/rpc_resources/blocks/2477663/transactions_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477663/transactions_0.json rename to testing-utils/rpc_resources/blocks/2477663/transactions_0.json diff --git a/library/tests/rpc_resources/blocks/2477663/txs_hashes_0.json b/testing-utils/rpc_resources/blocks/2477663/txs_hashes_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477663/txs_hashes_0.json rename to testing-utils/rpc_resources/blocks/2477663/txs_hashes_0.json diff --git a/library/tests/rpc_resources/blocks/2477664/block.json b/testing-utils/rpc_resources/blocks/2477664/block.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477664/block.json rename to testing-utils/rpc_resources/blocks/2477664/block.json diff --git a/library/tests/rpc_resources/blocks/2477664/daemon_height.json b/testing-utils/rpc_resources/blocks/2477664/daemon_height.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477664/daemon_height.json rename to testing-utils/rpc_resources/blocks/2477664/daemon_height.json diff --git a/library/tests/rpc_resources/blocks/2477664/transactions_0.json b/testing-utils/rpc_resources/blocks/2477664/transactions_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477664/transactions_0.json rename to testing-utils/rpc_resources/blocks/2477664/transactions_0.json diff --git a/library/tests/rpc_resources/blocks/2477664/txs_hashes_0.json b/testing-utils/rpc_resources/blocks/2477664/txs_hashes_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477664/txs_hashes_0.json rename to testing-utils/rpc_resources/blocks/2477664/txs_hashes_0.json diff --git a/library/tests/rpc_resources/blocks/2477665/block.json b/testing-utils/rpc_resources/blocks/2477665/block.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477665/block.json rename to testing-utils/rpc_resources/blocks/2477665/block.json diff --git a/library/tests/rpc_resources/blocks/2477665/daemon_height.json b/testing-utils/rpc_resources/blocks/2477665/daemon_height.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477665/daemon_height.json rename to testing-utils/rpc_resources/blocks/2477665/daemon_height.json diff --git a/library/tests/rpc_resources/blocks/2477665/transactions_0.json b/testing-utils/rpc_resources/blocks/2477665/transactions_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477665/transactions_0.json rename to testing-utils/rpc_resources/blocks/2477665/transactions_0.json diff --git a/library/tests/rpc_resources/blocks/2477665/txs_hashes_0.json b/testing-utils/rpc_resources/blocks/2477665/txs_hashes_0.json similarity index 100% rename from library/tests/rpc_resources/blocks/2477665/txs_hashes_0.json rename to testing-utils/rpc_resources/blocks/2477665/txs_hashes_0.json diff --git a/library/tests/rpc_resources/transactions/hashes_with_payment.json b/testing-utils/rpc_resources/transactions/hashes_with_payment.json similarity index 100% rename from library/tests/rpc_resources/transactions/hashes_with_payment.json rename to testing-utils/rpc_resources/transactions/hashes_with_payment.json diff --git a/library/tests/rpc_resources/transactions/hashes_with_payment_2.json b/testing-utils/rpc_resources/transactions/hashes_with_payment_2.json similarity index 100% rename from library/tests/rpc_resources/transactions/hashes_with_payment_2.json rename to testing-utils/rpc_resources/transactions/hashes_with_payment_2.json diff --git a/library/tests/rpc_resources/transactions/hashes_with_payment_account_0.json b/testing-utils/rpc_resources/transactions/hashes_with_payment_account_0.json similarity index 100% rename from library/tests/rpc_resources/transactions/hashes_with_payment_account_0.json rename to testing-utils/rpc_resources/transactions/hashes_with_payment_account_0.json diff --git a/library/tests/rpc_resources/transactions/hashes_with_payment_timelock.json b/testing-utils/rpc_resources/transactions/hashes_with_payment_timelock.json similarity index 100% rename from library/tests/rpc_resources/transactions/hashes_with_payment_timelock.json rename to testing-utils/rpc_resources/transactions/hashes_with_payment_timelock.json diff --git a/library/tests/rpc_resources/transactions/txs_with_payment.json b/testing-utils/rpc_resources/transactions/txs_with_payment.json similarity index 100% rename from library/tests/rpc_resources/transactions/txs_with_payment.json rename to testing-utils/rpc_resources/transactions/txs_with_payment.json diff --git a/library/tests/rpc_resources/transactions/txs_with_payment_2.json b/testing-utils/rpc_resources/transactions/txs_with_payment_2.json similarity index 100% rename from library/tests/rpc_resources/transactions/txs_with_payment_2.json rename to testing-utils/rpc_resources/transactions/txs_with_payment_2.json diff --git a/library/tests/rpc_resources/transactions/txs_with_payment_account_0.json b/testing-utils/rpc_resources/transactions/txs_with_payment_account_0.json similarity index 100% rename from library/tests/rpc_resources/transactions/txs_with_payment_account_0.json rename to testing-utils/rpc_resources/transactions/txs_with_payment_account_0.json diff --git a/library/tests/rpc_resources/transactions/txs_with_payment_timelock.json b/testing-utils/rpc_resources/transactions/txs_with_payment_timelock.json similarity index 100% rename from library/tests/rpc_resources/transactions/txs_with_payment_timelock.json rename to testing-utils/rpc_resources/transactions/txs_with_payment_timelock.json diff --git a/library/tests/rpc_resources/txpools/hashes.json b/testing-utils/rpc_resources/txpools/hashes.json similarity index 100% rename from library/tests/rpc_resources/txpools/hashes.json rename to testing-utils/rpc_resources/txpools/hashes.json diff --git a/library/tests/rpc_resources/txpools/hashes_with_payment.json b/testing-utils/rpc_resources/txpools/hashes_with_payment.json similarity index 100% rename from library/tests/rpc_resources/txpools/hashes_with_payment.json rename to testing-utils/rpc_resources/txpools/hashes_with_payment.json diff --git a/library/tests/rpc_resources/txpools/hashes_with_payment_2.json b/testing-utils/rpc_resources/txpools/hashes_with_payment_2.json similarity index 100% rename from library/tests/rpc_resources/txpools/hashes_with_payment_2.json rename to testing-utils/rpc_resources/txpools/hashes_with_payment_2.json diff --git a/library/tests/rpc_resources/txpools/hashes_with_payment_account_0.json b/testing-utils/rpc_resources/txpools/hashes_with_payment_account_0.json similarity index 100% rename from library/tests/rpc_resources/txpools/hashes_with_payment_account_0.json rename to testing-utils/rpc_resources/txpools/hashes_with_payment_account_0.json diff --git a/library/tests/rpc_resources/txpools/hashes_with_payment_timelock.json b/testing-utils/rpc_resources/txpools/hashes_with_payment_timelock.json similarity index 100% rename from library/tests/rpc_resources/txpools/hashes_with_payment_timelock.json rename to testing-utils/rpc_resources/txpools/hashes_with_payment_timelock.json diff --git a/library/tests/rpc_resources/txpools/txpool.json b/testing-utils/rpc_resources/txpools/txpool.json similarity index 100% rename from library/tests/rpc_resources/txpools/txpool.json rename to testing-utils/rpc_resources/txpools/txpool.json diff --git a/library/tests/rpc_resources/txpools/txpool_with_payment.json b/testing-utils/rpc_resources/txpools/txpool_with_payment.json similarity index 100% rename from library/tests/rpc_resources/txpools/txpool_with_payment.json rename to testing-utils/rpc_resources/txpools/txpool_with_payment.json diff --git a/library/tests/rpc_resources/txpools/txpool_with_payment_2.json b/testing-utils/rpc_resources/txpools/txpool_with_payment_2.json similarity index 100% rename from library/tests/rpc_resources/txpools/txpool_with_payment_2.json rename to testing-utils/rpc_resources/txpools/txpool_with_payment_2.json diff --git a/library/tests/rpc_resources/txpools/txpool_with_payment_account_0.json b/testing-utils/rpc_resources/txpools/txpool_with_payment_account_0.json similarity index 100% rename from library/tests/rpc_resources/txpools/txpool_with_payment_account_0.json rename to testing-utils/rpc_resources/txpools/txpool_with_payment_account_0.json diff --git a/library/tests/rpc_resources/txpools/txpool_with_payment_timelock.json b/testing-utils/rpc_resources/txpools/txpool_with_payment_timelock.json similarity index 100% rename from library/tests/rpc_resources/txpools/txpool_with_payment_timelock.json rename to testing-utils/rpc_resources/txpools/txpool_with_payment_timelock.json diff --git a/testing-utils/src/daemon.rs b/testing-utils/src/daemon.rs new file mode 100644 index 0000000..7e3b73e --- /dev/null +++ b/testing-utils/src/daemon.rs @@ -0,0 +1,233 @@ +use std::{collections::HashMap, fs, ops::Deref, sync::Mutex}; + +use httpmock::{Mock, MockServer}; +use serde_json::{json, Value}; + +pub struct MockDaemon { + server: MockServer, + daemon_height_id: Mutex>, + block_ids: Mutex>, + txpool_id: Mutex>, + txpool_hashes_id: Mutex>, + txpool_transactions_id: Mutex>, +} + +impl Deref for MockDaemon { + type Target = MockServer; + + fn deref(&self) -> &MockServer { + &self.server + } +} + +impl MockDaemon { + pub async fn new_mock_daemon() -> MockDaemon { + let mock_daemon = MockDaemon { + server: MockServer::start_async().await, + daemon_height_id: Mutex::new(None), + block_ids: Mutex::new(HashMap::new()), + txpool_id: Mutex::new(None), + txpool_hashes_id: Mutex::new(None), + txpool_transactions_id: Mutex::new(None), + }; + // Mock daemon height request. + mock_daemon.mock_daemon_height(2477657); + // Mock txpool request. + mock_daemon.mock_txpool("../testing-utils/rpc_resources/txpools/txpool.json"); + // Mock txpool hashes. + mock_daemon.mock_txpool_hashes("../testing-utils/rpc_resources/txpools/hashes.json"); + + // Mock blocks. + for i in 2477647..2477666 { + // Mock block requests. + let response_path = "../testing-utils/rpc_resources/blocks/".to_owned() + + &i.to_string() + + "/block.json"; + mock_daemon.mock_block(i, &response_path); + + // Skip block 2477661 when mocking transactions, because it has none. + if i == 2477661 { + continue; + } + + // Mock block transaction requests. + let request_path = "../testing-utils/rpc_resources/blocks/".to_owned() + + &i.to_string() + + "/txs_hashes_0.json"; + let response_path = "../testing-utils/rpc_resources/blocks/".to_owned() + + &i.to_string() + + "/transactions_0.json"; + mock_daemon.mock_transactions(&request_path, &response_path); + } + mock_daemon + } + + pub fn mock_daemon_height(&self, height: u64) -> Mock { + // Use mock ID to delete old daemon height mock. + if let Some(id) = *self + .daemon_height_id + .lock() + .expect("PoisonError when reading daemon height mock ID") + { + Mock::new(id, self).delete(); + }; + + // Create the new daemon height mock. + let mock = self.mock(|when, then| { + when.path("/json_rpc") + .body(r#"{"jsonrpc":"2.0","id":"0","method":"get_block_count"}"#); + then.status(200) + .header("content-type", "application/json") + .json_body(json!({ + "id": "0", + "jsonrpc": "2.0", + "result": { + "count": height, + "status": "OK" + } + })); + }); + *self + .daemon_height_id + .lock() + .expect("PoisonError when writing daemon height mock ID") = Some(mock.id); + mock + } + + pub fn mock_alt_2477657(&self) { + // Mock block requests. + let response_path = "../testing-utils/rpc_resources/blocks/2477657_alt/block.json"; + self.mock_block(2477657, response_path); + + // Mock block transaction requests. + let request_path = "../testing-utils/rpc_resources/blocks/2477657_alt/txs_hashes_0.json"; + let response_path = "../testing-utils/rpc_resources/blocks/2477657_alt/transactions_0.json"; + self.mock_transactions(request_path, response_path); + } + + pub fn mock_alt_2477658(&self) { + // Mock block requests. + let response_path = "../testing-utils/rpc_resources/blocks/2477658_alt/block.json"; + self.mock_block(2477658, response_path); + + // Mock block transaction requests. + let request_path = "../testing-utils/rpc_resources/blocks/2477658_alt/txs_hashes_0.json"; + let response_path = "../testing-utils/rpc_resources/blocks/2477658_alt/transactions_0.json"; + self.mock_transactions(request_path, response_path); + } + + pub fn mock_txpool(&self, path: &str) -> Mock { + // Use ID to delete old mock. + if let Some(id) = *self + .txpool_id + .lock() + .expect("PoisonError when reading txpool mock ID") + { + Mock::new(id, self).delete(); + }; + + // Create new mock. + let mock = self.mock(|when, then| { + when.path("/get_transaction_pool").body(""); + then.status(200) + .header("content-type", "application/json") + .body_from_file(path); + }); + *self + .txpool_id + .lock() + .expect("PoisonError when writing txpool mock ID") = Some(mock.id); + mock + } + + pub fn mock_transactions(&self, request_path: &str, response_path: &str) -> Mock { + let when_body: Value = serde_json::from_str( + &fs::read_to_string(request_path) + .expect("failed to read transaction request from file when preparing mock"), + ) + .expect("failed to parse transaction request as json"); + let when_txs: Vec<&str> = when_body["txs_hashes"] + .as_array() + .into_iter() + .flatten() + .map(|v| v.as_str().expect("failed to parse tx hash as string")) + .collect(); + self.mock(|when, then| { + let mut when = when.path("/get_transactions"); + for hash in when_txs { + // Ensure the request contains the hashes of all the expected transactions. + when = when.body_contains(hash); + } + then.status(200) + .header("content-type", "application/json") + .body_from_file(response_path); + }) + } + + pub fn mock_block(&self, height: u64, response_path: &str) { + // Use ID to delete old mock. + if let Some(id) = self + .block_ids + .lock() + .expect("PoisonError when reading txpool mock ID") + .get(&height) + { + Mock::new(*id, self).delete(); + }; + let mock = self.mock(|when, then| { + when.path("/json_rpc").body( + r#"{"jsonrpc":"2.0","id":"0","method":"get_block","params":{"height":"#.to_owned() + + &height.to_string() + + "}}", + ); + then.status(200) + .header("content-type", "application/json") + .body_from_file(response_path); + }); + self.block_ids + .lock() + .expect("PoisonError when writing daemon height mock ID") + .insert(height, mock.id); + } + + pub fn mock_txpool_hashes(&self, response_path: &str) -> Mock { + // Use ID to delete old mock. + if let Some(id) = *self + .txpool_hashes_id + .lock() + .expect("PoisonError when reading txpool hashes mock ID") + { + Mock::new(id, self).delete(); + }; + + // Create new mock. + let mock = self.mock(|when, then| { + when.path("/get_transaction_pool_hashes").body(""); + then.status(200) + .header("content-type", "application/json") + .body_from_file(response_path); + }); + *self + .txpool_hashes_id + .lock() + .expect("PoisonError when writing txpool hashes mock ID") = Some(mock.id); + mock + } + + pub fn mock_txpool_transactions(&self, request_path: &str, response_path: &str) -> Mock { + // Use ID to delete old mock. + if let Some(id) = *self + .txpool_transactions_id + .lock() + .expect("PoisonError when reading txpool transactions mock ID") + { + Mock::new(id, self).delete(); + }; + let mock = self.mock_transactions(request_path, response_path); + *self + .txpool_transactions_id + .lock() + .expect("PoisonError when writing txpool transactions mock ID") = Some(mock.id); + mock + } +} diff --git a/testing-utils/src/invoice.rs b/testing-utils/src/invoice.rs new file mode 100644 index 0000000..2bb8b67 --- /dev/null +++ b/testing-utils/src/invoice.rs @@ -0,0 +1,78 @@ +use std::cmp::max; + +use acceptxmr::{Invoice, SubIndex}; + +#[derive(Clone)] +pub struct MockInvoice { + pub address: Option, + pub index: SubIndex, + pub creation_height: u64, + pub amount_requested: u64, + pub amount_paid: u64, + pub paid_height: Option, + pub confirmations_required: u64, + pub current_height: u64, + pub expiration_height: u64, + pub description: String, + + // Calculated fields. + pub is_expired: bool, + pub expires_in: u64, + pub is_confirmed: bool, + pub confirmations: Option, +} + +impl MockInvoice { + pub fn new( + address: Option, + index: SubIndex, + creation_height: u64, + amount_requested: u64, + confirmations_required: u64, + expires_in: u64, + description: String, + ) -> MockInvoice { + MockInvoice { + address, + index, + creation_height, + amount_requested, + amount_paid: 0, + paid_height: None, + confirmations_required, + current_height: creation_height, + expiration_height: creation_height + expires_in, + description, + + is_expired: false, + expires_in, + is_confirmed: false, + confirmations: None, + } + } + + pub fn assert_eq(&self, update: &Invoice) { + if let Some(address) = &self.address { + assert_eq!(update.address(), address); + } + assert_eq!(update.index(), self.index); + assert_eq!(update.creation_height(), self.creation_height); + assert_eq!(update.amount_requested(), self.amount_requested); + assert_eq!(update.amount_paid(), self.amount_paid); + assert_eq!(update.confirmations_required(), self.confirmations_required); + assert_eq!(update.current_height(), self.current_height); + assert_eq!(update.expiration_height(), self.expiration_height); + assert_eq!(update.expiration_height(), self.expiration_height); + assert_eq!( + update.expiration_height() - max(update.creation_height(), update.current_height()), + self.expires_in + ); + assert_eq!(update.description(), self.description); + + // Calculated fields. + assert_eq!(update.is_expired(), self.is_expired); + assert_eq!(update.expiration_in(), self.expires_in); + assert_eq!(update.is_confirmed(), self.is_confirmed); + assert_eq!(update.confirmations(), self.confirmations); + } +} diff --git a/testing-utils/src/lib.rs b/testing-utils/src/lib.rs new file mode 100644 index 0000000..b237d1e --- /dev/null +++ b/testing-utils/src/lib.rs @@ -0,0 +1,40 @@ +mod daemon; +mod invoice; + +pub use daemon::MockDaemon; +pub use invoice::MockInvoice; +use tempfile::Builder; +use tracing_subscriber::{filter::LevelFilter, prelude::*, EnvFilter}; + +pub const PRIVATE_VIEW_KEY: &str = + "ad2093a5705b9f33e6f0f0c1bc1f5f639c756cdfc168c8f2ac6127ccbdab3a03"; +pub const PRIMARY_ADDRESS: &str = + "4613YiHLM6JMH4zejMB2zJY5TwQCxL8p65ufw8kBP5yxX9itmuGLqp1dS4tkVoTxjyH3aYhYNrtGHbQzJQP5bFus3KHVdmf"; + +pub fn new_temp_dir() -> String { + Builder::new() + .prefix("temp_db_") + .rand_bytes(16) + .tempdir() + .expect("failed to generate temporary directory") + .path() + .to_str() + .expect("failed to get temporary directory path") + .to_string() +} + +/// Initialize the logging implementation. Defaults to `Trace` verbosity for +/// `AcceptXMR-Server`, `Debug` for `AcceptXMR`, and `Info` for dependencies. +pub fn init_logger() { + // let console_layer = console_subscriber::spawn(); + let filter = EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .parse_lossy("acceptxmr=debug,acceptxmr_server=trace"); + let fmt_layer = tracing_subscriber::fmt::layer() + .with_test_writer() + .with_filter(filter); + let _ = tracing_subscriber::registry() + //.with(console_layer) + .with(fmt_layer) + .try_init(); +}