diff --git a/.env b/.env new file mode 100644 index 0000000..4982993 --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +# This is an example .env file. You should replace these values with your own, +# or set these environment variable some other way. + +PRIVATE_VIEWKEY=ad2093a5705b9f33e6f0f0c1bc1f5f639c756cdfc168c8f2ac6127ccbdab3a03 +INTERNAL_API_TOKEN=supersecrettoken diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 432364b..aaeff3b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -36,7 +36,7 @@ jobs: strategy: matrix: rust: [ - 1.68.0, + 1.70.0, nightly ] @@ -58,7 +58,7 @@ jobs: strategy: matrix: rust: [ - 1.68.0, + 1.70.0, nightly ] diff --git a/Cargo.lock b/Cargo.lock index d03a16e..40ccad4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,7 +21,7 @@ dependencies = [ "httpmock", "hyper", "hyper-rustls", - "indexmap 2.0.0", + "indexmap 2.0.2", "log", "md-5", "monero", @@ -49,24 +49,43 @@ dependencies = [ "actix-session", "actix-web", "actix-web-actors", + "actix-web-httpauth", + "anyhow", "bytestring", + "clap", + "dotenv", "env_logger", + "futures", + "http", + "hyper", + "hyper-rustls", "log", + "monero", "rand", "rand_chacha", + "rcgen", + "rustls", + "rustls-pemfile", + "secrecy", "serde", "serde_json", + "serde_with", + "serde_yaml", + "test-case", + "thiserror", + "tokio", ] [[package]] name = "actix" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f728064aca1c318585bf4bb04ffcfac9e75e508ab4e8b1bd9ba5dfe04e2cbed5" +checksum = "cba56612922b907719d4a01cf11c8d5b458e7d3dba946d0435f20f58d6795ed2" dependencies = [ + "actix-macros", "actix-rt", "actix_derive", - "bitflags 1.3.2", + "bitflags 2.4.1", "bytes", "crossbeam-channel", "futures-core", @@ -124,17 +143,18 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.3.1" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2079246596c18b4a33e274ae10c0e50613f4d32a4198e09c7b93771013fed74" +checksum = "a92ef85799cba03f76e4f7c10f533e66d87c9a7e7055f3391f09000ad8351bc9" dependencies = [ "actix-codec", "actix-rt", "actix-service", + "actix-tls", "actix-utils", - "ahash 0.8.3", - "base64 0.21.2", - "bitflags 1.3.2", + "ahash", + "base64 0.21.4", + "bitflags 2.4.1", "brotli", "bytes", "bytestring", @@ -168,7 +188,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.28", + "syn 2.0.38", ] [[package]] @@ -186,9 +206,9 @@ dependencies = [ [[package]] name = "actix-rt" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15265b6b8e2347670eb363c47fc8c75208b4a4994b27192f345fcbe707804f3e" +checksum = "28f32d40287d3f402ae0028a9d54bef51af15c8769492826a69d28f81893151d" dependencies = [ "futures-core", "tokio", @@ -196,9 +216,9 @@ dependencies = [ [[package]] name = "actix-server" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e8613a75dd50cc45f473cee3c34d59ed677c0f7b44480ce3b8247d7dc519327" +checksum = "3eb13e7eef0423ea6eab0e59f6c72e7cb46d33691ad56a726b3cd07ddec2c2d4" dependencies = [ "actix-rt", "actix-service", @@ -206,8 +226,7 @@ dependencies = [ "futures-core", "futures-util", "mio", - "num_cpus", - "socket2", + "socket2 0.5.4", "tokio", "tracing", ] @@ -225,9 +244,9 @@ dependencies = [ [[package]] name = "actix-session" -version = "0.7.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da8b818ae1f11049a4d218975345fe8e56ce5a5f92c11f972abcff5ff80e87" +checksum = "2e6a28f813a6671e1847d005cad0be36ae4d016287690f765c303379837c13d6" dependencies = [ "actix-service", "actix-utils", @@ -240,6 +259,27 @@ dependencies = [ "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" @@ -252,9 +292,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.3.1" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3cb42f9566ab176e1ef0b8b3a896529062b4efc6be0123046095914c4c1c96" +checksum = "0e4a5b5e29603ca8c94a77c65cf874718ceb60292c5a5c3e5f4ace041af462b9" dependencies = [ "actix-codec", "actix-http", @@ -263,9 +303,10 @@ dependencies = [ "actix-rt", "actix-server", "actix-service", + "actix-tls", "actix-utils", "actix-web-codegen", - "ahash 0.7.6", + "ahash", "bytes", "bytestring", "cfg-if", @@ -274,7 +315,6 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "http", "itoa", "language-tags", "log", @@ -286,7 +326,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2", + "socket2 0.5.4", "time", "url", ] @@ -311,32 +351,47 @@ dependencies = [ [[package]] name = "actix-web-codegen" -version = "4.2.0" +version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2262160a7ae29e3415554a3f1fc04c764b1540c116aa524683208078b7a75bc9" +checksum = "eb1f50ebbb30eca122b188319a4398b3f7bb4a8cdf50ecfb73bfc6a3c3ce54f5" dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 1.0.109", + "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", ] [[package]] name = "actix_derive" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d44b8fee1ced9671ba043476deddef739dd0959bf77030b26b738cc591737a7" +checksum = "7c7db3d5a9718568e4cf4a537cfd7070e6e6ff7481510d0237fb529ac850f6d3" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.38", ] [[package]] name = "addr2line" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -370,9 +425,9 @@ dependencies = [ [[package]] name = "aes-gcm" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "209b47e8954a928e1d72e86eca7000ebb6655fe1436d33eefc2201cad027e237" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" dependencies = [ "aead", "aes", @@ -382,17 +437,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom 0.2.10", - "once_cell", - "version_check", -] - [[package]] name = "ahash" version = "0.8.3" @@ -400,16 +444,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ "cfg-if", - "getrandom 0.2.10", + "getrandom", "once_cell", "version_check", ] [[package]] name = "aho-corasick" -version = "1.0.2" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] @@ -429,11 +473,74 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "anyhow" -version = "1.0.72" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "ascii-canvas" @@ -467,20 +574,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" dependencies = [ "concurrent-queue", - "event-listener", + "event-listener 2.5.3", "futures-core", ] [[package]] name = "async-executor" -version = "1.5.1" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb" +checksum = "2c1da3ae8dabd9c00f453a329dfe1fb28da3c0a72e2478cdcd93171740c20499" dependencies = [ "async-lock", "async-task", "concurrent-queue", - "fastrand 1.9.0", + "fastrand 2.0.1", "futures-lite", "slab", ] @@ -514,19 +621,19 @@ dependencies = [ "log", "parking", "polling", - "rustix 0.37.23", + "rustix 0.37.25", "slab", - "socket2", + "socket2 0.4.9", "waker-fn", ] [[package]] name = "async-lock" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" dependencies = [ - "event-listener", + "event-listener 2.5.3", ] [[package]] @@ -540,19 +647,36 @@ dependencies = [ [[package]] name = "async-process" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9" +checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" dependencies = [ "async-io", "async-lock", - "autocfg", + "async-signal", "blocking", "cfg-if", - "event-listener", + "event-listener 3.0.0", "futures-lite", - "rustix 0.37.23", - "signal-hook", + "rustix 0.38.19", + "windows-sys", +] + +[[package]] +name = "async-signal" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2a5415b7abcdc9cd7d63d6badba5288b2ca017e3fbd4173b8f405449f1a2399" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 0.38.19", + "signal-hook-registry", + "slab", "windows-sys", ] @@ -585,26 +709,26 @@ dependencies = [ [[package]] name = "async-task" -version = "4.4.0" +version = "4.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" +checksum = "b9441c6b2fe128a7c2bf680a44c34d0df31ce09e5b7e401fcca3faa483dbc921" [[package]] name = "async-trait" -version = "0.1.72" +version = "0.1.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.38", ] [[package]] name = "atomic-waker" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" @@ -614,9 +738,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", @@ -629,9 +753,9 @@ dependencies = [ [[package]] name = "base58-monero" -version = "1.0.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d079cdf47e1ca75554200bb2f30bff5a5af16964cac4a566b18de9a5d48db2b" +checksum = "978e81a45367d2409ecd33369a45dda2e9a3ca516153ec194de1fbda4b9fb79d" dependencies = [ "thiserror", ] @@ -644,9 +768,9 @@ checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" [[package]] name = "base64" -version = "0.21.2" +version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" [[package]] name = "basic-cookies" @@ -701,9 +825,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.3" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" [[package]] name = "block-buffer" @@ -716,24 +840,25 @@ dependencies = [ [[package]] name = "blocking" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65" +checksum = "8c36a4d0d48574b3dd360b4b7d95cc651d2b6557b6402848a27d4b228a473e2a" dependencies = [ "async-channel", "async-lock", "async-task", - "atomic-waker", - "fastrand 1.9.0", + "fastrand 2.0.1", + "futures-io", "futures-lite", - "log", + "piper", + "tracing", ] [[package]] name = "brotli" -version = "3.3.4" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -742,9 +867,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "2.3.4" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" +checksum = "da74e2b81409b1b743f8f0c62cc6254afefb8b8e50bbfe3735550f7aeefa3448" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -752,27 +877,27 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "bytemuck" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "bytestring" @@ -791,9 +916,9 @@ checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" [[package]] name = "cc" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "jobserver", "libc", @@ -811,6 +936,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17cc5e6b5ab06331c33589842070416baa137e8b0eb912b008cfd4a78ada7919" +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets", +] + [[package]] name = "cipher" version = "0.4.4" @@ -821,17 +959,50 @@ dependencies = [ "inout", ] +[[package]] +name = "clap" +version = "4.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" + [[package]] name = "color_quant" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "concurrent-queue" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" dependencies = [ "crossbeam-utils", ] @@ -860,6 +1031,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + [[package]] name = "cpufeatures" version = "0.2.9" @@ -923,7 +1100,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core 0.6.4", + "rand_core", "typenum", ] @@ -947,15 +1124,15 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "socket2", + "socket2 0.4.9", "winapi", ] [[package]] name = "curl-sys" -version = "0.4.65+curl-8.2.1" +version = "0.4.68+curl-8.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "961ba061c9ef2fe34bbd12b807152d96f0badd2bebe7b90ce6c8c8b7572a0986" +checksum = "b4a0d18d88360e374b16b2273c832b5e57258ffc1d4aa4f96b108e0738d5752f" dependencies = [ "cc", "libc", @@ -964,27 +1141,81 @@ dependencies = [ "openssl-sys", "pkg-config", "vcpkg", - "winapi", + "windows-sys", ] [[package]] name = "curve25519-dalek" -version = "3.2.1" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0" +checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.5.1", + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "platforms", + "rustc_version", + "serde", "subtle", "zeroize", ] +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "darling" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.38", +] + +[[package]] +name = "darling_macro" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.38", +] + [[package]] name = "deranged" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929" +checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +dependencies = [ + "powerfmt", + "serde", +] [[package]] name = "derive_more" @@ -1005,15 +1236,6 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - [[package]] name = "digest" version = "0.10.7" @@ -1046,6 +1268,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "either" version = "1.9.0" @@ -1063,9 +1291,9 @@ dependencies = [ [[package]] name = "encoding_rs" -version = "0.8.32" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if", ] @@ -1091,30 +1319,30 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.2" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" +checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" dependencies = [ - "errno-dragonfly", "libc", "windows-sys", ] [[package]] -name = "errno-dragonfly" -version = "0.1.2" +name = "event-listener" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "2.5.3" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +checksum = "29e56284f00d94c1bc7fd3c77027b4623c88c1f53d8d2394c6199f2921dea325" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] [[package]] name = "fastrand" @@ -1127,9 +1355,15 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.0" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "fiat-crypto" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +checksum = "d0870c84016d4b481be5c9f323c24f65e31e901ae618f0e80f4308fb00de1d2d" [[package]] name = "fixed-hash" @@ -1151,9 +1385,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.26" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", "miniz_oxide", @@ -1184,6 +1418,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.28" @@ -1191,6 +1440,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -1199,6 +1449,17 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.28" @@ -1228,7 +1489,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.38", ] [[package]] @@ -1249,9 +1510,13 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -1276,17 +1541,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - [[package]] name = "getrandom" version = "0.2.10" @@ -1295,7 +1549,7 @@ checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", ] [[package]] @@ -1310,9 +1564,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.3" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "gloo-timers" @@ -1328,9 +1582,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.20" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" dependencies = [ "bytes", "fnv", @@ -1347,9 +1601,9 @@ dependencies = [ [[package]] name = "handlebars" -version = "4.3.7" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c3372087601b532857d332f5957cbae686da52bb7810bf038c3e3c3cc2fa0d" +checksum = "c39b3bc2a8f715298032cf5087e58573809374b08160aa7d750582bdb82d2683" dependencies = [ "log", "pest", @@ -1368,18 +1622,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" - -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] +checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" [[package]] name = "heck" @@ -1389,9 +1634,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "hex" @@ -1401,9 +1646,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hex-literal" -version = "0.3.4" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" [[package]] name = "hkdf" @@ -1420,7 +1665,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.7", + "digest", ] [[package]] @@ -1459,9 +1704,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "httpmock" @@ -1472,7 +1717,7 @@ dependencies = [ "assert-json-diff", "async-object-pool", "async-trait", - "base64 0.21.2", + "base64 0.21.4", "basic-cookies", "crossbeam-utils", "form_urlencoded", @@ -1514,7 +1759,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -1534,9 +1779,38 @@ dependencies = [ "rustls", "tokio", "tokio-rustls", - "webpki-roots", + "webpki-roots 0.23.1", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", ] +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.4.0" @@ -1561,6 +1835,12 @@ dependencies = [ "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" @@ -1569,16 +1849,18 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] name = "indexmap" -version = "2.0.0" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" dependencies = [ "equivalent", - "hashbrown 0.14.0", + "hashbrown 0.14.1", + "serde", ] [[package]] @@ -1617,7 +1899,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", - "rustix 0.38.7", + "rustix 0.38.19", "windows-sys", ] @@ -1633,7 +1915,7 @@ dependencies = [ "curl", "curl-sys", "encoding_rs", - "event-listener", + "event-listener 2.5.3", "futures-lite", "http", "log", @@ -1665,9 +1947,9 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "jobserver" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" dependencies = [ "libc", ] @@ -1741,9 +2023,9 @@ checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" [[package]] name = "libc" -version = "0.2.147" +version = "0.2.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" [[package]] name = "libnghttp2-sys" @@ -1775,19 +2057,18 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.4.5" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" +checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" [[package]] name = "local-channel" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c" +checksum = "e0a493488de5f18c8ffcba89eebb8532ffc562dc400490eb65b84893fae0b178" dependencies = [ "futures-core", "futures-sink", - "futures-util", "local-waker", ] @@ -1809,27 +2090,29 @@ dependencies = [ [[package]] name = "log" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" dependencies = [ + "serde", "value-bag", ] [[package]] name = "md-5" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ - "digest 0.10.7", + "cfg-if", + "digest", ] [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "memoffset" @@ -1873,15 +2156,15 @@ checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "windows-sys", ] [[package]] name = "monero" -version = "0.18.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a8965a7510c5d9389e2086e406c292d6bbecac099eef195be55a2d2043448b9" +checksum = "fdf671c5fd0fa1f1dcc2495b6ca97a83f0aa6b2a737957e773d34c6c6bac0659" dependencies = [ "base58-monero", "curve25519-dalek", @@ -1889,6 +2172,8 @@ dependencies = [ "hex", "hex-literal", "sealed", + "serde", + "serde-big-array", "thiserror", "tiny-keccak", ] @@ -1933,9 +2218,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", ] @@ -1952,9 +2237,9 @@ dependencies = [ [[package]] name = "object" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ "memchr", ] @@ -1979,9 +2264,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.91" +version = "0.9.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "866b5f16f90776b9bb8dc1e1802ac6f0513de3a7a7465867bfbc563dc737faac" +checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" dependencies = [ "cc", "libc", @@ -1991,9 +2276,9 @@ dependencies = [ [[package]] name = "parking" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" +checksum = "e52c774a4c39359c1d1c52e43f73dd91a75a614652c825408eec30c95a9b2067" [[package]] name = "parking_lot" @@ -2049,6 +2334,16 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "pem" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3163d2912b7c3b52d651a055f2c7eec9ba5cd22d26ef75b8dd3a59980b185923" +dependencies = [ + "base64 0.21.4", + "serde", +] + [[package]] name = "percent-encoding" version = "2.3.0" @@ -2057,19 +2352,20 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pest" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1acb4a4365a13f749a93f1a094a7805e5cfa0955373a9de860d962eaa3a5fe5a" +checksum = "c022f1e7b65d6a24c0dbbd5fb344c66881bc01f3e5ae74a1c8100f2f985d98a4" dependencies = [ + "memchr", "thiserror", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "666d00490d4ac815001da55838c500eafb0320019bbaa44444137c48b443a853" +checksum = "35513f630d46400a977c4cb58f78e1bfbe01434316e60c37d27b9ad6139c66d8" dependencies = [ "pest", "pest_generator", @@ -2077,22 +2373,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ca01446f50dbda87c1786af8770d535423fa8a53aec03b8f4e3d7eb10e0929" +checksum = "bc9fc1b9e7057baba189b5c626e2d6f40681ae5b6eb064dc7c7834101ec8123a" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.38", ] [[package]] name = "pest_meta" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56af0a30af74d0445c0bf6d9d051c979b516a1a5af790d251daee76005420a48" +checksum = "1df74e9e7ec4053ceb980e7c0c8bd3594e977fde1af91daba9c928e8e8c6708d" dependencies = [ "once_cell", "pest", @@ -2101,12 +2397,12 @@ dependencies = [ [[package]] name = "petgraph" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 1.9.3", + "indexmap 2.0.2", ] [[package]] @@ -2135,14 +2431,14 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.38", ] [[package]] name = "pin-project-lite" -version = "0.2.11" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c516611246607d0c04186886dbb3a754368ef82c79e9827a802c6d836dd111c" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -2150,12 +2446,29 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +dependencies = [ + "atomic-waker", + "fastrand 2.0.1", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "platforms" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4503fa043bf02cee09a9582e9554b4c6403b2ef55e4612e96561d294419429f8" + [[package]] name = "polling" version = "2.8.0" @@ -2184,6 +2497,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -2222,9 +2541,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] @@ -2241,9 +2560,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.32" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -2256,7 +2575,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -2266,25 +2585,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", + "rand_core", ] [[package]] name = "rand_core" -version = "0.5.1" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.1.16", + "getrandom", ] [[package]] -name = "rand_core" -version = "0.6.4" +name = "rcgen" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "52c4f3084aa3bc7dfbba4eff4fab2a54db4324965d8872ab933565e6fbd83bc6" dependencies = [ - "getrandom 0.2.10", + "pem", + "ring", + "time", + "yasna", ] [[package]] @@ -2311,32 +2633,32 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom 0.2.10", + "getrandom", "redox_syscall 0.2.16", "thiserror", ] [[package]] name = "regex" -version = "1.9.3" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" +checksum = "aaac441002f822bc9705a681810a4dd2963094b9ca0ddc41cb963a4c189189ea" dependencies = [ "aho-corasick", "memchr", "regex-automata", - "regex-syntax 0.7.4", + "regex-syntax 0.8.2", ] [[package]] name = "regex-automata" -version = "0.3.6" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" +checksum = "5011c7e263a695dc8ca064cddb722af1be54e517a280b12a5356f98366899e5d" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.4", + "regex-syntax 0.8.2", ] [[package]] @@ -2347,9 +2669,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.4" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "ring" @@ -2389,9 +2711,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.23" +version = "0.37.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +checksum = "d4eb579851244c2c03e7c24f501c3432bed80b8f720af1d6e5b0e0f01555a035" dependencies = [ "bitflags 1.3.2", "errno", @@ -2403,34 +2725,43 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.7" +version = "0.38.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "172891ebdceb05aa0005f533a6cbfca599ddd7d966f6f5d4d9b2e70478e70399" +checksum = "745ecfa778e66b2b63c88a61cb36e0eea109e803b0b86bf9879fbc77c70e86ed" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.4.1", "errno", "libc", - "linux-raw-sys 0.4.5", + "linux-raw-sys 0.4.10", "windows-sys", ] [[package]] name = "rustls" -version = "0.21.6" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1feddffcfcc0b33f5c6ce9a29e341e4cd59c3f78e7ee45f4a40c038b1d6cbb" +checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" dependencies = [ "log", "ring", - "rustls-webpki 0.101.3", + "rustls-webpki 0.101.6", "sct", ] +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64 0.21.4", +] + [[package]] name = "rustls-webpki" -version = "0.100.1" +version = "0.100.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" +checksum = "5f6a5fc258f1c1276dfe3016516945546e2d5383911efc0fc4f1cdc5df3a4ae3" dependencies = [ "ring", "untrusted", @@ -2438,9 +2769,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.101.3" +version = "0.101.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261e9e0888cba427c3316e6322805653c9425240b6fd96cee7cb671ab70ab8d0" +checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" dependencies = [ "ring", "untrusted", @@ -2494,47 +2825,66 @@ dependencies = [ [[package]] name = "sealed" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b5e421024b5e5edfbaa8e60ecf90bda9dbffc602dbb230e6028763f85f0c68c" +checksum = "f4a8caec23b7800fb97971a1c6ae365b6239aaeddfb934d6265f8505e795699d" dependencies = [ - "heck 0.3.3", + "heck", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.38", +] + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "serde", + "zeroize", ] [[package]] name = "semver" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" -version = "1.0.183" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" +checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" dependencies = [ "serde_derive", ] +[[package]] +name = "serde-big-array" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" -version = "1.0.183" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" +checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.38", ] [[package]] name = "serde_json" -version = "1.0.104" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" dependencies = [ "itoa", "ryu", @@ -2564,35 +2914,67 @@ dependencies = [ ] [[package]] -name = "sha1" -version = "0.10.5" +name = "serde_with" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "1ca3b16a3d82c4088f343b7480a93550b3eabe1a358569c2dfe38bbcead07237" dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.7", + "base64 0.21.4", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.0.2", + "serde", + "serde_json", + "serde_with_macros", + "time", ] [[package]] -name = "sha2" -version = "0.10.7" +name = "serde_with_macros" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e6be15c453eb305019bfa438b1593c731f36a289a7853f7707ee29e870b3b3c" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "serde_yaml" +version = "0.9.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" +dependencies = [ + "indexmap 2.0.2", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha1" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.7", + "digest", ] [[package]] -name = "signal-hook" -version = "0.3.17" +name = "sha2" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ - "libc", - "signal-hook-registry", + "cfg-if", + "cpufeatures", + "digest", ] [[package]] @@ -2606,21 +2988,21 @@ dependencies = [ [[package]] name = "similar" -version = "2.2.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" +checksum = "2aeaf503862c419d66959f5d7ca015337d864e9c49485d771b732e2a20453597" [[package]] name = "siphasher" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "slab" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] @@ -2654,9 +3036,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "socket2" @@ -2668,6 +3050,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "spin" version = "0.5.2" @@ -2676,9 +3068,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "sqlite" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3ddda64c469a257a3b31298805427784d992c226c94b81003f96e8b122286ad" +checksum = "05439db7afa0ce0b38f6d1b4c691f368adde108df021e15e900fec6a1af92488" dependencies = [ "libc", "sqlite3-sys", @@ -2723,6 +3115,12 @@ dependencies = [ "precomputed-hash", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "strum" version = "0.25.0" @@ -2734,15 +3132,15 @@ dependencies = [ [[package]] name = "strum_macros" -version = "0.25.2" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" dependencies = [ - "heck 0.4.1", + "heck", "proc-macro2", "quote", "rustversion", - "syn 2.0.28", + "syn 2.0.38", ] [[package]] @@ -2764,9 +3162,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.28" +version = "2.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" dependencies = [ "proc-macro2", "quote", @@ -2775,14 +3173,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.7.1" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc02fddf48964c42031a0b3fe0428320ecf3a73c401040fc0096f97794310651" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if", - "fastrand 2.0.0", + "fastrand 2.0.1", "redox_syscall 0.3.5", - "rustix 0.38.7", + "rustix 0.38.19", "windows-sys", ] @@ -2799,76 +3197,77 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" dependencies = [ "winapi-util", ] [[package]] name = "test-case" -version = "3.1.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a1d6e7bde536b0412f20765b76e921028059adfd1b90d8974d33fd3c91b25df" +checksum = "c8f1e820b7f1d95a0cdbf97a5df9de10e1be731983ab943e56703ac1b8e9d425" dependencies = [ "test-case-macros", ] [[package]] name = "test-case-core" -version = "3.1.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d10394d5d1e27794f772b6fc854c7e91a2dc26e2cbf807ad523370c2a59c0cee" +checksum = "54c25e2cb8f5fcd7318157634e8838aa6f7e4715c96637f969fabaccd1ef5462" dependencies = [ "cfg-if", "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.38", ] [[package]] name = "test-case-macros" -version = "3.1.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeb9a44b1c6a54c1ba58b152797739dba2a83ca74e18168a68c980eb142f9404" +checksum = "37cfd7bbc88a0104e304229fba519bdc45501a30b760fb72240342f1289ad257" dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.38", "test-case-core", ] [[package]] name = "thiserror" -version = "1.0.44" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.44" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.38", ] [[package]] name = "time" -version = "0.3.25" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" dependencies = [ "deranged", "itoa", + "powerfmt", "serde", "time-core", "time-macros", @@ -2876,15 +3275,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" dependencies = [ "time-core", ] @@ -2915,11 +3314,10 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.29.1" +version = "1.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" dependencies = [ - "autocfg", "backtrace", "bytes", "libc", @@ -2928,7 +3326,7 @@ dependencies = [ "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.4", "tokio-macros", "windows-sys", ] @@ -2941,7 +3339,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.38", ] [[package]] @@ -2956,9 +3354,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" dependencies = [ "bytes", "futures-core", @@ -2976,11 +3374,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "ee2ef2af84856a50c1d430afce2fdded0a4ec7eda868db86409b4543df0797f9" dependencies = [ - "cfg-if", "log", "pin-project-lite", "tracing-attributes", @@ -2989,20 +3386,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.38", ] [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] @@ -3025,9 +3422,9 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "ucd-trie" @@ -3037,9 +3434,9 @@ checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" [[package]] name = "unicase" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" dependencies = [ "version_check", ] @@ -3052,9 +3449,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" @@ -3065,12 +3462,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-segmentation" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" - [[package]] name = "unicode-xid" version = "0.2.4" @@ -3087,6 +3478,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" + [[package]] name = "untrusted" version = "0.7.1" @@ -3095,15 +3492,21 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "value-bag" version = "1.4.1" @@ -3130,15 +3533,15 @@ checksum = "9dcc60c0624df774c82a0ef104151231d37da4962957d691c011c852b2473314" [[package]] name = "waker-fn" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" +checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" [[package]] name = "walkdir" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" dependencies = [ "same-file", "winapi-util", @@ -3153,12 +3556,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -3186,7 +3583,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.38", "wasm-bindgen-shared", ] @@ -3220,7 +3617,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.38", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3247,9 +3644,15 @@ version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338" dependencies = [ - "rustls-webpki 0.100.1", + "rustls-webpki 0.100.3", ] +[[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" @@ -3268,9 +3671,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] @@ -3281,6 +3684,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -3292,9 +3704,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", @@ -3307,51 +3719,60 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] [[package]] name = "zeroize" -version = "1.3.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" [[package]] name = "zstd" @@ -3374,11 +3795,10 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.8+zstd.1.5.5" +version = "2.0.9+zstd.1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" +checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" dependencies = [ "cc", - "libc", "pkg-config", ] diff --git a/Dockerfile b/Dockerfile index 927c327..2325f1a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.71-slim-bookworm as build +FROM rust:1.73-slim-bookworm as build # Create a new empty shell project. RUN USER=root cargo new --bin acceptxmr-server @@ -11,6 +11,7 @@ COPY ./Cargo.toml ./Cargo.toml COPY ./library/Cargo.toml ./library/Cargo.toml # Create main.rs so build succeeds. RUN cargo init server +RUN touch server/src/lib.rs RUN rm ./server/Cargo.toml COPY ./server/Cargo.toml ./server/Cargo.toml @@ -19,13 +20,14 @@ COPY ./library ./library # This build step will cache the dependencies (including the AcceptXMR lib). RUN cargo build --release -RUN rm ./server/src/*.rs # Copy the source tree. +RUN rm ./server/src/*.rs COPY ./server/src ./server/src # Build for release. RUN rm ./target/release/deps/acceptxmr_server* +RUN rm ./target/release/deps/libacceptxmr_server* RUN cargo build --release # Final base. @@ -37,11 +39,9 @@ COPY --from=build /acceptxmr-server/target/release/acceptxmr-server . # Copy the static files. COPY ./server/static ./server/static -# Add metadata that the container will listen to port 8080. +# Add metadata that the container will listen to port 8080 and 8081. EXPOSE 8080 - -# Set an environment variable so the AcceptXMR knows it's in a docker container. -ENV DOCKER=true +EXPOSE 8081 # Set the startup command to run the binary. CMD ["./acceptxmr-server"] diff --git a/acceptxmr.yaml b/acceptxmr.yaml new file mode 100644 index 0000000..32e9868 --- /dev/null +++ b/acceptxmr.yaml @@ -0,0 +1,70 @@ +# This is an example configuration. You should change the values below to suit +# your needs. + +# The external API can safely be served to end users. +external-api: + port: 8080 + + # Uncomment the line below to enable IPv6. + # ipv6: ::1 + + # If running inside docker, localhost will not work. Consider using `0.0.0.0` + # instead in that case. + ipv4: 127.0.0.1 + + # This example assumes AcceptXMR-Server is behind a reverse proxy, with TLS + # being provided by that reverse proxy. + tls: null + + # Specify where static HTML/CSS/JS files can be found. + static_dir: server/static/ + +# The internal API allows actions such as querying all invoices, or creating new +# invoices. In most use-cases, it should not be exposed to the end user. +internal-api: + port: 8081 + + # Uncomment the line below to enable IPv6. + # ipv6: ::1 + + # If running inside docker, localhost will not work. Consider using `0.0.0.0` + # instead in that case. + ipv4: 127.0.0.1 + + # If you are using a token to secure this API, TLS must be configured to + # protect the token "in flight". The token can be set using the + # INTERNAL_API_TOKEN environment variable. + # + # If the specified certificate and key cannot be found, a warning will be + # logged and a self-signed certificate and key will be generated and placed at + # the specified locations instead. + tls: + cert: server/tests/testdata/cert/certificate.pem + key: server/tests/testdata/cert/privatekey.pem + + # Specify where static HTML/CSS/JS files can be found. + static_dir: server/static/ + +# Remember to change the address below to your own. You will also need to set +# your private viewkey using the PRIVATE_VIEWKEY environment variable. +# +# For best protection against the burning bug, you should use a fresh wallet or +# account index that is only used with AcceptXMR so that AcceptXMR can reliably +# track used stealth addresses. +wallet: + primary-address: 4613YiHLM6JMH4zejMB2zJY5TwQCxL8p65ufw8kBP5yxX9itmuGLqp1dS4tkVoTxjyH3aYhYNrtGHbQzJQP5bFus3KHVdmf + account-index: 0 + # The restore height of your wallet. This is used for burning bug mitigation. + # AcceptXMR will sync from this height the first time it is run. If `null`, + # AcceptXMR will skip to the blockchain tip the first time it runs. + restore-height: null + +daemon: + url: http://xmr-node.cakewallet.com:18081/ + login: null + +database: + path: AcceptXMR_DB/ + +logging: + verbosity: DEBUG diff --git a/docker-compose.yml b/docker-compose.yml index e7165a7..33e5894 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,23 @@ +# To use this docker-compose file: +# 1. Install Docker: https://docs.docker.com/get-docker/ +# 2. Clone this repository: +# $ git clone https://github.com/busyboredom/acceptxmr.git && cd acceptxmr +# 3. Run it: +# $ docker compose up +# +# This file builds AcceptXMR-Server locally instead of pulling it from docker +# hub. + services: server: build: . ports: - "8080:8080" + - "8081:8081" volumes: - db:/AcceptXMR_DB + - ./server/tests/testdata/cert:/server/tests/testdata/cert/ + - ./acceptxmr.yaml:/acceptxmr.yaml + env_file: .env volumes: db: diff --git a/docker.sh b/docker.sh new file mode 100644 index 0000000..897b155 --- /dev/null +++ b/docker.sh @@ -0,0 +1,23 @@ +#! /bin/bash + +# To use this command: +# 1. Install Docker: https://docs.docker.com/get-docker/ +# 2. Clone this repository: +# $ git clone https://github.com/busyboredom/acceptxmr.git && cd acceptxmr +# 3. Build the image: +# $ docker build . -t acceptxmr +# 4. Run it: +# $ sh docker.sh +# +# This command builds AcceptXMR-Server locally instead of pulling it from docker +# hub. + +docker run \ + --name acceptxmr \ + -p 8080:8080 \ + -p 8081:8081 \ + --mount type=bind,source=${PWD}/AcceptXMR_DB,target=/AcceptXMR_DB \ + --mount type=bind,source=${PWD}/server/tests/testdata/cert,target=/server/tests/testdata/cert \ + --mount type=bind,source=${PWD}/acceptxmr.yaml,target=/acceptxmr.yaml \ + --env-file .env \ + acceptxmr diff --git a/flake.lock b/flake.lock index 9fb11ce..a9b9982 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1689068808, - "narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=", + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", "owner": "numtide", "repo": "flake-utils", - "rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", "type": "github" }, "original": { @@ -19,11 +19,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1691403902, - "narHash": "sha256-J74y4xWtKPDPyVtF4arzrwuSOGznlFlJ+uB9RwNNnbo=", + "lastModified": 1695318763, + "narHash": "sha256-FHVPDRP2AfvsxAdc+AsgFJevMz5VBmnZglFUMlxBkcY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "c91024273f020df2dcb209cc133461ca17848026", + "rev": "e12483116b3b51a185a33a272bf351e357ba9a99", "type": "github" }, "original": { diff --git a/library/Cargo.toml b/library/Cargo.toml index 014a521..d50f042 100644 --- a/library/Cargo.toml +++ b/library/Cargo.toml @@ -2,7 +2,7 @@ name = "acceptxmr" version = "0.13.0" edition = "2021" -rust-version = "1.68" +rust-version = "1.70" license = "MIT OR Apache-2.0" description = "Accept monero in your application." repository = "https://github.com/busyboredom/acceptxmr" @@ -27,10 +27,10 @@ hyper-rustls = { version = "0.24", features = ["logging", "http1", "http2", "tls indexmap = "2" log = "0.4" md-5 = "0.10" -monero = "0.18" +monero = "0.19" rand = "0.8" rand_chacha = "0.3" -serde = {version = "1", features = ["derive"], optional = true } +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 } @@ -48,7 +48,7 @@ sqlite = ["bincode", "dep:sqlite"] [dev-dependencies] actix = "0.13" actix-files = "0.6" -actix-session = { version = "0.7", features = ["cookie-session"] } +actix-session = { version = "0.8", features = ["cookie-session"] } actix-web = "4" actix-web-actors = "4" bytestring = "1" diff --git a/library/README.md b/library/README.md index ffdabe7..1530df2 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.68.0-blue)](https://blog.rust-lang.org/2023/03/09/Rust-1.68.0.html) +[![MSRV](https://img.shields.io/badge/MSRV-1.70.0-blue)](https://blog.rust-lang.org/2023/06/01/Rust-1.70.0.html) # `AcceptXMR`: Accept Monero in Your Application `AcceptXMR` is a library for building payment gateways. diff --git a/library/src/caching/txpool_cache.rs b/library/src/caching/txpool_cache.rs index ee21c1d..b6d4f68 100644 --- a/library/src/caching/txpool_cache.rs +++ b/library/src/caching/txpool_cache.rs @@ -43,7 +43,7 @@ impl TxpoolCache { // // TODO: Find a way to do this without cloning. let rpc_client = self.rpc_client.clone(); - let (new_transactions, _) = join!(rpc_client.transactions_by_hashes(&new_hashes), async { + let (new_transactions, ()) = join!(rpc_client.transactions_by_hashes(&new_hashes), async { self.transactions.retain(|k, _| txpool_hashes.contains(k)); self.discovered_transfers .retain(|k, _| txpool_hashes.contains(k)); diff --git a/library/src/invoice.rs b/library/src/invoice.rs index 60f1fd7..dd721eb 100644 --- a/library/src/invoice.rs +++ b/library/src/invoice.rs @@ -31,6 +31,9 @@ pub struct Invoice { creation_height: u64, amount_requested: u64, pub(crate) amount_paid: u64, + /// The height at which the `Invoice` was fully paid. Will be `None` + /// if not yet fully paid, or if the required XMR is still in the + /// txpool (which has no height). pub(crate) paid_height: Option, confirmations_required: u64, pub(crate) current_height: u64, @@ -56,9 +59,6 @@ impl Invoice { creation_height, amount_requested, amount_paid: 0, - /// The height at which the `Invoice` was fully paid. Will be `None` - /// if not yet fully paid, or if the required XMR is still in the - /// txpool (which has no height). paid_height: None, confirmations_required, current_height: 0, diff --git a/library/src/lib.rs b/library/src/lib.rs index 3d4e4fe..ed1708e 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -157,7 +157,7 @@ pub enum AcceptXmrError { /// An error storing/retrieving data from the storage layer. #[error("storage error: {0}")] Storage(Box), - /// [`Subscriber`](crate::Subscriber) failed to retrieve update. + /// [`Subscriber`] failed to retrieve update. #[error("subscriber failed to receive update: {0}")] Subscriber(#[from] SubscriberError), /// Failure to unblind the amount of an owned output. diff --git a/library/src/payment_gateway.rs b/library/src/payment_gateway.rs index 57bad86..c00041e 100644 --- a/library/src/payment_gateway.rs +++ b/library/src/payment_gateway.rs @@ -211,7 +211,7 @@ impl PaymentGateway { .unwrap_or_else(PoisonError::into_inner) .take(); match owned_handle.map(ScannerHandle::join) { - None | Some(Ok(Ok(_))) => PaymentGatewayStatus::NotRunning, + None | Some(Ok(Ok(()))) => PaymentGatewayStatus::NotRunning, Some(Ok(Err(e))) => PaymentGatewayStatus::Error(e), Some(Err(_)) => { PaymentGatewayStatus::Error(AcceptXmrError::ScanningThreadPanic) @@ -244,7 +244,7 @@ impl PaymentGateway { { None => Ok(()), Some(thread) if thread.is_finished() => match thread.join() { - Ok(Ok(_)) => Ok(()), + Ok(Ok(())) => Ok(()), Ok(Err(e)) => Err(e), Err(_) => Err(AcceptXmrError::ScanningThreadPanic), }, @@ -256,7 +256,7 @@ impl PaymentGateway { .send(MessageToScanner::Stop) .map_err(|e| AcceptXmrError::StopSignal(e.to_string()))?; match thread.join() { - Ok(Ok(_)) => Ok(()), + Ok(Ok(())) => Ok(()), Ok(Err(e)) => Err(e), Err(_) => Err(AcceptXmrError::ScanningThreadPanic), } diff --git a/library/src/storage/stores/sled.rs b/library/src/storage/stores/sled.rs index 0e9f6c7..26f166e 100644 --- a/library/src/storage/stores/sled.rs +++ b/library/src/storage/stores/sled.rs @@ -76,7 +76,7 @@ impl InvoiceStorage for Sled { .compare_and_swap(key, None::, Some(value)) .map_err(DatabaseError::from)? { - Ok(_) => Ok(()), + Ok(()) => Ok(()), Err(_) => Err(SledStorageError::DuplicateInvoiceId), } } diff --git a/library/tests/common/mod.rs b/library/tests/common/mod.rs index 19f79bb..19ec045 100644 --- a/library/tests/common/mod.rs +++ b/library/tests/common/mod.rs @@ -12,7 +12,6 @@ use log::LevelFilter; use serde_json::{json, Value}; use tempfile::Builder; use test_case::test_case; -use tokio::runtime::Runtime; pub const PRIVATE_VIEW_KEY: &str = "ad2093a5705b9f33e6f0f0c1bc1f5f639c756cdfc168c8f2ac6127ccbdab3a03"; @@ -132,9 +131,9 @@ impl Deref for MockDaemon { } impl MockDaemon { - pub fn new_mock_daemon() -> MockDaemon { + pub async fn new_mock_daemon() -> MockDaemon { let mock_daemon = MockDaemon { - server: MockServer::start(), + server: MockServer::start_async().await, daemon_height_id: Mutex::new(None), block_ids: Mutex::new(HashMap::new()), txpool_id: Mutex::new(None), @@ -341,14 +340,14 @@ impl MockDaemon { #[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 reproducible_rand(store: S) +#[tokio::test] +async fn reproducible_rand(store: S) where S: Storage + 'static, { // Setup. init_logger(); - let mock_daemon = MockDaemon::new_mock_daemon(); - let rt = Runtime::new().expect("failed to create tokio runtime"); + let mock_daemon = MockDaemon::new_mock_daemon().await; // Create payment gateway pointing at temp directory and mock daemon. let payment_gateway = PaymentGatewayBuilder::new( @@ -365,28 +364,26 @@ where .expect("failed to build payment gateway"); // Run it. - rt.block_on(async { - payment_gateway - .run() - .await - .expect("failed to run payment gateway"); - - // Add the invoice. - let invoice_id = payment_gateway - .new_invoice(1, 5, 10, "test invoice".to_string()) - .expect("failed to add new invoice to payment gateway for tracking"); - let mut subscriber = payment_gateway - .subscribe(invoice_id) - .expect("invoice does not exist"); - - // Get initial update. - let update = subscriber - .recv_timeout(Duration::from_millis(5000)) - .await - .expect("timeout waiting for invoice update") - .expect("subscription channel is closed"); - - // Check that it is as expected. - assert_eq!(update.index(), SubIndex::new(1, 97)); - }) + payment_gateway + .run() + .await + .expect("failed to run payment gateway"); + + // Add the invoice. + let invoice_id = payment_gateway + .new_invoice(1, 5, 10, "test invoice".to_string()) + .expect("failed to add new invoice to payment gateway for tracking"); + let mut subscriber = payment_gateway + .subscribe(invoice_id) + .expect("invoice does not exist"); + + // Get initial update. + let update = subscriber + .recv_timeout(Duration::from_secs(120)) + .await + .expect("timeout waiting for invoice update") + .expect("subscription channel is closed"); + + // Check that it is as expected. + assert_eq!(update.index(), SubIndex::new(1, 97)); } diff --git a/library/tests/integration_tests/block_cache.rs b/library/tests/integration_tests/block_cache.rs index 20d3687..c269042 100644 --- a/library/tests/integration_tests/block_cache.rs +++ b/library/tests/integration_tests/block_cache.rs @@ -8,7 +8,6 @@ use acceptxmr::{ PaymentGatewayBuilder, SubIndex, }; use test_case::test_case; -use tokio::runtime::Runtime; use crate::common::{ init_logger, new_temp_dir, MockDaemon, MockInvoice, PRIMARY_ADDRESS, PRIVATE_VIEW_KEY, @@ -17,14 +16,14 @@ use crate::common::{ #[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 fix_reorg(store: S) +#[tokio::test] +async fn fix_reorg(store: S) where S: Storage + 'static, { // Setup. init_logger(); - let mock_daemon = MockDaemon::new_mock_daemon(); - let rt = Runtime::new().expect("failed to create tokio runtime"); + let mock_daemon = MockDaemon::new_mock_daemon().await; // Create payment gateway pointing at temp directory and mock daemon. let payment_gateway = PaymentGatewayBuilder::new( @@ -41,73 +40,71 @@ where .expect("failed to build payment gateway"); // Run it. - rt.block_on(async { - payment_gateway - .run() - .await - .expect("failed to run payment gateway"); - - // Add the invoice. - let invoice_id = payment_gateway - .new_invoice(70000000, 2, 7, "invoice".to_string()) - .expect("failed to add new invoice to payment gateway for tracking"); - let mut subscriber = payment_gateway - .subscribe(invoice_id) - .expect("invoice does not exist"); - - // Get initial update. - let update = subscriber - .recv_timeout(Duration::from_millis(5000)) - .await - .expect("timeout waiting for invoice update") - .expect("subscription channel is closed"); - - let mut expected = MockInvoice::new( - Some(update.address().to_string()), - SubIndex::new(1, 97), - 2477657, - 70000000, - 2, - 7, - "invoice".to_string(), - ); - - // Check that it is as expected. - expected.assert_eq(&update); - - mock_daemon.mock_daemon_height(2477658); - - let update = subscriber - .recv_timeout(Duration::from_millis(5000)) - .await - .expect("timeout waiting for invoice update") - .expect("subscription channel is closed"); - - expected.amount_paid = 37419570; - expected.expires_in = 6; - expected.current_height = 2477658; - expected.assert_eq(&update); - - // Reorg to invalidate payment. - mock_daemon.mock_alt_2477657(); - mock_daemon.mock_alt_2477658(); - - subscriber - .recv_timeout(Duration::from_millis(5000)) - .await - .expect_err("should not have received an update, but did"); - - mock_daemon.mock_daemon_height(24776659); - - let update = subscriber - .recv_timeout(Duration::from_millis(5000)) - .await - .expect("timeout waiting for invoice update") - .expect("subscription channel is closed"); - - expected.amount_paid = 0; - expected.expires_in = 5; - expected.current_height = 2477659; - expected.assert_eq(&update); - }) + payment_gateway + .run() + .await + .expect("failed to run payment gateway"); + + // Add the invoice. + let invoice_id = payment_gateway + .new_invoice(70000000, 2, 7, "invoice".to_string()) + .expect("failed to add new invoice to payment gateway for tracking"); + let mut subscriber = payment_gateway + .subscribe(invoice_id) + .expect("invoice does not exist"); + + // Get initial update. + let update = subscriber + .recv_timeout(Duration::from_secs(120)) + .await + .expect("timeout waiting for invoice update") + .expect("subscription channel is closed"); + + let mut expected = MockInvoice::new( + Some(update.address().to_string()), + SubIndex::new(1, 97), + 2477657, + 70000000, + 2, + 7, + "invoice".to_string(), + ); + + // Check that it is as expected. + expected.assert_eq(&update); + + mock_daemon.mock_daemon_height(2477658); + + let update = subscriber + .recv_timeout(Duration::from_secs(120)) + .await + .expect("timeout waiting for invoice update") + .expect("subscription channel is closed"); + + expected.amount_paid = 37419570; + expected.expires_in = 6; + expected.current_height = 2477658; + expected.assert_eq(&update); + + // Reorg to invalidate payment. + mock_daemon.mock_alt_2477657(); + mock_daemon.mock_alt_2477658(); + + subscriber + .recv_timeout(Duration::from_secs(1)) + .await + .expect_err("should not have received an update, but did"); + + mock_daemon.mock_daemon_height(24776659); + + let update = subscriber + .recv_timeout(Duration::from_secs(120)) + .await + .expect("timeout waiting for invoice update") + .expect("subscription channel is closed"); + + expected.amount_paid = 0; + expected.expires_in = 5; + expected.current_height = 2477659; + expected.assert_eq(&update); } diff --git a/library/tests/integration_tests/invoice_tracking.rs b/library/tests/integration_tests/invoice_tracking.rs index 06a62e7..47d7496 100644 --- a/library/tests/integration_tests/invoice_tracking.rs +++ b/library/tests/integration_tests/invoice_tracking.rs @@ -9,7 +9,6 @@ use acceptxmr::{ }; use monero::consensus::deserialize; use test_case::test_case; -use tokio::runtime::Runtime; use crate::common::{ init_logger, new_temp_dir, MockDaemon, MockInvoice, PRIMARY_ADDRESS, PRIVATE_VIEW_KEY, @@ -18,14 +17,14 @@ use crate::common::{ #[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 new_invoice(store: S) +#[tokio::test] +async fn new_invoice(store: S) where S: Storage + 'static, { // Setup. init_logger(); - let mock_daemon = MockDaemon::new_mock_daemon(); - let rt = Runtime::new().expect("failed to create tokio runtime"); + let mock_daemon = MockDaemon::new_mock_daemon().await; // Create payment gateway pointing at temp directory and mock daemon. let payment_gateway = PaymentGatewayBuilder::new( @@ -40,58 +39,56 @@ where .expect("failed to build payment gateway"); // Run it. - rt.block_on(async { + payment_gateway + .run() + .await + .expect("failed to run payment gateway"); + + // Add the invoice. + let invoice_id = payment_gateway + .new_invoice(1, 5, 10, "test invoice".to_string()) + .expect("failed to add new invoice to payment gateway for tracking"); + let mut subscriber = payment_gateway + .subscribe(invoice_id) + .expect("invoice does not exist"); + + // Get initial update. + let update = subscriber + .recv_timeout(Duration::from_secs(120)) + .await + .expect("timeout waiting for invoice update") + .expect("subscription channel is closed"); + + // Check that it is as expected. + assert_eq!(update.amount_requested(), 1); + assert_eq!(update.amount_paid(), 0); + assert!(!update.is_expired()); + assert!(!update.is_confirmed()); + assert_eq!(update.expiration_height() - update.creation_height(), 10); + assert_eq!(update.creation_height(), update.current_height()); + assert_eq!(update.confirmations_required(), 5); + assert_eq!(update.confirmations(), None); + assert_eq!(update.description(), "test invoice".to_string()); + assert_eq!( + update.current_height(), payment_gateway - .run() + .daemon_height() .await - .expect("failed to run payment gateway"); - - // Add the invoice. - let invoice_id = payment_gateway - .new_invoice(1, 5, 10, "test invoice".to_string()) - .expect("failed to add new invoice to payment gateway for tracking"); - let mut subscriber = payment_gateway - .subscribe(invoice_id) - .expect("invoice does not exist"); - - // Get initial update. - let update = subscriber - .recv_timeout(Duration::from_millis(5000)) - .await - .expect("timeout waiting for invoice update") - .expect("subscription channel is closed"); - - // Check that it is as expected. - assert_eq!(update.amount_requested(), 1); - assert_eq!(update.amount_paid(), 0); - assert!(!update.is_expired()); - assert!(!update.is_confirmed()); - assert_eq!(update.expiration_height() - update.creation_height(), 10); - assert_eq!(update.creation_height(), update.current_height()); - assert_eq!(update.confirmations_required(), 5); - assert_eq!(update.confirmations(), None); - assert_eq!(update.description(), "test invoice".to_string()); - assert_eq!( - update.current_height(), - payment_gateway - .daemon_height() - .await - .expect("failed to retrieve daemon height") - ); - }) + .expect("failed to retrieve daemon height") + ); } #[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 default_account_index(store: S) +#[tokio::test] +async fn default_account_index(store: S) where S: Storage + 'static, { // Setup. init_logger(); - let mock_daemon = MockDaemon::new_mock_daemon(); - let rt = Runtime::new().expect("failed to create tokio runtime"); + let mock_daemon = MockDaemon::new_mock_daemon().await; // Create payment gateway pointing at temp directory and mock daemon. let payment_gateway = PaymentGatewayBuilder::new( @@ -107,79 +104,77 @@ where .expect("failed to build payment gateway"); // Run it. - rt.block_on(async { + payment_gateway + .run() + .await + .expect("failed to run payment gateway"); + + // Add the invoice. + let invoice_id = payment_gateway + .new_invoice(1, 5, 10, "test invoice".to_string()) + .expect("failed to add new invoice to payment gateway for tracking"); + let mut subscriber = payment_gateway + .subscribe(invoice_id) + .expect("invoice does not exist"); + + // Get initial update. + let update = subscriber + .recv_timeout(Duration::from_secs(120)) + .await + .expect("timeout waiting for invoice update") + .expect("subscription channel is closed"); + + let mut expected = MockInvoice::new( + Some(update.address().to_string()), + SubIndex::new(0, 97), + 2477657, + 1, + 5, + 10, + "test invoice".to_string(), + ); + + // Check that it is as expected. + expected.assert_eq(&update); + assert_eq!( + update.current_height(), payment_gateway - .run() - .await - .expect("failed to run payment gateway"); - - // Add the invoice. - let invoice_id = payment_gateway - .new_invoice(1, 5, 10, "test invoice".to_string()) - .expect("failed to add new invoice to payment gateway for tracking"); - let mut subscriber = payment_gateway - .subscribe(invoice_id) - .expect("invoice does not exist"); - - // Get initial update. - let update = subscriber - .recv_timeout(Duration::from_millis(5000)) - .await - .expect("timeout waiting for invoice update") - .expect("subscription channel is closed"); - - let mut expected = MockInvoice::new( - Some(update.address().to_string()), - SubIndex::new(0, 97), - 2477657, - 1, - 5, - 10, - "test invoice".to_string(), - ); - - // Check that it is as expected. - expected.assert_eq(&update); - assert_eq!( - update.current_height(), - payment_gateway - .daemon_height() - .await - .expect("failed to retrieve daemon height") - ); - - // 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("tests/rpc_resources/txpools/hashes_with_payment_account_0.json"); - - // Get update. - let update = subscriber - .recv_timeout(Duration::from_millis(5000)) - .await - .expect("timeout waiting for invoice update") - .expect("subscription channel is closed"); - - expected.amount_paid = 1468383460; - expected.confirmations = Some(0); - expected.assert_eq(&update); - }) + .daemon_height() + .await + .expect("failed to retrieve daemon height") + ); + + // 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("tests/rpc_resources/txpools/hashes_with_payment_account_0.json"); + + // Get update. + let update = subscriber + .recv_timeout(Duration::from_secs(120)) + .await + .expect("timeout waiting for invoice update") + .expect("subscription channel is closed"); + + expected.amount_paid = 1468383460; + expected.confirmations = Some(0); + expected.assert_eq(&update); } #[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 zero_conf_invoice(store: S) +#[tokio::test] +async fn zero_conf_invoice(store: S) where S: Storage + 'static, { // Setup. init_logger(); - let mock_daemon = MockDaemon::new_mock_daemon(); - let rt = Runtime::new().expect("failed to create tokio runtime"); + let mock_daemon = MockDaemon::new_mock_daemon().await; // Create payment gateway pointing at temp directory and mock daemon. let payment_gateway = PaymentGatewayBuilder::new( @@ -196,69 +191,67 @@ where .expect("failed to build payment gateway"); // Run it. - rt.block_on(async { - payment_gateway - .run() - .await - .expect("failed to run payment gateway"); - - // Add the invoice. - let invoice_id = payment_gateway - .new_invoice(37419570, 0, 10, "test invoice".to_string()) - .expect("failed to add new invoice to payment gateway for tracking"); - let mut subscriber = payment_gateway - .subscribe(invoice_id) - .expect("invoice does not exist"); - - // Get initial update. - let update = subscriber - .recv_timeout(Duration::from_millis(5000)) - .await - .expect("timeout waiting for invoice update") - .expect("subscription channel is closed"); - - let mut expected = MockInvoice::new( - Some(update.address().to_string()), - SubIndex::new(1, 97), - 2477657, - 37419570, - 0, - 10, - "test invoice".to_string(), - ); - - // Check that it is as expected. - 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"); - - // Get update. - let update = subscriber - .recv_timeout(Duration::from_millis(5000)) - .await - .expect("timeout waiting for invoice update") - .expect("subscription channel is closed"); - - expected.amount_paid = 37419570; - expected.confirmations = Some(0); - expected.is_confirmed = true; - expected.assert_eq(&update); - }) + payment_gateway + .run() + .await + .expect("failed to run payment gateway"); + + // Add the invoice. + let invoice_id = payment_gateway + .new_invoice(37419570, 0, 10, "test invoice".to_string()) + .expect("failed to add new invoice to payment gateway for tracking"); + let mut subscriber = payment_gateway + .subscribe(invoice_id) + .expect("invoice does not exist"); + + // Get initial update. + let update = subscriber + .recv_timeout(Duration::from_secs(120)) + .await + .expect("timeout waiting for invoice update") + .expect("subscription channel is closed"); + + let mut expected = MockInvoice::new( + Some(update.address().to_string()), + SubIndex::new(1, 97), + 2477657, + 37419570, + 0, + 10, + "test invoice".to_string(), + ); + + // Check that it is as expected. + 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"); + + // Get update. + let update = subscriber + .recv_timeout(Duration::from_secs(120)) + .await + .expect("timeout waiting for invoice update") + .expect("subscription channel is closed"); + + expected.amount_paid = 37419570; + expected.confirmations = Some(0); + expected.is_confirmed = true; + expected.assert_eq(&update); } #[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 timelock_rejection(store: S) +#[tokio::test] +async fn timelock_rejection(store: S) where S: Storage + 'static, { // Setup. init_logger(); - let mock_daemon = MockDaemon::new_mock_daemon(); - let rt = Runtime::new().expect("failed to create tokio runtime"); + let mock_daemon = MockDaemon::new_mock_daemon().await; // Create payment gateway pointing at temp directory and mock daemon. let payment_gateway = PaymentGatewayBuilder::new( @@ -274,67 +267,65 @@ where .expect("failed to build payment gateway"); // Run it. - rt.block_on(async { - payment_gateway - .run() - .await - .expect("failed to run payment gateway"); - - // Add the invoice. - let invoice_id = payment_gateway - .new_invoice(123, 1, 1, "test invoice".to_string()) - .expect("failed to add new invoice to payment gateway for tracking"); - let mut subscriber = payment_gateway - .subscribe(invoice_id) - .expect("invoice does not exist"); - - // Get initial update. - let update = subscriber - .recv_timeout(Duration::from_millis(5000)) - .await - .expect("timeout waiting for invoice update") - .expect("subscription channel is closed"); - - let expected = MockInvoice::new( - Some(update.address().to_string()), - SubIndex::new(0, 97), - 2477657, - 123, - 1, - 1, - "test invoice".to_string(), - ); - - // Check that it is as expected. - 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 _transactions_mock = mock_daemon.mock_transactions( - "tests/rpc_resources/transactions/hashes_with_payment_timelock.json", - "tests/rpc_resources/transactions/txs_with_payment_timelock.json", - ); - - // There shouldn't be any update. - subscriber - .recv_timeout(Duration::from_millis(5000)) - .await - .expect_err("timeout waiting for invoice update"); - }) + payment_gateway + .run() + .await + .expect("failed to run payment gateway"); + + // Add the invoice. + let invoice_id = payment_gateway + .new_invoice(123, 1, 1, "test invoice".to_string()) + .expect("failed to add new invoice to payment gateway for tracking"); + let mut subscriber = payment_gateway + .subscribe(invoice_id) + .expect("invoice does not exist"); + + // Get initial update. + let update = subscriber + .recv_timeout(Duration::from_secs(120)) + .await + .expect("timeout waiting for invoice update") + .expect("subscription channel is closed"); + + let expected = MockInvoice::new( + Some(update.address().to_string()), + SubIndex::new(0, 97), + 2477657, + 123, + 1, + 1, + "test invoice".to_string(), + ); + + // Check that it is as expected. + 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 _transactions_mock = mock_daemon.mock_transactions( + "tests/rpc_resources/transactions/hashes_with_payment_timelock.json", + "tests/rpc_resources/transactions/txs_with_payment_timelock.json", + ); + + // There shouldn't be any update. + subscriber + .recv_timeout(Duration::from_secs(1)) + .await + .expect_err("timeout waiting for invoice update"); } #[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 burning_bug(mut store: S) +#[tokio::test] +async fn burning_bug(mut store: S) where S: Storage + 'static, { // Setup. init_logger(); - let mock_daemon = MockDaemon::new_mock_daemon(); - let rt = Runtime::new().expect("failed to create tokio runtime"); + let mock_daemon = MockDaemon::new_mock_daemon().await; let tx_hex = hex::decode("02000202000bde84861298e38c01c99338d5b005cab9038d26d8e301d442e304a419f406af616b0e66f90a5ff643ee8ff496a25e2caf3d8fccf65895073212a795ae7a7c02000ba09ef20f9da08e02dcb4a301eba60def810cc3b20e9e8801bf9101cdcf04f6ae02f7a50118130528cdc0df212e936b5c95b35ebcdecd443000e94b637782f47a2a47171c0300026f5955625996a0051ff3a419b5fe967fd4a691ad99b2e7dce6fcb35d21821f0d0002b024664bccba2226849a95dfe653c64e02c5498ea88265457c44f85778f018f700027fd35731e1bd4d1ef4bb1f0f008c4b0ba54bb214bb3304fab61c2b5e3a8570f4830101349587e7557632f77ab342e1fb69dc1d106900bc3557e45b9f18d117be363bbc0403909dd7c66c8dba74062fa6d53bcbaa9c3c3a2481dd23d27be3b178449a33303aa17cbf28ae53a1ed402ba4bda9466b92471ce9ddafd5c2ae6e07470ed4d210728a20e76551ee2b320b3fac21c6d823bd6bf2c645e039e226290bc427cc3621930590d99107d6f209fb953c4370924e52a5b1b3df48fbf8306474b69d0998c657aa6aeb1f2f2ee09d5c8e8acf0a32a76aa7f949305829b5a35d53a8b7f72a9960e05ce91198e32bb1f5b049ae2f09aba0975fca8df6f176c947f70170e1514c5c6a75bfcea627e9fcedc7d75fc4a8440b5c331ff2b8a06b0d9c01aca9a1016700a9b4ca3f1f52fccda5354b21bd22bd0e0e895a12925137873dc2a7591e2650ff864e696f540e3c081c4d8d839388447bbba489a0662ca001ad5ac4008d4586d19e4b629957a95b566ad0ce9f78e3e4ac8bd98781eec4d785587f1a3410251141bd0263b04ee2e4948fbd04e63469d6c78b07d16810ad36399e8d751e5437863acd407f54a3814e7f1293a292830cc56882793cb5710bd46a80366c86880ed5c018b026277beb0892896941fe57e5e74a2222627d1dc5d9bd7e89048e0e0308b76103351b7bb3f1a188136a594c1eaa210e35b8c8e6f496cf75cb4df26d41a2a4008fa53018c49b5814d191eb59f79def3599cedae951e1fabe03040aab2a88dda1ebe4280667f758bef886709de3b0cce728b7eec6fb715d167d209532320603b31dc492a21d3804dfe17737c4f50fdbc3cfeb092cdc607bf24fc2d069ca033e1bb17baac9b23db8b853296c82e5daf9d3b1e79bc300b5ba6f9a5f97a13b1d2f27d086c4e237404d2b06df5d2fd16a7f0206e4b534e0b1c2408e9946f0d22a805eb23ea77bf4c5470d7daf0b0ec6df7f923c53197b0cfe68232ae28f3d79472d3a691a5f1c54c3bd31b75510d4ecc54ffafd6c07d4a6ad99a177d544bbef9b08ccf52f7df026fc714f1b7f2501f220d90972c89c10f87dd63fa28f61c1e900cae45f3baa4a6d596f95cb1b603481345360fd728c7f65202b9701caca1d099fe0006eba48e052d52e8b30f116c1107aa98bd8e32288f3a9bd658bbae512d83eec72ecbed4c22b0bbdb34205edb3207c218254a0bc860cf7f9f6a5b67ec934b1517a282d97076f4d43a999aeddca80d6f949eb36b65252b29f9d0053829565d29022fc82c9128ce08733afae38aee26395eaaee03313d75d9f9d6dc1a2a554ca13018d6b8ffd47d097e8f77798d8f5431a93154c5c4588783318115cf9d23f53e296319792368fd762e7aed6c16f404e9cf17345655213f7d95834218ece675a07bf2f07920bfe0bc61ac73ed8fed0d177ab9e116ef0afff097f301216bb9ee80c18a207918b57b6a5dfc59b6a1e9e4420fe9b8355721e384d1af64fa73953b005aeff256d47e8c4666b0995315c039745dbb866fa3c2138d103d53c7a2411c80b8a07eacd709743eda1f7358265358944b017bbeecb8b2e4afd062da7bdcee50d97cfc304ffdb3fb2bf10eb6c79797298afcf7aa9a698f14310896ac1ea3e61044b73a0f9bd4ede0f1907734789b66f0752f2e40cb0aafae3c7ecf284a239e5086205377c5458c1e31762ad6eab90a234ba7699595bf805886b460f32dcae6601d3a8fb42c28d751e7e13ad8a896448688308490e657b24b15c7ade2a6f60aa046148e609e420c8418f945db4df76f4011987870fca43ad2e4dc31c9667cabd0cf192a860df67bec0d794f85d93c99e645a6b27dc51cfe4593a27570d7489ad090e55dde38cd6acaedd04eb88982904d217929e10bb29e599fd3e80a3d5a86202fb629a13cbb47c1b8edd56d1f39b74b1fc6f3e8294e02bcd74e8490344e5c70f001260de40fe26fd7289b5a271460168fdb828dda2d548ca395fc0481188c904bb1c900be39468da0dbd46203addd690bfd53b25b9e3a551a65fc99132abbc0b1aa7de6e2878f6a326aaa73ec10ec17cfb7b16ae83279fa644b5ad69fa4a44093a86c19d31279efc8361f0d71237051689e5538e3409056edb5cdf209c648c9086fa753ea32a85a9254651ed68fb9c4fa89340fdf2e97e84ace81e8aba8be50adea34b61bd4e16e53a49c8a6731e9ea6ac0806aa30cbcf02b54aaab7a604710e5d40354cd2d7ef23863bbc764f0398a1da11597d9a49130d2b22e95bfb62770dbb5815145c5fe442d21b671c2852a8695b58abaedc9de67708d4a18b03fd0f053b77aaecdd12603947be45e4e44a960365ad881f1f79fb098022f4766628a20668de0253abf0ef530ab111368bd8b8a4940dd5fea0c66c37c887e1343d58770de2632ac9ba7400e79c40ff2c4558ef768d8fb1f21d2341a00ecc57ff34aa170b9f665fec6f8c120acb300955e25fd5e04c22edd9b6f939ae2ab8a5bac1210d0ba66f5b2eb7113dc36d469893ca52f7d6110cf1d849b5f32ad308ca1b3935700b76ba4726fff29019f0b4a85a5c0c1e69310f597a37b3580af823bbd6c7dec80ab0aabfe62aa035ec5596f954d6477d47e76090a40d8354694965cb66ab3d280b0465558641f266cd84010b8554a6e394a9a2764ff9e7d355f0e76c5af11e170c847278309f7905d98213e59cc2435e06b8a563b5929676dca3de7eaa0fa970a09a65a845966d8300fa6b941a0fcef6a2daf8a92c1bc69de7a3b6290086ccac9e98d42bec94d2ad302cee609a1bd2e533675a9b5702f30a985a0578b2d823029d").unwrap(); let tx: monero::Transaction = deserialize(&tx_hex).unwrap(); @@ -362,67 +353,65 @@ where .expect("failed to build payment gateway"); // Run it. - rt.block_on(async { - payment_gateway - .run() - .await - .expect("failed to run payment gateway"); - - // Add the invoice. - let invoice_id = payment_gateway - .new_invoice(123, 1, 1, "test invoice".to_string()) - .expect("failed to add new invoice to payment gateway for tracking"); - let mut subscriber = payment_gateway - .subscribe(invoice_id) - .expect("invoice does not exist"); - - // Get initial update. - let update = subscriber - .recv_timeout(Duration::from_millis(5000)) - .await - .expect("timeout waiting for invoice update") - .expect("subscription channel is closed"); - - let expected = MockInvoice::new( - Some(update.address().to_string()), - SubIndex::new(1, 97), - 2477657, - 123, - 1, - 1, - "test invoice".to_string(), - ); - - // Check that it is as expected. - 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 _transactions_mock = mock_daemon.mock_transactions( - "tests/rpc_resources/transactions/hashes_with_payment.json", - "tests/rpc_resources/transactions/txs_with_payment.json", - ); - - // There shouldn't be any update. - subscriber - .recv_timeout(Duration::from_millis(5000)) - .await - .expect_err("timeout waiting for invoice update"); - }) + payment_gateway + .run() + .await + .expect("failed to run payment gateway"); + + // Add the invoice. + let invoice_id = payment_gateway + .new_invoice(123, 1, 1, "test invoice".to_string()) + .expect("failed to add new invoice to payment gateway for tracking"); + let mut subscriber = payment_gateway + .subscribe(invoice_id) + .expect("invoice does not exist"); + + // Get initial update. + let update = subscriber + .recv_timeout(Duration::from_secs(120)) + .await + .expect("timeout waiting for invoice update") + .expect("subscription channel is closed"); + + let expected = MockInvoice::new( + Some(update.address().to_string()), + SubIndex::new(1, 97), + 2477657, + 123, + 1, + 1, + "test invoice".to_string(), + ); + + // Check that it is as expected. + 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 _transactions_mock = mock_daemon.mock_transactions( + "tests/rpc_resources/transactions/hashes_with_payment.json", + "tests/rpc_resources/transactions/txs_with_payment.json", + ); + + // There shouldn't be any update. + subscriber + .recv_timeout(Duration::from_secs(1)) + .await + .expect_err("timeout waiting for invoice update"); } #[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 track_parallel_invoices(store: S) +#[tokio::test] +async fn track_parallel_invoices(store: S) where S: Storage + 'static, { // Setup. init_logger(); - let mock_daemon = MockDaemon::new_mock_daemon(); - let rt = Runtime::new().expect("failed to create tokio runtime"); + let mock_daemon = MockDaemon::new_mock_daemon().await; // Create payment gateway pointing at temp directory and mock daemon. let payment_gateway = PaymentGatewayBuilder::new( @@ -439,248 +428,246 @@ where .expect("failed to build payment gateway"); // Run it. - rt.block_on(async { - payment_gateway - .run() - .await - .expect("failed to run payment gateway"); - - // Add the invoice. - let invoice_id = payment_gateway - .new_invoice(70000000, 2, 7, "invoice 1".to_string()) - .expect("failed to add new invoice to payment gateway for tracking"); - let mut subscriber_1 = payment_gateway - .subscribe(invoice_id) - .expect("invoice does not exist"); - - // Get initial update. - let update = subscriber_1 - .recv_timeout(Duration::from_millis(5000)) - .await - .expect("timeout waiting for invoice update") - .expect("subscription channel is closed"); - - let mut expected_1 = MockInvoice::new( - Some(update.address().to_string()), - SubIndex::new(1, 97), - 2477657, - 70000000, - 2, - 7, - "invoice 1".to_string(), - ); - - // 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()) - .expect("failed to add new invoice to payment gateway for tracking"); - let mut subscriber_2 = payment_gateway - .subscribe(invoice_id) - .expect("invoice does not exist"); - - // Get initial update. - let update = subscriber_2 - .recv_timeout(Duration::from_millis(5000)) - .await - .expect("timeout waiting for invoice update") - .expect("subscription channel is closed"); + payment_gateway + .run() + .await + .expect("failed to run payment gateway"); + + // Add the invoice. + let invoice_id = payment_gateway + .new_invoice(70000000, 2, 7, "invoice 1".to_string()) + .expect("failed to add new invoice to payment gateway for tracking"); + let mut subscriber_1 = payment_gateway + .subscribe(invoice_id) + .expect("invoice does not exist"); + + // Get initial update. + let update = subscriber_1 + .recv_timeout(Duration::from_secs(120)) + .await + .expect("timeout waiting for invoice update") + .expect("subscription channel is closed"); + + let mut expected_1 = MockInvoice::new( + Some(update.address().to_string()), + SubIndex::new(1, 97), + 2477657, + 70000000, + 2, + 7, + "invoice 1".to_string(), + ); + + // 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()) + .expect("failed to add new invoice to payment gateway for tracking"); + let mut subscriber_2 = payment_gateway + .subscribe(invoice_id) + .expect("invoice does not exist"); + + // Get initial update. + let update = subscriber_2 + .recv_timeout(Duration::from_secs(120)) + .await + .expect("timeout waiting for invoice update") + .expect("subscription channel is closed"); + + let mut expected_2 = expected_1.clone(); + expected_2.address = Some(update.address().to_string()); + expected_2.index = SubIndex::new(1, 138); + expected_2.description = "invoice 2".to_string(); + + // Check that it is as expected. + 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"); + // Mock for these transactions themselves is unnecessary, because they are all + // in block 2477657. + + // Get update. + let update = subscriber_1 + .recv_timeout(Duration::from_secs(120)) + .await + .expect("timeout waiting for invoice update") + .expect("subscription channel is closed"); + + // Check that it is as expected. + expected_1.amount_paid = 37419570; + expected_1.assert_eq(&update); + + // Get update. + let update = subscriber_2 + .recv_timeout(Duration::from_secs(120)) + .await + .expect("timeout waiting for invoice update") + .expect("subscription channel is closed"); + + // Check that it is as expected. + expected_2.amount_paid = 37419570; + 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"); + + // Both invoices should now show zero paid. + 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.amount_paid(), 0); + let update = subscriber_2 + .recv_timeout(Duration::from_secs(120)) + .await + .expect("timeout waiting for invoice update") + .expect("subscription channel is closed"); + assert_eq!(update.amount_paid(), 0); + + // Move forward a few blocks. + for height in 2477658..2477663 { + let height_mock = mock_daemon.mock_daemon_height(height); - let mut expected_2 = expected_1.clone(); - expected_2.address = Some(update.address().to_string()); - expected_2.index = SubIndex::new(1, 138); - expected_2.description = "invoice 2".to_string(); - - // Check that it is as expected. - 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"); - // Mock for these transactions themselves is unnecessary, because they are all - // in block 2477657. - - // Get update. let update = subscriber_1 - .recv_timeout(Duration::from_millis(5000)) + .recv_timeout(Duration::from_secs(120)) .await .expect("timeout waiting for invoice update") .expect("subscription channel is closed"); - // Check that it is as expected. - expected_1.amount_paid = 37419570; + expected_1.expires_in = 2477664 - height; + expected_1.current_height = height; expected_1.assert_eq(&update); - // Get update. let update = subscriber_2 - .recv_timeout(Duration::from_millis(5000)) + .recv_timeout(Duration::from_secs(120)) .await .expect("timeout waiting for invoice update") .expect("subscription channel is closed"); - // Check that it is as expected. - expected_2.amount_paid = 37419570; + expected_2.expires_in = 2477664 - height; + expected_2.current_height = height; 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"); - - // Both invoices should now show zero paid. - let update = subscriber_1 - .recv_timeout(Duration::from_millis(5000)) - .await - .expect("timeout waiting for invoice update") - .expect("subscription channel is closed"); - assert_eq!(update.amount_paid(), 0); - let update = subscriber_2 - .recv_timeout(Duration::from_millis(5000)) - .await - .expect("timeout waiting for invoice update") - .expect("subscription channel is closed"); - assert_eq!(update.amount_paid(), 0); - - // Move forward a few blocks. - for height in 2477658..2477663 { - let height_mock = mock_daemon.mock_daemon_height(height); - - let update = subscriber_1 - .recv_timeout(Duration::from_millis(5000)) - .await - .expect("timeout waiting for invoice update") - .expect("subscription channel is closed"); - - expected_1.expires_in = 2477664 - height; - expected_1.current_height = height; - expected_1.assert_eq(&update); - - let update = subscriber_2 - .recv_timeout(Duration::from_millis(5000)) - .await - .expect("timeout waiting for invoice update") - .expect("subscription channel is closed"); - - expected_2.expires_in = 2477664 - height; - expected_2.current_height = height; - expected_2.assert_eq(&update); - - assert!(height_mock.hits() > 0); - } - - // 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_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", - ); - - // Invoice 1 should be paid now. - let update = subscriber_1 - .recv_timeout(Duration::from_millis(5000)) - .await - .expect("timeout waiting for invoice update") - .expect("subscription channel is closed"); - - expected_1.amount_paid = 74839140; - expected_1.confirmations = Some(0); - expected_1.assert_eq(&update); - - // Invoice 2 should not have an update. - subscriber_2 - .recv_timeout(Duration::from_millis(5000)) - .await - .expect_err("should not have received an update, but did"); - - assert!(txpool_hashes_mock.hits() > 0); - assert!(txpool_transactions_mock.hits() > 0); - - // Move forward a block - // (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"); - subscriber_1 - .recv_timeout(Duration::from_millis(5000)) - .await - .expect("timeout waiting for invoice update") - .expect("subscription channel is closed"); - subscriber_2 - .recv_timeout(Duration::from_millis(5000)) - .await - .expect_err("should not have received an update, but did"); - let height_mock = mock_daemon.mock_daemon_height(2477663); - - let update = subscriber_1 - .recv_timeout(Duration::from_millis(5000)) - .await - .expect("timeout waiting for invoice update") - .expect("subscription channel is closed"); - - expected_1.confirmations = Some(1); - expected_1.expires_in = 1; - expected_1.current_height = 2477663; - expected_1.assert_eq(&update); - - let update = subscriber_2 - .recv_timeout(Duration::from_millis(5000)) - .await - .expect("timeout waiting for invoice update") - .expect("subscription channel is closed"); - - expected_2.expires_in = 1; - expected_2.current_height = 2477663; - 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 update = subscriber_1 - .recv_timeout(Duration::from_millis(5000)) - .await - .expect("timeout waiting for invoice update") - .expect("subscription channel is closed"); - - expected_1.confirmations = Some(2); - expected_1.is_confirmed = true; - expected_1.expires_in = 0; - expected_1.current_height = 2477664; - expected_1.assert_eq(&update); - - let update = subscriber_2 - .recv_timeout(Duration::from_millis(5000)) - .await - .expect("timeout waiting for invoice update") - .expect("subscription channel is closed"); - - expected_2.expires_in = 0; - expected_2.is_expired = true; - expected_2.current_height = 2477664; - expected_2.assert_eq(&update); - - assert!(txpool_hashes_mock.hits() > 0); - assert!(height_mock.hits() > 0); - }) + } + + // 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_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", + ); + + // Invoice 1 should be paid now. + let update = subscriber_1 + .recv_timeout(Duration::from_secs(120)) + .await + .expect("timeout waiting for invoice update") + .expect("subscription channel is closed"); + + expected_1.amount_paid = 74839140; + expected_1.confirmations = Some(0); + expected_1.assert_eq(&update); + + // Invoice 2 should not have an update. + subscriber_2 + .recv_timeout(Duration::from_secs(1)) + .await + .expect_err("should not have received an update, but did"); + + assert!(txpool_hashes_mock.hits() > 0); + assert!(txpool_transactions_mock.hits() > 0); + + // Move forward a block + // (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"); + subscriber_1 + .recv_timeout(Duration::from_secs(120)) + .await + .expect("timeout waiting for invoice update") + .expect("subscription channel is closed"); + subscriber_2 + .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 update = subscriber_1 + .recv_timeout(Duration::from_secs(120)) + .await + .expect("timeout waiting for invoice update") + .expect("subscription channel is closed"); + + expected_1.confirmations = Some(1); + expected_1.expires_in = 1; + expected_1.current_height = 2477663; + expected_1.assert_eq(&update); + + let update = subscriber_2 + .recv_timeout(Duration::from_secs(120)) + .await + .expect("timeout waiting for invoice update") + .expect("subscription channel is closed"); + + expected_2.expires_in = 1; + expected_2.current_height = 2477663; + 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 update = subscriber_1 + .recv_timeout(Duration::from_secs(120)) + .await + .expect("timeout waiting for invoice update") + .expect("subscription channel is closed"); + + expected_1.confirmations = Some(2); + expected_1.is_confirmed = true; + expected_1.expires_in = 0; + expected_1.current_height = 2477664; + expected_1.assert_eq(&update); + + let update = subscriber_2 + .recv_timeout(Duration::from_secs(120)) + .await + .expect("timeout waiting for invoice update") + .expect("subscription channel is closed"); + + expected_2.expires_in = 0; + expected_2.is_expired = true; + expected_2.current_height = 2477664; + expected_2.assert_eq(&update); + + assert!(txpool_hashes_mock.hits() > 0); + assert!(height_mock.hits() > 0); } #[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 set_initial_height(store: S) +#[tokio::test] +async fn set_initial_height(store: S) where S: Storage + 'static, { // Setup. init_logger(); - let mock_daemon = MockDaemon::new_mock_daemon(); - let rt = Runtime::new().expect("failed to create tokio runtime"); + let mock_daemon = MockDaemon::new_mock_daemon().await; // Create payment gateway pointing at temp directory and mock daemon. let payment_gateway_with_height = PaymentGatewayBuilder::new( @@ -714,76 +701,74 @@ where let _height_mock = mock_daemon.mock_daemon_height(2477664); // Run it. - rt.block_on(async { - payment_gateway_with_height - .run() - .await - .expect("failed to run payment gateway"); - payment_gateway - .run() - .await - .expect("failed to run payment gateway"); - - // Add the invoice. - let invoice_id = payment_gateway_with_height - .new_invoice(70000000, 2, 7, "invoice 1".to_string()) - .expect("failed to add new invoice to payment gateway for tracking"); - let mut subscriber_1 = payment_gateway_with_height - .subscribe(invoice_id) - .expect("invoice does not exist"); - - // Get initial update. + payment_gateway_with_height + .run() + .await + .expect("failed to run payment gateway"); + payment_gateway + .run() + .await + .expect("failed to run payment gateway"); + + // Add the invoice. + let invoice_id = payment_gateway_with_height + .new_invoice(70000000, 2, 7, "invoice 1".to_string()) + .expect("failed to add new invoice to payment gateway for tracking"); + let mut subscriber_1 = payment_gateway_with_height + .subscribe(invoice_id) + .expect("invoice does not exist"); + + // Get initial update. + let update = subscriber_1 + .recv_timeout(Duration::from_secs(120)) + .await + .expect("timeout waiting for invoice update") + .expect("subscription channel is closed"); + + let mut expected_1 = MockInvoice::new( + Some(update.address().to_string()), + SubIndex::new(1, 97), + 2477664, + 70000000, + 2, + 7, + "invoice 1".to_string(), + ); + expected_1.current_height = 2477658; + expected_1.expiration_height = 2477671; + + // 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()) + .expect("failed to add new invoice to payment gateway for tracking"); + let mut subscriber_2 = payment_gateway + .subscribe(invoice_id) + .expect("invoice does not exist"); + + // Get initial update. + let update = subscriber_2 + .recv_timeout(Duration::from_secs(120)) + .await + .expect("timeout waiting for invoice update") + .expect("subscription channel is closed"); + + let mut expected_2 = expected_1.clone(); + expected_2.current_height = 2477664; + 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_millis(5000)) - .await - .expect("timeout waiting for invoice update") - .expect("subscription channel is closed"); - - let mut expected_1 = MockInvoice::new( - Some(update.address().to_string()), - SubIndex::new(1, 97), - 2477664, - 70000000, - 2, - 7, - "invoice 1".to_string(), - ); - expected_1.current_height = 2477658; - expected_1.expiration_height = 2477671; - - // 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()) - .expect("failed to add new invoice to payment gateway for tracking"); - let mut subscriber_2 = payment_gateway - .subscribe(invoice_id) - .expect("invoice does not exist"); - - // Get initial update. - let update = subscriber_2 - .recv_timeout(Duration::from_millis(5000)) + .recv_timeout(Duration::from_secs(120)) .await .expect("timeout waiting for invoice update") .expect("subscription channel is closed"); - - let mut expected_2 = expected_1.clone(); - expected_2.current_height = 2477664; - 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_millis(5000)) - .await - .expect("timeout waiting for invoice update") - .expect("subscription channel is closed"); - assert_eq!(update.current_height(), height); - assert_eq!(update.amount_paid(), 0); - } - }); + assert_eq!(update.current_height(), height); + assert_eq!(update.amount_paid(), 0); + } } diff --git a/library/tests/integration_tests/scanning_thread_management.rs b/library/tests/integration_tests/scanning_thread_management.rs index c9201d0..e4de78e 100644 --- a/library/tests/integration_tests/scanning_thread_management.rs +++ b/library/tests/integration_tests/scanning_thread_management.rs @@ -1,17 +1,15 @@ use acceptxmr::{ storage::stores::Sled, AcceptXmrError, PaymentGatewayBuilder, PaymentGatewayStatus, }; -use tokio::runtime::Runtime; use crate::common::{init_logger, new_temp_dir, MockDaemon, PRIMARY_ADDRESS, PRIVATE_VIEW_KEY}; -#[test] -fn run_payment_gateway() { +#[tokio::test] +async fn run_payment_gateway() { // Setup. init_logger(); let temp_dir = new_temp_dir(); - let mock_daemon = MockDaemon::new_mock_daemon(); - let rt = Runtime::new().expect("failed to create tokio runtime"); + let mock_daemon = MockDaemon::new_mock_daemon().await; let store = Sled::new(&temp_dir, "invoices", "output keys", "height") .expect("failed to create sled storage layer."); @@ -27,21 +25,18 @@ fn run_payment_gateway() { .expect("failed to build payment gateway"); // Run it. - rt.block_on(async { - payment_gateway - .run() - .await - .expect("failed to run payment gateway"); - }) + payment_gateway + .run() + .await + .expect("failed to run payment gateway"); } -#[test] -fn cannot_run_payment_gateway_twice() { +#[tokio::test] +async fn cannot_run_payment_gateway_twice() { // Setup. init_logger(); let temp_dir = new_temp_dir(); - let mock_daemon = MockDaemon::new_mock_daemon(); - let rt = Runtime::new().expect("failed to create tokio runtime"); + let mock_daemon = MockDaemon::new_mock_daemon().await; let store = Sled::new(&temp_dir, "invoices", "output keys", "height") .expect("failed to create sled storage layer."); @@ -57,29 +52,26 @@ fn cannot_run_payment_gateway_twice() { .expect("failed to build payment gateway"); // Run it. - rt.block_on(async { - payment_gateway - .run() - .await - .expect("failed to run payment gateway"); - - assert!( - matches!( - payment_gateway.run().await, - Err(AcceptXmrError::AlreadyRunning) - ), - "payment gateway was run twice" - ); - }) + payment_gateway + .run() + .await + .expect("failed to run payment gateway"); + + assert!( + matches!( + payment_gateway.run().await, + Err(AcceptXmrError::AlreadyRunning) + ), + "payment gateway was run twice" + ); } -#[test] -fn stop_payment_gateway() { +#[tokio::test] +async fn stop_payment_gateway() { // Setup. init_logger(); let temp_dir = new_temp_dir(); - let mock_daemon = MockDaemon::new_mock_daemon(); - let rt = Runtime::new().expect("failed to create tokio runtime"); + let mock_daemon = MockDaemon::new_mock_daemon().await; let store = Sled::new(&temp_dir, "invoices", "output keys", "height") .expect("failed to create sled storage layer."); @@ -95,17 +87,15 @@ fn stop_payment_gateway() { .expect("failed to build payment gateway"); // Run it. - rt.block_on(async { - payment_gateway - .run() - .await - .expect("failed to run payment gateway"); - - assert!(matches!( - payment_gateway.status(), - PaymentGatewayStatus::Running, - )); - - assert!(payment_gateway.stop().is_ok()); - }) + payment_gateway + .run() + .await + .expect("failed to run payment gateway"); + + assert!(matches!( + payment_gateway.status(), + PaymentGatewayStatus::Running, + )); + + assert!(payment_gateway.stop().is_ok()); } diff --git a/server/Cargo.toml b/server/Cargo.toml index 3c3e0ac..12899c1 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.68" +rust-version = "1.70" license = "MIT OR Apache-2.0" description = "A monero payment gateway." repository = "https://github.com/busyboredom/acceptxmr" @@ -19,13 +19,35 @@ path = "src/main.rs" acceptxmr = { path = "../library", features = ["serde", "sqlite"] } actix = "0.13" actix-files = "0.6" -actix-session = { version = "0.7", features = ["cookie-session"] } -actix-web = "4" +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" -log = "0.4" +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" + +[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"] } diff --git a/server/README.md b/server/README.md index 1675cf1..f306eda 100644 --- a/server/README.md +++ b/server/README.md @@ -12,56 +12,95 @@ please see the [`AcceptXMR`](../library/) library instead. ### Build and Run from Source 1. Install rust: https://www.rust-lang.org/tools/install 2. Clone this repository: - ``` - git clone https://github.com/busyboredom/acceptxmr.git + ```bash + $ git clone https://github.com/busyboredom/acceptxmr.git + $ cd acceptxmr && ``` 3. Run it: - ``` - cargo run --release + ```bash + $ cargo run --release ``` ### Run with Docker 1. Install Docker: https://docs.docker.com/get-docker/ 2. Pull the latest AcceptXMR image: + ```bash + $ docker pull busyboredom/acceptxmr:latest ``` - docker pull busyboredom/acceptxmr:latest - ``` -3. Run it (setting port and database directory to whatever you desire): - ``` - docker run -d \ +3. Run it (setting ports and paths to whatever you desire): + ```bash + $ docker run -d \ --name acceptxmr \ --restart=always \ - -p :8080 \ + -p :8080 \ + -p :8081 \ --mount type=bind,source=,target=/AcceptXMR_DB \ + --mount type=bind,source=,target=/cert \ + --mount type=bind,source=,target=/acceptxmr.yaml \ + --env-file \ busyboredom/acceptxmr:latest ``` -4. That's it, you are now serving a payment gateway at `localhost:`. +Note that the `acceptxmr.yaml` configuration file (described +[here](#Configuration)) applies directly to the bare `AcceptXMR-Server` service +running inside docker. The command in step (3) above will need to be adapted +appropriately if ports or paths in `acceptxmr.yaml` are changed. + +Click [here](../docker.sh) for an example command with paths and ports filled +out. ### Run with Docker Compose 1. Install Docker: https://docs.docker.com/get-docker/ 2. Create a file called `docker-compose.yml` with the following contents, - setting port to whatever you desire: - ``` + setting ports and paths to whatever you desire: + ```yaml name: acceptxmr services: server: image: busyboredom/acceptxmr:latest ports: - - ":8080" + - ":8080" + - ":8081" volumes: - db:/AcceptXMR_DB + - :/acceptxmr.yaml + - :/cert + env_file: restart: always volumes: db: ``` 3. Run it: + ```bash + $ docker compose up -d ``` - docker compose up -d - ``` -4. That's it, you are now serving a payment gateway at `localhost:`. + +Note that the `acceptxmr.yaml` configuration file (described +[here](#Configuration)) applies directly to the bare `AcceptXMR-Server` service +running inside docker. The file in step (2) above will need to be adapted +appropriately if ports or paths in `acceptxmr.yaml` are changed. + +Click [here](../docker-compose.yml) for an example `docker-compose.yml` with +paths and ports filled out. ### Configuration -TODO +`AcceptXMR-Server` uses a configuration file named `acceptxmr.yaml` for most +configuration, and uses environment variables for secrets. + +The location of `acceptxmr.yaml` is expected to be the current directory by +default. An alternative location can be specified by passing the `--config-file +` command line argument, or by setting the `CONFIG_FILE` +environment variable. + +Please click [here](../acceptxmr.yaml) for an example of what can be configured +in `acceptxmr.yaml`. + +Secrets should be configured via environment variable. Your priviate viewkey can +be set using the `PRIVATE_VIEWKEY` environment variable, and bearer +authentication tokens can be set using the `INTERNAL_API_TOKEN` and +`EXTERNAL_API_TOKEN` variables if desired. + +Please click [here](../.env) for an example of how to configure secrets in a +`.env` file. ### API TODO diff --git a/server/acceptxmr.yaml b/server/acceptxmr.yaml new file mode 100644 index 0000000..800272b --- /dev/null +++ b/server/acceptxmr.yaml @@ -0,0 +1,23 @@ +external-api: + port: 8080 + ipv4: 127.0.0.1 + ipv6: ::1 + static_dir: static/ +internal-api: + port: 8081 + ipv4: 127.0.0.1 + ipv6: ::1 + tls: + cert: tests/testdata/cert/certificate.pem + key: tests/testdata/cert/privatekey.pem + static_dir: static/ +wallet: + primary-address: 4613YiHLM6JMH4zejMB2zJY5TwQCxL8p65ufw8kBP5yxX9itmuGLqp1dS4tkVoTxjyH3aYhYNrtGHbQzJQP5bFus3KHVdmf + account-index: 0 + restore-height: null +daemon: + url: https://xmr-node.cakewallet.com:18081/ +database: + path: AcceptXMR_DB/ +logging: + verbosity: DEBUG diff --git a/server/src/config/daemon.rs b/server/src/config/daemon.rs new file mode 100644 index 0000000..6071b67 --- /dev/null +++ b/server/src/config/daemon.rs @@ -0,0 +1,148 @@ +use std::{env, env::VarError}; + +use actix_web::http::Uri; +use anyhow::Result; +use log::warn; +use secrecy::{ExposeSecret, Secret}; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DisplayFromStr}; + +#[serde_as] +#[derive(Deserialize, PartialEq, Debug, Serialize)] +pub struct DaemonConfig { + /// URL of monero daemon. + #[serde_as(as = "DisplayFromStr")] + pub url: Uri, + /// Monero daemon login credentials, if applicable. + #[serde(skip_serializing_if = "Option::is_none")] + pub login: Option, +} + +impl DaemonConfig { + pub(super) fn apply_env_overrides(mut self) -> Result { + match env::var("DAEMON_PASSWORD") { + Ok(password) => { + if let Some(login) = self.login.as_mut() { + login.password = Some(Secret::new(password)); + } else { + warn!("Environment variable DAEMON_PASSWORD was set, but no username was found in the configuration file"); + } + } + Err(VarError::NotPresent) => {} + Err(e) => return Err(e)?, + } + Ok(self) + } + + pub(super) fn validate(&self) { + if let Some(login) = self.login.as_ref() { + assert!( + login.password.is_some(), + "daemon login exists in config, but a password was not set. For best security, set it using the DAEMON_PASSWORD environment variable." + ); + } + } +} + +impl Default for DaemonConfig { + fn default() -> Self { + Self { + url: Uri::from_static("https://xmr-node.cakewallet.com:18081"), + login: None, + } + } +} + +/// Username and password of monero daemon. +#[derive(Deserialize, Debug, Serialize)] +pub struct DaemonLoginConfig { + pub username: String, + /// Daemon login password. For best security, this should be set via the + /// `DAEMON_PASSWORD` environment variable. + #[serde(skip_serializing)] + pub password: Option>, +} + +impl PartialEq for DaemonLoginConfig { + fn eq(&self, other: &Self) -> bool { + let usernames_match = self.username == other.username; + let passwords_match = match (self.password.as_ref(), other.password.as_ref()) { + (Some(password), Some(other_password)) => { + password.expose_secret() == other_password.expose_secret() + } + (None, None) => true, + _ => false, + }; + + usernames_match && passwords_match + } +} + +#[cfg(test)] +mod test { + use std::{env, panic::catch_unwind}; + + use actix_web::http::Uri; + use secrecy::{ExposeSecret, Secret}; + use test_case::test_case; + + use super::{DaemonConfig, DaemonLoginConfig}; + + #[test_case(None => Some("supersecretpassword".to_string()); "env var password only")] + #[test_case(Some("configpass") => Some("supersecretpassword".to_string()); "password override")] + fn apply_env_overrides(config_pass: Option<&str>) -> Option { + let mut config = DaemonConfig { + login: Some(DaemonLoginConfig { + username: "jsmith".to_string(), + password: config_pass.map(|pass| Secret::new(pass.to_string())), + }), + ..Default::default() + }; + + env::set_var("DAEMON_PASSWORD", "supersecretpassword"); + + config = config.apply_env_overrides().unwrap(); + config + .login + .unwrap() + .password + .map(|pass| pass.expose_secret().clone()) + } + + #[test_case(&DaemonConfig::default() => true; "default")] + #[test_case( + &DaemonConfig { + url: Uri::from_static("http://example.com"), + login: Some(DaemonLoginConfig {username: "jsmith".to_string(), password: None}) + } => 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()))}) + } + => true; "with password" + )] + fn validate(config: &DaemonConfig) -> bool { + catch_unwind(|| config.validate()).is_ok() + } + + #[test_case("jsmith", None, "jsmith", None => true; "match no password")] + #[test_case("jsmith", Some("pass"), "jsmith", Some("pass") => true; "match with password")] + #[test_case("jsmith", None, "jgalt", None => false; "mismatch no password")] + #[test_case("jsmith", Some("pass"), "jsmith", Some("pass2") => false; "mismatch with password")] + #[test_case("jsmith", Some("pass"), "jsmith", None => false; "mismatch one password")] + fn eq(user1: &str, pass1: Option<&str>, user2: &str, pass2: Option<&str>) -> bool { + let login1 = DaemonLoginConfig { + username: user1.to_string(), + password: pass1.map(|pass| Secret::new(pass.to_string())), + }; + + let login2 = DaemonLoginConfig { + username: user2.to_string(), + password: pass2.map(|pass| Secret::new(pass.to_string())), + }; + + login1 == login2 + } +} diff --git a/server/src/config/database.rs b/server/src/config/database.rs new file mode 100644 index 0000000..cf8f29b --- /dev/null +++ b/server/src/config/database.rs @@ -0,0 +1,19 @@ +use std::{path::PathBuf, str::FromStr}; + +use serde::{Deserialize, Serialize}; + +/// Default invoice storage database directory. +const DEFAULT_DB_DIR: &str = "AcceptXMR_DB/"; + +#[derive(Clone, Deserialize, PartialEq, Eq, Debug, Serialize)] +pub struct DatabaseConfig { + pub path: PathBuf, +} + +impl Default for DatabaseConfig { + fn default() -> Self { + Self { + path: PathBuf::from_str(DEFAULT_DB_DIR).unwrap(), + } + } +} diff --git a/server/src/config/logging.rs b/server/src/config/logging.rs new file mode 100644 index 0000000..29f7676 --- /dev/null +++ b/server/src/config/logging.rs @@ -0,0 +1,15 @@ +use log::LevelFilter; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, PartialEq, Eq, Clone, Copy, Debug, Serialize)] +pub struct LoggingConfig { + pub verbosity: LevelFilter, +} + +impl Default for LoggingConfig { + fn default() -> Self { + Self { + verbosity: LevelFilter::Info, + } + } +} diff --git a/server/src/config/mod.rs b/server/src/config/mod.rs new file mode 100644 index 0000000..b90838e --- /dev/null +++ b/server/src/config/mod.rs @@ -0,0 +1,283 @@ +mod daemon; +mod database; +mod logging; +mod server; +mod wallet; + +use std::{ + env::{self, VarError}, + fs::File, + io::{ErrorKind as IoErrorKind, Write}, + path::PathBuf, +}; + +use anyhow::Result; +use clap::{Arg, ArgAction, Command}; +pub use daemon::{DaemonConfig, DaemonLoginConfig}; +pub use database::DatabaseConfig; +use dotenv::dotenv; +use log::info; +pub use logging::LoggingConfig; +use secrecy::Secret; +use serde::{Deserialize, Serialize}; +pub use server::{ServerConfig, TlsConfig}; +pub use wallet::WalletConfig; + +/// AcceptXMR-Server configuration. +#[derive(Deserialize, PartialEq, Debug, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct Config { + /// Config for the client-facing API. + pub external_api: ServerConfig, + /// Config for the internal API. + pub internal_api: ServerConfig, + /// Monero wallet configuration. + pub wallet: WalletConfig, + /// Monero daemon configuration. + pub daemon: DaemonConfig, + /// Database configuration. + pub database: DatabaseConfig, + /// Logging configuration. + pub logging: LoggingConfig, +} + +impl Config { + /// Get config file path from CLI argument, env variable, or default (in + /// that order). + fn get_path() -> PathBuf { + let cli_matches = Command::new("AcceptXMR-Server") + .arg( + Arg::new("config-file") + .short('f') + .long("config-file") + .action(ArgAction::Set) + .value_name("FILE") + .env("CONFIG_FILE") + .default_value("acceptxmr.yaml") + .help("Specifies the config file to use. Defaults to ./acceptxmr.yaml"), + ) + .get_matches(); + + // This `unwrap` is safe because args with a default never return `None`. + PathBuf::from(cli_matches.get_one::("config-file").unwrap()) + } + + /// 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) { + Ok(f) => f, + Err(e) if e.kind() == IoErrorKind::NotFound => { + info!( + "Config file {} not found. Creating it from defaults.", + config_path.display() + ); + let mut f = File::create(config_path)?; + let config = Config::default(); + f.write_all(serde_yaml::to_string(&config)?.as_bytes())?; + return Ok(config); + } + Err(e) => return Err(e)?, + }; + + Ok(serde_yaml::from_reader(config_file)?) + } + + fn apply_env_overrides(mut self) -> Result { + // Read from dotenv file if real environment variables are not set. + dotenv().ok(); + + self.wallet = self.wallet.apply_env_overrides()?; + self.daemon = self.daemon.apply_env_overrides()?; + + match env::var("INTERNAL_API_TOKEN") { + Ok(token) => { + self.internal_api.token = Some(Secret::new(token)); + } + Err(VarError::NotPresent) => {} + Err(e) => return Err(e)?, + } + + match env::var("EXTERNAL_API_TOKEN") { + Ok(token) => { + self.external_api.token = Some(Secret::new(token)); + } + Err(VarError::NotPresent) => {} + Err(e) => return Err(e)?, + } + + Ok(self) + } + + /// Validates configuration, panicking if it is invalid. + pub fn validate(&self) { + self.wallet.validate(); + self.daemon.validate(); + self.internal_api.validate(); + self.external_api.validate(); + } + + pub fn read() -> Result { + Self::from_file()?.apply_env_overrides() + } +} + +impl Default for Config { + fn default() -> Self { + Self { + external_api: ServerConfig::default(), + internal_api: ServerConfig { + port: 8081, + // Default to self-signed certs if none are provided. + tls: Some(TlsConfig { + cert: PathBuf::from("./cert/certificate.pem"), + key: PathBuf::from("./cert/privatekey.pem"), + }), + ..Default::default() + }, + wallet: WalletConfig::default(), + daemon: DaemonConfig::default(), + database: DatabaseConfig::default(), + logging: LoggingConfig::default(), + } + } +} + +#[cfg(test)] +mod test { + use std::{ + env, + net::{Ipv4Addr, Ipv6Addr}, + panic::catch_unwind, + path::PathBuf, + str::FromStr, + }; + + use actix_web::http::Uri; + use log::LevelFilter; + use monero::{Address, PrivateKey}; + use secrecy::Secret; + + use super::{ + Config, DaemonConfig, DaemonLoginConfig, LoggingConfig, ServerConfig, TlsConfig, + WalletConfig, + }; + use crate::config::DatabaseConfig; + + #[test] + fn default() { + let config = Config::default(); + + let expected_config = Config { + external_api: ServerConfig { + port: 8080, + ipv4: Ipv4Addr::LOCALHOST, + ipv6: Some(Ipv6Addr::LOCALHOST), + token: None, + tls: None, + static_dir: PathBuf::from("./server/static/"), + }, + internal_api: ServerConfig { + port: 8081, + ipv4: Ipv4Addr::LOCALHOST, + ipv6: Some(Ipv6Addr::LOCALHOST), + token: None, + tls: Some(TlsConfig { + cert: PathBuf::from("./cert/certificate.pem"), + key: PathBuf::from("./cert/privatekey.pem"), + }), + static_dir: PathBuf::from("./server/static/"), + }, + wallet: WalletConfig { + primary_address: None, + private_viewkey: None, + account_index: 0, + restore_height: None, + }, + daemon: DaemonConfig { + url: Uri::from_static("https://xmr-node.cakewallet.com:18081"), + login: None, + }, + database: DatabaseConfig { + path: PathBuf::from_str("AcceptXMR_DB/").unwrap(), + }, + logging: LoggingConfig { + verbosity: LevelFilter::Info, + }, + }; + + assert_eq!(config, expected_config); + } + + #[test] + fn from_yaml() { + let yaml = include_str!("../../tests/testdata/config/config_full.yaml"); + + let expected_config = expected_config(); + + let config: Config = serde_yaml::from_str(yaml).unwrap(); + assert_eq!(config, expected_config); + config.validate(); + } + + #[test] + fn from_yaml_and_env() { + let expected_config = expected_config(); + env::set_var( + "CONFIG_FILE", + "tests/testdata/config/config_no_secrets.yaml", + ); + + let config_without_secrets = Config::from_file().unwrap(); + assert_ne!(config_without_secrets, expected_config); + catch_unwind(|| config_without_secrets.validate()) + .expect_err("config without secrets should be invalid"); + + env::set_var("DAEMON_PASSWORD", "supersecretpassword"); + env::set_var( + "PRIVATE_VIEWKEY", + "ad2093a5705b9f33e6f0f0c1bc1f5f639c756cdfc168c8f2ac6127ccbdab3a03", + ); + env::set_var("INTERNAL_API_TOKEN", "supersecrettoken"); + let config = Config::read().unwrap(); + assert_eq!(config, expected_config); + config.validate(); + } + + fn expected_config() -> Config { + Config { + external_api: ServerConfig::default(), + internal_api: ServerConfig { + port: 8081, + ipv4: Ipv4Addr::LOCALHOST, + ipv6: None, + token: Some(Secret::new("supersecrettoken".to_string())), + tls: Some(TlsConfig { + cert: PathBuf::from_str("/path/to/cert.pem").unwrap(), + key: PathBuf::from_str("/path/to/key.pem").unwrap(), + }), + static_dir: PathBuf::from("./server/static/"), + }, + wallet: WalletConfig { + primary_address: Some(Address::from_str("4613YiHLM6JMH4zejMB2zJY5TwQCxL8p65ufw8kBP5yxX9itmuGLqp1dS4tkVoTxjyH3aYhYNrtGHbQzJQP5bFus3KHVdmf").unwrap()), + private_viewkey: Some(Secret::new(PrivateKey::from_str("ad2093a5705b9f33e6f0f0c1bc1f5f639c756cdfc168c8f2ac6127ccbdab3a03").unwrap().to_string())), + account_index: 0, + restore_height: Some(2_947_000), + }, + daemon: DaemonConfig { + url: Uri::from_static("https://node.example.com:18081"), + login: Some(DaemonLoginConfig { + username: "pinkpanther".to_string(), + password: Some(Secret::new("supersecretpassword".to_string())), + }), + }, + database: DatabaseConfig { + path: PathBuf::from_str("server/tests/AcceptXMR_DB/").unwrap(), + }, + logging: LoggingConfig { + verbosity: LevelFilter::Debug, + }, + } + } +} diff --git a/server/src/config/server.rs b/server/src/config/server.rs new file mode 100644 index 0000000..95f0666 --- /dev/null +++ b/server/src/config/server.rs @@ -0,0 +1,107 @@ +use std::{ + net::{Ipv4Addr, Ipv6Addr}, + path::PathBuf, +}; + +use secrecy::{ExposeSecret, Secret}; +use serde::{Deserialize, Serialize}; + +// Http(s) server configuration. +#[derive(Deserialize, Debug, Serialize, Clone)] +pub struct ServerConfig { + /// Port to listen on. + pub port: u16, + /// IPv4 to listen on. + pub ipv4: Ipv4Addr, + /// IPv6 to listen on. + pub ipv6: Option, + /// Bearer auth token to require. If used, TLS should also be enabled to + /// prevent exposing the token over the wire. + /// + /// It is recommended that secrets like this be set via environment variable + /// when possible. + #[serde(skip_serializing)] + pub token: Option>, + /// TLS configuration. Should be enabled if tokens are used. + pub tls: Option, + /// Directory containing static HTML/CSS and image files. + pub static_dir: PathBuf, +} + +impl ServerConfig { + pub(super) fn validate(&self) { + assert!( + self.token.is_none() || self.tls.is_some(), + "API tokens without TLS are insecure. Please enable TLS in order to use an API token." + ); + } +} + +impl PartialEq for ServerConfig { + fn eq(&self, other: &Self) -> bool { + let ports_match = self.port == other.port; + let ipv4s_match = self.ipv4 == other.ipv4; + let ipv6s_match = self.ipv6 == other.ipv6; + let tokens_match = self.token.as_ref().map(ExposeSecret::expose_secret) + == other.token.as_ref().map(ExposeSecret::expose_secret); + let tls_matches = self.tls == other.tls; + + ports_match && ipv4s_match && ipv6s_match && tokens_match && tls_matches + } +} + +impl Default for ServerConfig { + fn default() -> Self { + Self { + port: 8080, + ipv4: Ipv4Addr::LOCALHOST, + ipv6: Some(Ipv6Addr::LOCALHOST), + token: None, + tls: None, + static_dir: PathBuf::from("./server/static/"), + } + } +} + +#[derive(Deserialize, PartialEq, Eq, Debug, Serialize, Clone)] +pub struct TlsConfig { + /// Path to TLS certificate `.pem` file. + pub cert: PathBuf, + /// Path to TLS certificate `.key` file. + pub key: PathBuf, +} + +#[cfg(test)] +mod test { + use std::{panic::catch_unwind, path::PathBuf}; + + use secrecy::Secret; + use test_case::test_case; + + use super::{ServerConfig, TlsConfig}; + + #[test_case(true, true => true; "tls and token")] + #[test_case(false, false => true; "no tls or token")] + #[test_case(true, false => true; "tls without token")] + #[test_case(false, true => false; "token without tls")] + fn validate(tls: bool, token: bool) -> bool { + let config = ServerConfig { + tls: if tls { + Some(TlsConfig { + cert: PathBuf::from("path/to/cert.pem"), + key: PathBuf::from("path/to/key.pem"), + }) + } else { + None + }, + token: if token { + Some(Secret::new("supersecrettoken".to_string())) + } else { + None + }, + ..Default::default() + }; + + catch_unwind(|| config.validate()).is_ok() + } +} diff --git a/server/src/config/wallet.rs b/server/src/config/wallet.rs new file mode 100644 index 0000000..612a9af --- /dev/null +++ b/server/src/config/wallet.rs @@ -0,0 +1,162 @@ +use std::{ + env::{self, VarError}, + str::FromStr, +}; + +use anyhow::Result; +use monero::{Address, PrivateKey}; +use secrecy::{ExposeSecret, Secret}; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Debug, Serialize, Default)] +#[serde(rename_all = "kebab-case")] +pub struct WalletConfig { + /// Monero wallet's primary address. Should begin with a `4`. + pub primary_address: Option
, + /// Monero wallet private view key. For best security, this should be set + /// via the `PRIVATE_VIEWKEY` environment variable. + #[serde(skip_serializing)] + pub private_viewkey: Option>, + /// The account index to be used. Defaults to 0. + #[serde(default)] + pub account_index: u32, + /// The restore height of the wallet. Defaults to the current blockchain + /// tip. + #[serde(default)] + pub restore_height: Option, +} + +impl WalletConfig { + pub(super) fn apply_env_overrides(mut self) -> Result { + match env::var("PRIVATE_VIEWKEY") { + Ok(key) => { + self.private_viewkey = Some(Secret::new(key)); + } + Err(VarError::NotPresent) => {} + Err(e) => return Err(e)?, + } + Ok(self) + } + + pub(super) fn validate(&self) { + assert!( + self.primary_address.is_some(), + "please configure your monero primary address. Your primary address should begin with a 4." + ); + assert!( + self.private_viewkey.is_some(), + "please configure your monero private viewkey. For best security, set it using the PRIVATE_VIEWKEY environment variable." + ); + if let Some(key) = &self.private_viewkey { + PrivateKey::from_str(key.expose_secret()).expect("invalid private view key"); + } + } +} + +impl PartialEq for WalletConfig { + fn eq(&self, other: &Self) -> bool { + let addresses_match = self.primary_address == other.primary_address; + let viewkeys_match = match ( + self.private_viewkey.as_ref(), + other.private_viewkey.as_ref(), + ) { + (Some(viewkey), Some(other_viewkey)) => { + viewkey.expose_secret() == other_viewkey.expose_secret() + } + (None, None) => true, + _ => false, + }; + let accounts_match = self.account_index == other.account_index; + let restore_heights_match = self.restore_height == other.restore_height; + + addresses_match && viewkeys_match && accounts_match && restore_heights_match + } +} + +#[cfg(test)] +#[allow(clippy::too_many_arguments)] +mod test { + use std::{env, panic::catch_unwind, str::FromStr}; + + use monero::Address; + use secrecy::{ExposeSecret, Secret}; + use test_case::test_case; + + use super::WalletConfig; + + const ADDRESS_1: &str = "4613YiHLM6JMH4zejMB2zJY5TwQCxL8p65ufw8kBP5yxX9itmuGLqp1dS4tkVoTxjyH3aYhYNrtGHbQzJQP5bFus3KHVdmf"; + const ADDRESS_2: &str = "82assiV5dy7guoxxV7vSReZTyY5rGMrWg6BsfvFqiEKRcTiDs7LGMpg5dF5gXVGUWPEXQxyt8SNYx8L8HiGAzvtBK3eJ3EY"; + const ADDRESS_INVALID: &str = "5613YiHLM6JMH4zejMB2zJY5TwQCxL8p65ufw8kBP5yxX9itmuGLqp1dS4tkVoTxjyH3aYhYNrtGHbQzJQP5bFus3KHVdmf"; + const VIEWKEY_1: &str = "ad2093a5705b9f33e6f0f0c1bc1f5f639c756cdfc168c8f2ac6127ccbdab3a03"; + const VIEWKEY_INVALID: &str = "d2093a5705b9f33e6f0f0c1bc1f5f639c756cdfc168c8f2ac6127ccbdab3a03"; + + #[test_case(None => Some(VIEWKEY_1.to_string()); "env var key only")] + #[test_case(Some("configkey") => Some(VIEWKEY_1.to_string()); "key override")] + fn apply_env_overrides(config_key: Option<&str>) -> Option { + let mut config = WalletConfig { + private_viewkey: config_key.map(|key| Secret::new(key.to_string())), + ..Default::default() + }; + + env::set_var("PRIVATE_VIEWKEY", VIEWKEY_1); + + config = config.apply_env_overrides().unwrap(); + config + .private_viewkey + .map(|key| key.expose_secret().clone()) + } + + #[test_case(None, None => false; "not configured")] + #[test_case(None, Some(VIEWKEY_1) => false; "no address")] + #[test_case(Some(ADDRESS_1), None => false; "no key")] + #[test_case(Some(ADDRESS_1), Some(VIEWKEY_1)=> true; "all configured")] + #[test_case(Some(ADDRESS_INVALID), Some(VIEWKEY_1)=> false; "invalid address")] + #[test_case(Some(ADDRESS_1), Some(VIEWKEY_INVALID)=> false; "invalid key")] + fn validate(address: Option<&str>, viewkey: Option<&str>) -> bool { + catch_unwind(|| { + let config = WalletConfig { + primary_address: address.map(|addr| Address::from_str(addr).unwrap()), + private_viewkey: viewkey.map(|key| Secret::new(key.to_string())), + account_index: 123, + restore_height: Some(12345), + }; + config.validate(); + }) + .is_ok() + } + + #[test_case(Some(ADDRESS_1), None, 0, None, Some(ADDRESS_1), None, 0, None => true; "match no key")] + #[test_case(Some(ADDRESS_1), Some(VIEWKEY_1), 0, None, Some(ADDRESS_1), Some(VIEWKEY_1), 0, None => true; "match with key")] + #[test_case(Some(ADDRESS_1), None, 0, None, Some(ADDRESS_2), None, 0, None => false; "mismatch no key")] + #[test_case(Some(ADDRESS_1), Some(VIEWKEY_1), 0, None, Some(ADDRESS_1), Some(VIEWKEY_INVALID), 0, None => false; "mismatch with key")] + #[test_case(Some(ADDRESS_1), Some(VIEWKEY_1), 0, None, Some(ADDRESS_1), None, 0, None => false; "mismatch one key")] + #[test_case(None, Some(VIEWKEY_1), 0, None, None, Some(VIEWKEY_1), 0, None => true; "match no address")] + #[test_case(Some(ADDRESS_1), Some(VIEWKEY_1), 0, None, Some(ADDRESS_1), Some(VIEWKEY_1), 123, None => false; "mismatch account index")] + #[test_case(Some(ADDRESS_1), Some(VIEWKEY_1), 0, Some(123), Some(ADDRESS_1), Some(VIEWKEY_1), 0, Some(124) => false; "mismatch restore height")] + fn eq( + address1: Option<&str>, + viewkey1: Option<&str>, + account_index1: u32, + restore_height1: Option, + address2: Option<&str>, + viewkey2: Option<&str>, + account_index2: u32, + restore_height2: Option, + ) -> bool { + let wallet1 = WalletConfig { + primary_address: address1.map(|addr| Address::from_str(addr).unwrap()), + private_viewkey: viewkey1.map(|key| Secret::new(key.to_string())), + account_index: account_index1, + restore_height: restore_height1, + }; + + let wallet2 = WalletConfig { + primary_address: address2.map(|addr| Address::from_str(addr).unwrap()), + private_viewkey: viewkey2.map(|key| Secret::new(key.to_string())), + account_index: account_index2, + restore_height: restore_height2, + }; + + wallet1 == wallet2 + } +} diff --git a/server/src/lib.rs b/server/src/lib.rs new file mode 100644 index 0000000..32082fe --- /dev/null +++ b/server/src/lib.rs @@ -0,0 +1,170 @@ +//! # `libacceptxmr_server`: Everything needed for a standalone monero payment gateway. +//! `libacceptxmr_server` is a batteries-included monero payment gateway library +//! built around the general purpose `AcceptXMR` library. +//! +//! 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 config; +pub mod logging; +mod server; + +use acceptxmr::{storage::stores::Sqlite, PaymentGateway, PaymentGatewayBuilder}; +use futures::try_join; +use log::{debug, error, info, warn}; +use secrecy::ExposeSecret; + +use crate::{ + config::Config, + logging::{init_logger, set_verbosity}, + server::{ + api::{external, internal}, + new_server, + }, +}; + +/// Start a standalone payment gateway. +pub async fn entrypoint() { + init_logger(); + let config = load_config(); + set_verbosity(config.logging); + + let payment_gateway = build_gateway(&config); + info!("Payment gateway created."); + + let gateway_clone = spawn_gateway(payment_gateway).await; + + run_server(config, gateway_clone).await; +} + +/// Loads config. +/// +/// # Panics +/// +/// 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"); + config.validate(); + + config +} + +/// Build a payment gateway from provided config. +/// +/// # Panics +/// +/// Panics if the payment gateway could not be built. +pub 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 + .path + .canonicalize() + .expect("could not determine absolute database path") + .join("database"); + let db_path_str = db_path.to_str().expect("failed to cast DB path to string"); + + let private_view_key = config + .wallet + .private_viewkey + .as_ref() + .expect("private viewkey must be configured"); + let primary_address = config + .wallet + .primary_address + .expect("primary address must be configured"); + + let store = Sqlite::new(db_path_str, "invoices", "output keys", "height") + .expect("failed to open invoice store"); + let mut payment_gateway_builder = PaymentGatewayBuilder::new( + private_view_key.expose_secret().clone(), + primary_address.to_string(), + store, + ) + .account_index(config.wallet.account_index) + .daemon_url(config.daemon.url.to_string()); + + // Use daemon login if one was configured. + if let Some(login) = config.daemon.login.as_ref() { + payment_gateway_builder = payment_gateway_builder.daemon_login( + login.username.clone(), + login + .password + .as_ref() + .map(|pass| pass.expose_secret().clone()) + .unwrap_or_default(), + ); + } + + // Use restore height if one was configured. + if let Some(restore_height) = config.wallet.restore_height { + payment_gateway_builder = payment_gateway_builder.initial_height(restore_height); + } + + payment_gateway_builder + .build() + .expect("failed to build payment gateway") +} + +/// Run the payment gateway and spawn a thread to monitor it, returning a clone +/// of the running gateway. +/// +/// # Panics +/// +/// Panics if the payment gateway could not be run. +pub async fn spawn_gateway(payment_gateway: PaymentGateway) -> PaymentGateway { + payment_gateway + .run() + .await + .expect("failed to run payment gateway"); + info!("Payment gateway running."); + + let gateway_clone = payment_gateway.clone(); + + // Watch for invoice updates and deal with them accordingly. + std::thread::spawn(move || { + // Watch all invoice updates. + let mut subscriber = payment_gateway.subscribe_all(); + loop { + let Some(invoice) = subscriber.blocking_recv() else { + // TODO: Should this attempt to restart instead? + panic!("Blockchain scanner crashed!") + }; + // If it's confirmed or expired, we probably shouldn't bother tracking it + // anymore. + if (invoice.is_confirmed() && invoice.creation_height() < invoice.current_height()) + || invoice.is_expired() + { + debug!( + "Invoice to index {} is either confirmed or expired. Removing invoice now", + invoice.index() + ); + if let Err(e) = payment_gateway.remove_invoice(invoice.id()) { + error!("Failed to remove fully confirmed invoice: {}", e); + }; + } + } + }); + + gateway_clone +} + +/// 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(); +} diff --git a/server/src/logging.rs b/server/src/logging.rs new file mode 100644 index 0000000..f87be93 --- /dev/null +++ b/server/src/logging.rs @@ -0,0 +1,25 @@ +//! Logging utilities for `AcceptXMR` Server. + +use log::LevelFilter; + +use crate::config::LoggingConfig; + +/// Initialize the logging implementation. Defaults to `Trace` verbosity for +/// `AcceptXMR` and `Warn` for dependencies. +pub fn init_logger() { + env_logger::builder() + .filter_level(LevelFilter::Warn) + .filter_module("acceptxmr", LevelFilter::Trace) + .filter_module("acceptxmr_server", LevelFilter::Trace) + .init(); +} + +/// Set verbosity to one of: +/// * Trace +/// * Debug +/// * Info +/// * Error +/// * Warn +pub fn set_verbosity(config: LoggingConfig) { + log::set_max_level(config.verbosity); +} diff --git a/server/src/main.rs b/server/src/main.rs index f611ab3..db472c2 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -10,328 +10,9 @@ #![warn(clippy::cargo)] #![allow(clippy::module_name_repetitions)] -use std::{ - env, - future::Future, - path::PathBuf, - pin::Pin, - task::Poll, - time::{Duration, Instant}, -}; - -use acceptxmr::{ - storage::stores::Sqlite, Invoice, InvoiceId, PaymentGateway, PaymentGatewayBuilder, Subscriber, -}; -use actix::{prelude::Stream, Actor, ActorContext, AsyncContext, StreamHandler}; -use actix_files::Files; -use actix_session::{ - config::CookieContentSecurity, storage::CookieSessionStore, Session, SessionMiddleware, -}; -use actix_web::{ - cookie, get, - http::header::{CacheControl, CacheDirective}, - post, web, - web::Data, - App, HttpRequest, HttpResponse, HttpServer, -}; -use actix_web_actors::ws; -use bytestring::ByteString; -use log::{debug, error, info, warn, LevelFilter}; -use rand::{thread_rng, Rng}; -use serde::Deserialize; -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); -/// Length in bytes of secure session key for cookies. -const SESSION_KEY_LEN: usize = 64; -/// Default invoice storage database directory. -const DEFAULT_DB_DIR: &str = "AcceptXMR_DB/"; +use acceptxmr_server::entrypoint; #[actix_web::main] -async fn main() -> std::io::Result<()> { - env_logger::builder() - .filter_level(LevelFilter::Warn) - .filter_module("acceptxmr", log::LevelFilter::Debug) - .filter_module("acceptxmr-server", log::LevelFilter::Trace) - .init(); - - let mut db_dir = PathBuf::from(DEFAULT_DB_DIR.to_string()); - - // If not running in docker, try reading DB directory from environment. - if env::var("DOCKER") != Ok("true".to_string()) { - db_dir = PathBuf::from(env::var("DB_DIR").unwrap_or(DEFAULT_DB_DIR.to_string())); - }; - - std::fs::create_dir_all(&db_dir).expect("failed to create DB dir"); - let db_path = db_dir - .canonicalize() - .expect("could not determine absolute database path") - .join("database"); - let db_path_str = db_path.to_str().expect("failed to cast DB path to string"); - - // The private view key should be stored securely outside of the git repository. - // It is hardcoded here for demonstration purposes only. - let private_view_key = "ad2093a5705b9f33e6f0f0c1bc1f5f639c756cdfc168c8f2ac6127ccbdab3a03"; - // No need to keep the primary address secret. - let primary_address = "4613YiHLM6JMH4zejMB2zJY5TwQCxL8p65ufw8kBP5yxX9itmuGLqp1dS4tkVoTxjyH3aYhYNrtGHbQzJQP5bFus3KHVdmf"; - - let invoice_store = Sqlite::new(db_path_str, "invoices", "output keys", "height") - .expect("failed to open invoice store"); - let payment_gateway = PaymentGatewayBuilder::new( - private_view_key.to_string(), - primary_address.to_string(), - invoice_store, - ) - .daemon_url("http://xmr-node.cakewallet.com:18081".to_string()) - .build() - .expect("failed to build payment gateway"); - info!("Payment gateway created."); - - payment_gateway - .run() - .await - .expect("failed to run payment gateway"); - info!("Payment gateway running."); - - // Watch for invoice updates and deal with them accordingly. - let gateway_copy = payment_gateway.clone(); - std::thread::spawn(move || { - // Watch all invoice updates. - let mut subscriber = gateway_copy.subscribe_all(); - loop { - let Some(invoice) = subscriber.blocking_recv() else { - panic!("Blockchain scanner crashed!") - }; - // If it's confirmed or expired, we probably shouldn't bother tracking it - // anymore. - if (invoice.is_confirmed() && invoice.creation_height() < invoice.current_height()) - || invoice.is_expired() - { - debug!( - "Invoice to index {} is either confirmed or expired. Removing invoice now", - invoice.index() - ); - if let Err(e) = gateway_copy.remove_invoice(invoice.id()) { - error!("Failed to remove fully confirmed invoice: {}", e); - }; - } - } - }); - - // Create secure session key for cookies. - let mut key_arr = [0u8; SESSION_KEY_LEN]; - thread_rng().fill(&mut key_arr[..]); - let session_key = cookie::Key::generate(); - - // Run the demo webpage. - let shared_payment_gateway = Data::new(payment_gateway); - HttpServer::new(move || { - App::new() - .wrap( - SessionMiddleware::builder(CookieSessionStore::default(), session_key.clone()) - .cookie_name("acceptxmr_session".to_string()) - .cookie_secure(false) - .cookie_content_security(CookieContentSecurity::Private) - .build(), - ) - .app_data(shared_payment_gateway.clone()) - .service(update) - .service(checkout) - .service(websocket) - .service(Files::new("", "./server/static").index_file("index.html")) - }) - .bind("0.0.0.0:8080")? - .run() - .await -} - -#[derive(Deserialize)] -struct CheckoutInfo { - message: String, -} - -/// 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)?; - Ok(HttpResponse::Ok() - .append_header(CacheControl(vec![CacheDirective::NoStore])) - .finish()) -} - -// 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(), - } - ))); - }; - } - Ok(HttpResponse::Gone() - .append_header(CacheControl(vec![CacheDirective::NoStore])) - .finish()) -} - -/// WebSocket rout. -#[allow(clippy::unused_async)] -#[get("/ws/")] -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()); - }; - 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) -} - -/// Define websocket HTTP actor -struct WebSocket { - last_heartbeat: Instant, - invoice_subscriber: Option, -} - -impl WebSocket { - 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) - } +async fn main() { + entrypoint().await; } diff --git a/server/src/server/api/external.rs b/server/src/server/api/external.rs new file mode 100644 index 0000000..4df9cc8 --- /dev/null +++ b/server/src/server/api/external.rs @@ -0,0 +1,67 @@ +use acceptxmr::{storage::stores::Sqlite, InvoiceId, PaymentGateway}; +use actix_session::Session; +use actix_web::{ + get, + http::header::{CacheControl, CacheDirective}, + web, HttpRequest, HttpResponse, +}; +use actix_web_actors::ws; +use serde_json::json; + +use crate::server::WebSocket; + +pub fn external(service_config: &mut web::ServiceConfig) { + service_config.service(update).service(websocket); +} + +// 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(), + } + ))); + }; + } + Ok(HttpResponse::Gone() + .append_header(CacheControl(vec![CacheDirective::NoStore])) + .finish()) +} + +/// WebSocket rout. +#[allow(clippy::unused_async)] +#[get("/ws/")] +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()); + }; + 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) +} diff --git a/server/src/server/api/internal.rs b/server/src/server/api/internal.rs new file mode 100644 index 0000000..fd74296 --- /dev/null +++ b/server/src/server/api/internal.rs @@ -0,0 +1,35 @@ +use acceptxmr::{storage::stores::Sqlite, PaymentGateway}; +use actix_session::Session; +use actix_web::{ + http::header::{CacheControl, CacheDirective}, + post, web, HttpResponse, +}; +use log::debug; +use serde::Deserialize; + +pub fn internal(cfg: &mut web::ServiceConfig) { + cfg.service(checkout); +} + +#[derive(Deserialize)] +struct CheckoutInfo { + message: String, +} + +/// 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()) +} diff --git a/server/src/server/api/mod.rs b/server/src/server/api/mod.rs new file mode 100644 index 0000000..41902f0 --- /dev/null +++ b/server/src/server/api/mod.rs @@ -0,0 +1,5 @@ +mod external; +mod internal; + +pub use external::external; +pub use internal::internal; diff --git a/server/src/server/auth.rs b/server/src/server/auth.rs new file mode 100644 index 0000000..89537d4 --- /dev/null +++ b/server/src/server/auth.rs @@ -0,0 +1,34 @@ +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 crate::config::ServerConfig; + +#[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(); + + debug!("Authentication denied. Bearer auth token mismatch."); + return Err((AuthenticationError::from(bearer_config).into(), req)); + } + } else { + trace!("Bearer auth token not set. Not enforcing bearer auth."); + } + } 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 new file mode 100644 index 0000000..122915f --- /dev/null +++ b/server/src/server/mod.rs @@ -0,0 +1,73 @@ +pub mod api; +mod auth; +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 actix_web_httpauth::middleware::HttpAuthentication; +use auth::bearer_auth_validator; +use log::{debug, info}; +use tls::prepare_tls; +use websocket::WebSocket; + +use super::config::ServerConfig; + +pub fn new_server( + server_config: &ServerConfig, + service: F, + payment_gateway: PaymentGateway, +) -> std::io::Result +where + F: FnOnce(&mut ServiceConfig) + Copy + Send + 'static, +{ + 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()); + } + server_builder.bind_rustls_021((server_config.ipv4, server_config.port), rustls_config)? + } 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))? + }; + debug!("Bound to: {:?}", server_builder.addrs()); + Ok(server_builder.run()) +} diff --git a/server/src/server/tls.rs b/server/src/server/tls.rs new file mode 100644 index 0000000..435fad4 --- /dev/null +++ b/server/src/server/tls.rs @@ -0,0 +1,79 @@ +use std::{ + fs::{create_dir_all, File}, + io::{BufReader, Write}, +}; + +use log::warn; +use rcgen::generate_simple_self_signed; +use rustls::{Certificate, PrivateKey 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 { + // Init server config builder with safe defaults. + let rustls_config = RustlsServerConfig::builder() + .with_safe_defaults() + .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), + (Ok(false), Ok(false)) => { + warn!("TLS certificate not found. Generating self-signed certificate instead."); + generate_tls(config); + load_tls(config) + } + (Ok(true), Ok(false)) => panic!("TLS certificate found, but private key is missing"), + (Ok(false), Ok(true)) => panic!("TLS privatekey found, but certificate is missing"), + (Err(e), _) => panic!("failed to check for TLS certificate existance: {e}"), + (_, Err(e)) => panic!("failed to check for TLS private key existance: {e}"), + }; + + // Exit if no keys could be parsed + if keys.is_empty() { + eprintln!("Could not locate PKCS 8 private keys."); + std::process::exit(1); + } + + rustls_config + .with_single_cert(cert_chain, keys.remove(0)) + .unwrap() +} + +fn load_tls(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(); + + (cert_chain, keys) +} + +fn generate_tls(config: &TlsConfig) { + let cert = generate_simple_self_signed(vec!["localhost".to_string()]).unwrap(); + if let Some(path) = config.cert.parent() { + create_dir_all(path).expect("failed to create TLS certificate directory"); + } + if let Some(path) = config.key.parent() { + create_dir_all(path).expect("failed to create TLS private key directory"); + } + let mut cert_file = File::create(&config.cert).expect("failed to create TLS certificate"); + let mut key_file = File::create(&config.key).expect("failed to create TLS private key"); + write!(cert_file, "{}", cert.serialize_pem().unwrap()) + .expect("failed to write self-signed TLS certificate"); + write!(key_file, "{}", cert.serialize_private_key_pem()) + .expect("failed to write self-signed TLS private key"); +} diff --git a/server/src/server/websocket.rs b/server/src/server/websocket.rs new file mode 100644 index 0000000..47eead8 --- /dev/null +++ b/server/src/server/websocket.rs @@ -0,0 +1,132 @@ +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 new file mode 100644 index 0000000..cab8bd3 --- /dev/null +++ b/server/tests/common/mod.rs @@ -0,0 +1,174 @@ +use std::{sync::Arc, time::Duration}; + +use http::header::{ACCEPT, CONTENT_TYPE}; +use hyper::{ + client::connect::HttpConnector, header::AUTHORIZATION, Body, Method, Request, Response, Uri, +}; +use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; +use log::{debug, error, LevelFilter}; +use rustls::{ + client::{ServerCertVerified, ServerCertVerifier}, + ClientConfig, +}; +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"; + +#[derive(Debug, Clone)] +pub struct GatewayClient { + client: hyper::Client>, + pub url: Uri, + timeout: Duration, + pub token: Option, +} + +impl GatewayClient { + /// Returns a payment gateway client. + pub fn new( + url: Uri, + total_timeout: Duration, + connection_timeout: Duration, + token: Option, + ) -> GatewayClient { + 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_tls_config( + ClientConfig::builder() + .with_safe_defaults() + .with_custom_certificate_verifier(Arc::new(NoCertVerifier {})) + .with_no_client_auth(), + ) + .https_or_http() + .enable_http1() + .enable_http2() + .wrap_connector(hyper_connector); + let client = hyper::Client::builder().build(rustls_connector); + + GatewayClient { + client, + url, + timeout: total_timeout, + token, + } + } + + pub 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 request = match request_builder.body(Body::from(body.to_string())) { + Ok(r) => r, + Err(e) => { + response = Some(Err(e.into())); + break; + } + }; + + 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())) + } + }; + } + }) + .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, + } + + let body = r#"{"message":"This is a test message"}"#; + + self.request(body, endpoint).await + } +} + +impl Default for GatewayClient { + fn default() -> Self { + GatewayClient::new( + Uri::from_static("https://localhost:8081"), + Duration::from_secs(1), + Duration::from_millis(500), + 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(); +} + +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, + _ocsp_response: &[u8], + _now: std::time::SystemTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + + fn request_scts(&self) -> bool { + false + } +} + +#[derive(Error, Debug)] +pub enum ClientError { + #[error("HTTP request failed: {0}")] + Http(#[from] hyper::Error), + #[error("failed to build HTTP request: {0}")] + Request(#[from] hyper::http::Error), + #[error("HTTP request timed out: {0}")] + Timeout(#[from] error::Elapsed), + #[error("failed to interpret response body as json: {0}")] + InvalidJson(#[from] serde_json::Error), +} diff --git a/server/tests/integration_tests/bearer_auth.rs b/server/tests/integration_tests/bearer_auth.rs new file mode 100644 index 0000000..5ca8dd3 --- /dev/null +++ b/server/tests/integration_tests/bearer_auth.rs @@ -0,0 +1,41 @@ +use acceptxmr::{storage::stores::Sqlite, PaymentGatewayBuilder}; +use acceptxmr_server::{load_config, run_server}; +use hyper::StatusCode; +use log::{debug, info}; +use test_case::test_case; + +use crate::common::{init_logger, GatewayClient, PRIMARY_ADDRESS, PRIVATE_VIEW_KEY}; + +#[test_case(Some("supersecrettoken") => StatusCode::OK; "Correct token")] +#[test_case(Some("I am the wrong token!") => StatusCode::UNAUTHORIZED; "Wrong token")] +#[test_case(None => StatusCode::UNAUTHORIZED; "Missing token")] +#[tokio::test] +async fn bearer_auth(token: Option<&str>) -> StatusCode { + init_logger(); + + 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, + ) + .build() + .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 mut client = GatewayClient::default(); + client.token = token.map(|s| s.to_string()); + + let checkout_response = client + .checkout() + .await + .expect("failed to call `checkout` endpoint"); + debug!("Checkout response: {:?}", checkout_response); + checkout_response.status() +} diff --git a/server/tests/integration_tests/mod.rs b/server/tests/integration_tests/mod.rs new file mode 100644 index 0000000..5d46ef0 --- /dev/null +++ b/server/tests/integration_tests/mod.rs @@ -0,0 +1 @@ +mod bearer_auth; diff --git a/server/tests/main.rs b/server/tests/main.rs new file mode 100644 index 0000000..1b52936 --- /dev/null +++ b/server/tests/main.rs @@ -0,0 +1,2 @@ +mod common; +mod integration_tests; diff --git a/server/tests/testdata/cert/certificate.pem b/server/tests/testdata/cert/certificate.pem new file mode 100644 index 0000000..f8b540a --- /dev/null +++ b/server/tests/testdata/cert/certificate.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEJzCCAo+gAwIBAgIQKu5MWHrdyO4HsnfIu8alTDANBgkqhkiG9w0BAQsFADBt +MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExITAfBgNVBAsMGHJvYkBz +b21icmEueDUyLmRldiAoUm9iKTEoMCYGA1UEAwwfbWtjZXJ0IHJvYkBzb21icmEu +eDUyLmRldiAoUm9iKTAeFw0yMTEwMDYyMTMxMzNaFw0yNDAxMDYyMjMxMzNaMEwx +JzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTEhMB8GA1UE +CwwYcm9iQHNvbWJyYS54NTIuZGV2IChSb2IpMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAoI9BHaflPrNfnGKO6WmaEwhXfKKBH9sWlo4NKdP9ECZTC2Ef +ubvQzhjcJsPWIwYj1NDiAa11WfD6ayKG7YleoNynsDKnsOEBfXtFHU2IPWaESX4Q +rO8OaTXx001qdjwE3j/+K0AD43umXdnCeks3JYYlyG4/XxKa62pmpwu6KMgKbygA +MS3dIMe7WcYbKX+qPNl4xoF5xkeqlp2urO3SWPkgIYB+cDNsWRHb5vsMWw9s7Zos +W4mWAPZz0bLKw6w6imfo0rq0j5aoPJLNAyuH3/qhZIZC13tUCAxymIq0+pCeO+lZ +f0OC05dB/Hw1zSLxAxHgDzpOsaq9/NXSkIwEzwIDAQABo2QwYjAOBgNVHQ8BAf8E +BAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHwYDVR0jBBgwFoAUto/ox0MqZShm +QpViV/gjfJKrMDkwGgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMA0GCSqGSIb3 +DQEBCwUAA4IBgQCxMiND9F3xsyGlIyqIgHc+fp+wzFI5Yz9qD/02RP558qXHAj2o +6zGECzc4PeiBLh7Y7wHjcu4TTLIXnRtrdVFBUT/s58l/uoK8NGVjky74Rc4A+djt +zwcHS0snuj+FJ859Y+uS3rGKAmBAKWD22wmhB96UNRiZjG1QdJ/Or6hMZ3PVbELs +Hgv69UG1jJiL8y7cn4foBXC6Wgb10tPXNoz7TpD3B14+Pd82yergAHswCp3nj9Ip +D+9Ohko26OItO1dJYeDZWi0CurWdjP7xnEsZo2OaLIlSMiUbSyJOCMk/xWJCjuLW +BEc1VzaFwhkGZJUa1F6TOIc70geLC4wQWOaqZoLbsQfihYgRoUMZJOmjcDXJrNZz +wZofnBI+0tDsZfKjwXFyA4bzUD1I3lFY5Zy3wgQprUrZCm69uo8G4RtMWP9DmXCc +SEw6CxBVPu/l/ljYoxdqCyJTLvdQ97OlGgLv3b0DDcWqi7e0zB8NqT0aCTPm7J/M +OBWicNgMJ+1qL8M= +-----END CERTIFICATE----- diff --git a/server/tests/testdata/cert/privatekey.pem b/server/tests/testdata/cert/privatekey.pem new file mode 100644 index 0000000..e99c03c --- /dev/null +++ b/server/tests/testdata/cert/privatekey.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCgj0Edp+U+s1+c +Yo7paZoTCFd8ooEf2xaWjg0p0/0QJlMLYR+5u9DOGNwmw9YjBiPU0OIBrXVZ8Ppr +IobtiV6g3KewMqew4QF9e0UdTYg9ZoRJfhCs7w5pNfHTTWp2PATeP/4rQAPje6Zd +2cJ6SzclhiXIbj9fEprramanC7ooyApvKAAxLd0gx7tZxhspf6o82XjGgXnGR6qW +na6s7dJY+SAhgH5wM2xZEdvm+wxbD2ztmixbiZYA9nPRssrDrDqKZ+jSurSPlqg8 +ks0DK4ff+qFkhkLXe1QIDHKYirT6kJ476Vl/Q4LTl0H8fDXNIvEDEeAPOk6xqr38 +1dKQjATPAgMBAAECggEAVBfTvgmSuw1NtWW1fjDuHqvOzpt6T8n7Aa2y3UaHk67O +7fXXnPruuRMyMyd8/2kW2T7yMHi+LvZU4kn6K204X75SIanWRIEEu8kVgOx7v9Ty +0l8xsrGedaJoXwh8CyMSValkoRhtMPcxQpRsFItSfdfN8DU2AcCH3WckDrfIr9SJ +qvag8VsYeg/PH3rP3bNAh4xousaJzcvr8ifuNcN7NmoUDMoTXk3Pxhxeryj+sACS +cFxt777edShuYqL2BAziY/cTl0zcvCarX27NUS+q9exF7VYvMCuqiWHYcYkLlkH1 +UfrwPXQmdX5/CUBqt36xBsKyub5j74KoEk7shzOmkQKBgQDKTr0vc+53QNUR1mUD +7a8Pw+oWW1ddcd9SYtvzEJeNqb7s2aZsEzTRk4Pxdx3wrm8PAaPqjzJWwx1SmazU +iLt55SRFu3sPw8gTwNQj01fy2roae/ZzMP4MJRzw6vFtNPPcevLQK9JN9uKBQep+ +NU3xHYNYnT2I+X7QVJi6AsMwxwKBgQDLLA6iOwN+3aQmLlW1A4reRpIkFQ75RD92 +BtCnYQwXCqOtU4uUz3fIlmcuCI5jhqAYWG0m9IL+rxQD2SdFu9UaG1pEsMkapjUh ++mPLAm3UcoqnhKygGiiQ8iPL9zMFai3dfbBYrmBMsYgFxT7wkPuAgjWM0bvfyUqA +lwKrkykTuQKBgHdSZacdW6MerA0vRLlCcSR9Sw4QpcDJrwwqnswIFztIyQFthgjs +cxTBSusadKBGYd6Z+xIXj3s47YyQcy2Pz/OfQPuYDodH1DRCYV0YBCGK/IUuZDeg +x9Zl9WHrUKY2uzZpldlOX2X4nbPbKvFxgx0ZaSTU6Txm23MI0mOzyWh1AoGBAJYu +jvKkpMTWmUwP3BLd93yutcAuQM9I/5ADIaFYP1OY7bxlkTwC0AxaARMqB/bRwO2+ +D5FIFLymNilSD5GgcrnFlkhIVZ95VLU1HScnOIBd2thRXjlKnMnn80YGCJTsE9Mx +4XTsEQsf/+gkEY5J3V704RiiwDl/1a6P8c1aDnchAoGALEDzByXeADMiYjKi6M19 +1WK3+TDD9Sy8fu4x2qmTho9Z9nk5bw6ZPHbXDTaQ+jxnOD4Io6iZIQLEYMwzbXnO +951+ck9E5mwWo/IyNROOMo0aNT9yqLANu5Hp1CliQ5Yqmb1R1Qhuk4SZTWmUGjo/ +3I+uWHi2Foc2FU8LSAb4hLk= +-----END PRIVATE KEY----- diff --git a/server/tests/testdata/config/config_full.yaml b/server/tests/testdata/config/config_full.yaml new file mode 100644 index 0000000..a4c1d42 --- /dev/null +++ b/server/tests/testdata/config/config_full.yaml @@ -0,0 +1,26 @@ +external-api: + port: 8080 + ipv4: 127.0.0.1 + ipv6: ::1 + static_dir: server/static/ +internal-api: + port: 8081 + ipv4: 127.0.0.1 + token: "supersecrettoken" + tls: + cert: "/path/to/cert.pem" + key: "/path/to/key.pem" + static_dir: server/static/ +wallet: + primary-address: "4613YiHLM6JMH4zejMB2zJY5TwQCxL8p65ufw8kBP5yxX9itmuGLqp1dS4tkVoTxjyH3aYhYNrtGHbQzJQP5bFus3KHVdmf" + private-viewkey: "ad2093a5705b9f33e6f0f0c1bc1f5f639c756cdfc168c8f2ac6127ccbdab3a03" + restore-height: 2947000 +daemon: + url: "https://node.example.com:18081" + login: + username: "pinkpanther" + password: "supersecretpassword" +database: + path: "server/tests/AcceptXMR_DB/" +logging: + verbosity: "Debug" diff --git a/server/tests/testdata/config/config_no_secrets.yaml b/server/tests/testdata/config/config_no_secrets.yaml new file mode 100644 index 0000000..d2c695b --- /dev/null +++ b/server/tests/testdata/config/config_no_secrets.yaml @@ -0,0 +1,23 @@ +external-api: + port: 8080 + ipv4: 127.0.0.1 + ipv6: ::1 + static_dir: server/static/ +internal-api: + port: 8081 + ipv4: 127.0.0.1 + tls: + cert: "/path/to/cert.pem" + key: "/path/to/key.pem" + static_dir: server/static/ +wallet: + primary-address: "4613YiHLM6JMH4zejMB2zJY5TwQCxL8p65ufw8kBP5yxX9itmuGLqp1dS4tkVoTxjyH3aYhYNrtGHbQzJQP5bFus3KHVdmf" + restore-height: 2947000 +daemon: + url: "https://node.example.com:18081" + login: + username: "pinkpanther" +database: + path: "server/tests/AcceptXMR_DB/" +logging: + verbosity: "Debug"